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:
+54
-9
@@ -1360,7 +1360,7 @@
|
|||||||
// free-tier users, surface the upgrade prompt instead of opening an
|
// free-tier users, surface the upgrade prompt instead of opening an
|
||||||
// empty sidebar.
|
// empty sidebar.
|
||||||
if (!hasEntitlement("history")) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
toggleHistory();
|
toggleHistory();
|
||||||
@@ -1369,7 +1369,7 @@
|
|||||||
function handleSubscribeClick() {
|
function handleSubscribeClick() {
|
||||||
// Subscriptions / channel auto-queue is Pro-only.
|
// Subscriptions / channel auto-queue is Pro-only.
|
||||||
if (!hasEntitlement("subscriptions")) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
addSubscriptionFromInput();
|
addSubscriptionFromInput();
|
||||||
@@ -1666,10 +1666,37 @@
|
|||||||
render();
|
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 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 `
|
return `
|
||||||
<div class="free-banner" style="
|
<div class="upgrade-banner" style="
|
||||||
margin: 8px 0 12px;
|
margin: 8px 0 12px;
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
background: linear-gradient(90deg, rgba(168,85,247,0.12), rgba(99,102,241,0.10));
|
background: linear-gradient(90deg, rgba(168,85,247,0.12), rgba(99,102,241,0.10));
|
||||||
@@ -1683,18 +1710,19 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
">
|
">
|
||||||
<span style="flex:1; min-width: 220px;">
|
<span style="flex:1; min-width: 220px;">
|
||||||
<strong style="color:#c4b5fd;">Free mode</strong>
|
<strong style="color:#c4b5fd;">${label}</strong>
|
||||||
· one video at a time ·
|
· ${descr}
|
||||||
no library, no subscriptions
|
|
||||||
</span>
|
</span>
|
||||||
<a href="${escHtml(buyUrl)}" target="_blank" rel="noopener"
|
<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;">
|
style="background:#a855f7;color:#fff;border:none;padding:6px 12px;border-radius:6px;text-decoration:none;font-size:12px;font-weight:600;">
|
||||||
Upgrade
|
Upgrade
|
||||||
</a>
|
</a>
|
||||||
|
${free ? `
|
||||||
<button onclick="showActivationScreen()"
|
<button onclick="showActivationScreen()"
|
||||||
style="background:transparent;color:#94a3b8;border:1px solid #334155;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:12px;">
|
style="background:transparent;color:#94a3b8;border:1px solid #334155;padding:6px 10px;border-radius:6px;cursor:pointer;font-size:12px;">
|
||||||
I have a key
|
I have a key
|
||||||
</button>
|
</button>
|
||||||
|
` : ""}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -1741,7 +1769,9 @@
|
|||||||
// a server-side key set via the StartOS config action.
|
// a server-side key set via the StartOS config action.
|
||||||
const submitDisabled = !state.url.trim()
|
const submitDisabled = !state.url.trim()
|
||||||
|| (!isSubscribeUrl(state.url) && !state.apiKey.trim() && !state.hasServerKey);
|
|| (!isSubscribeUrl(state.url) && !state.apiKey.trim() && !state.hasServerKey);
|
||||||
app.innerHTML = `
|
let __renderedHtml;
|
||||||
|
try {
|
||||||
|
__renderedHtml = `
|
||||||
<!-- Top bar: title + action icons -->
|
<!-- Top bar: title + action icons -->
|
||||||
<div class="top-bar">
|
<div class="top-bar">
|
||||||
<div class="top-left-actions">
|
<div class="top-left-actions">
|
||||||
@@ -1835,7 +1865,7 @@
|
|||||||
<!-- Settings modal -->
|
<!-- Settings modal -->
|
||||||
${state.settingsOpen ? renderSettingsModal() : ""}
|
${state.settingsOpen ? renderSettingsModal() : ""}
|
||||||
|
|
||||||
${free ? renderFreeBanner() : ""}
|
${shouldShowUpgradeBanner() ? renderUpgradeBanner() : ""}
|
||||||
|
|
||||||
${isSubscribeUrl(state.url) ? `<div id="subscribe-prompt">${renderSubscribePrompt()}</div>` : ""}
|
${isSubscribeUrl(state.url) ? `<div id="subscribe-prompt">${renderSubscribePrompt()}</div>` : ""}
|
||||||
${state.queue.length > 0 ? renderQueue() : ""}
|
${state.queue.length > 0 ? renderQueue() : ""}
|
||||||
@@ -1852,6 +1882,21 @@
|
|||||||
${state.historyOpen ? renderHistorySidebar() : ""}
|
${state.historyOpen ? renderHistorySidebar() : ""}
|
||||||
${state.clipPanelOpen ? renderClipPanel() : ""}
|
${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
|
// Re-init player if the yt-player div exists
|
||||||
if (state.videoId && ytReady && document.getElementById("yt-player")) {
|
if (state.videoId && ytReady && document.getElementById("yt-player")) {
|
||||||
setTimeout(() => initPlayer(state.videoId), 50);
|
setTimeout(() => initPlayer(state.videoId), 50);
|
||||||
|
|||||||
Reference in New Issue
Block a user