UI: persistent upgrade banner; surface render errors
Two bugs reported in 0.1.16 testing:
1. Library button shows a toast "Library is a paid feature ... Tap
upgrade to unlock" but no Upgrade button is visible anywhere on the
page. The free-tier banner only rendered when isLicensed() was false,
so a licensed-but-missing-entitlements user got the toast directing
them at an invisible button.
2. Clicking the gear (Settings) icon does nothing — the modal never
appears. The Activity Log button works on the same page.
Fix 1: replace the unlicensed-only renderFreeBanner with renderUpgrade
Banner driven by shouldShowUpgradeBanner() = !isProTier(). Copy
adapts by tier:
• Free: "Free mode · one video at a time · no library, no subs"
• Limited license (missing core entitlements): "Limited license
— your license is missing some Core features"
• Core tier: "Core tier — upgrade to Pro for subs, auto-queue,
clips"
Pro tier shows nothing.
Fix 1b: rewordtoasts on Library / Subscribe clicks to drop the
"tap Upgrade" instruction — the persistent banner is the action
path now, no need to direct users at a maybe-not-there button.
Fix 2 (diagnostic): wrap the main app.innerHTML build in try/catch.
A thrown exception in any of the ${...} template substitutions
silently aborts the innerHTML assignment, leaving the previous
DOM in place — which looks exactly like a button doing nothing
when it actually fired and called render(). Now an exception
lands in a visible error-box with the message + console trace,
so the next reproduction tells us what's actually throwing.
This commit is contained in:
+58
-13
@@ -1360,7 +1360,7 @@
|
||||
// free-tier users, surface the upgrade prompt instead of opening an
|
||||
// empty sidebar.
|
||||
if (!hasEntitlement("history")) {
|
||||
showToast("Library is a paid feature — keep every summary you process. Tap upgrade to unlock.", "📚");
|
||||
showToast("Library requires a paid license — every summary you process gets saved.", "📚");
|
||||
return;
|
||||
}
|
||||
toggleHistory();
|
||||
@@ -1369,7 +1369,7 @@
|
||||
function handleSubscribeClick() {
|
||||
// Subscriptions / channel auto-queue is Pro-only.
|
||||
if (!hasEntitlement("subscriptions")) {
|
||||
showToast("Channel & podcast subscriptions are a Pro feature. Upgrade to auto-summarize new uploads.", "📡");
|
||||
showToast("Channel & podcast subscriptions are a Pro-tier feature.", "📡");
|
||||
return;
|
||||
}
|
||||
addSubscriptionFromInput();
|
||||
@@ -1666,10 +1666,37 @@
|
||||
render();
|
||||
}
|
||||
|
||||
function renderFreeBanner() {
|
||||
// True for anyone who isn't on the top tier — drives whether to show
|
||||
// the persistent upgrade banner. Covers: unlicensed, unlicensed-skipped,
|
||||
// Core (full or partial), and any "licensed but missing entitlements"
|
||||
// anomaly. Pro users see no banner.
|
||||
function shouldShowUpgradeBanner() {
|
||||
if (!state.license || !state.license.loaded) return false;
|
||||
return !isProTier();
|
||||
}
|
||||
|
||||
function renderUpgradeBanner() {
|
||||
const buyUrl = upgradeToProUrl();
|
||||
const free = !isLicensed();
|
||||
const partialCore = isLicensed() && (!hasEntitlement("history") || !hasEntitlement("library"));
|
||||
const fullCore = isLicensed() && hasEntitlement("history") && hasEntitlement("library") && !isProTier();
|
||||
|
||||
let label, descr;
|
||||
if (free) {
|
||||
label = "Free mode";
|
||||
descr = "one video at a time · no library, no subscriptions";
|
||||
} else if (partialCore) {
|
||||
label = "Limited license";
|
||||
descr = "your license is missing some Core features — contact the seller or upgrade";
|
||||
} else if (fullCore) {
|
||||
label = "Core tier";
|
||||
descr = "upgrade to Pro for channel & podcast subscriptions, auto-queue, and clips";
|
||||
} else {
|
||||
return ""; // shouldn't reach
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="free-banner" style="
|
||||
<div class="upgrade-banner" style="
|
||||
margin: 8px 0 12px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(90deg, rgba(168,85,247,0.12), rgba(99,102,241,0.10));
|
||||
@@ -1683,18 +1710,19 @@
|
||||
font-size: 13px;
|
||||
">
|
||||
<span style="flex:1; min-width: 220px;">
|
||||
<strong style="color:#c4b5fd;">Free mode</strong>
|
||||
· one video at a time ·
|
||||
no library, no subscriptions
|
||||
<strong style="color:#c4b5fd;">${label}</strong>
|
||||
· ${descr}
|
||||
</span>
|
||||
<a href="${escHtml(buyUrl)}" target="_blank" rel="noopener"
|
||||
style="background:#a855f7;color:#fff;border:none;padding:6px 12px;border-radius:6px;text-decoration:none;font-size:12px;font-weight:600;">
|
||||
Upgrade
|
||||
</a>
|
||||
<button onclick="showActivationScreen()"
|
||||
style="background:transparent;color:#94a3b8;border:1px solid #334155;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:12px;">
|
||||
I have a key
|
||||
</button>
|
||||
${free ? `
|
||||
<button onclick="showActivationScreen()"
|
||||
style="background:transparent;color:#94a3b8;border:1px solid #334155;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:12px;">
|
||||
I have a key
|
||||
</button>
|
||||
` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1741,7 +1769,9 @@
|
||||
// a server-side key set via the StartOS config action.
|
||||
const submitDisabled = !state.url.trim()
|
||||
|| (!isSubscribeUrl(state.url) && !state.apiKey.trim() && !state.hasServerKey);
|
||||
app.innerHTML = `
|
||||
let __renderedHtml;
|
||||
try {
|
||||
__renderedHtml = `
|
||||
<!-- Top bar: title + action icons -->
|
||||
<div class="top-bar">
|
||||
<div class="top-left-actions">
|
||||
@@ -1835,7 +1865,7 @@
|
||||
<!-- Settings modal -->
|
||||
${state.settingsOpen ? renderSettingsModal() : ""}
|
||||
|
||||
${free ? renderFreeBanner() : ""}
|
||||
${shouldShowUpgradeBanner() ? renderUpgradeBanner() : ""}
|
||||
|
||||
${isSubscribeUrl(state.url) ? `<div id="subscribe-prompt">${renderSubscribePrompt()}</div>` : ""}
|
||||
${state.queue.length > 0 ? renderQueue() : ""}
|
||||
@@ -1852,6 +1882,21 @@
|
||||
${state.historyOpen ? renderHistorySidebar() : ""}
|
||||
${state.clipPanelOpen ? renderClipPanel() : ""}
|
||||
`;
|
||||
app.innerHTML = __renderedHtml;
|
||||
} catch (renderErr) {
|
||||
// A thrown exception inside any ${...} expression silently aborts
|
||||
// the innerHTML assignment, leaving the previous DOM in place — so
|
||||
// a button click that calls render() looks like a no-op. Surface
|
||||
// the actual error instead of leaving the user wondering.
|
||||
console.error("[render] failed to build HTML:", renderErr);
|
||||
app.innerHTML = `<div class="error-box" style="margin:20px;">
|
||||
<strong>Render error:</strong> ${escHtml(renderErr && renderErr.message || String(renderErr))}
|
||||
<div style="font-size:11px;margin-top:6px;opacity:0.7;">
|
||||
Check the browser console for the full stack trace, or share it with the developer.
|
||||
</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
// Re-init player if the yt-player div exists
|
||||
if (state.videoId && ytReady && document.getElementById("yt-player")) {
|
||||
setTimeout(() => initPlayer(state.videoId), 50);
|
||||
|
||||
Reference in New Issue
Block a user