feat(extension): in-app update prompt — popup banner + toolbar badge (#1489)
CI / lint (push) Successful in 3s
extension / lint (push) Successful in 10s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 39s
CI / integration (push) Successful in 3m51s
extension / lint (pull_request) Successful in 10s
CI / lint (push) Successful in 3s
extension / lint (push) Successful in 10s
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 39s
CI / integration (push) Successful in 3m51s
extension / lint (pull_request) Successful in 10s
The extension is installed per-instance from the operator's FC host, so Firefox's
static update_url can't apply (each instance has a different host) and updates
were fully manual. Add a self-hosted-friendly update surface that reuses the
existing public GET /api/extension/manifest ({version, latest_url, sha256}):
- lib/api.js: getExtensionManifest().
- background.js: checkForUpdateInfo() compares the instance's latest published
version against runtime.getManifest().version (dotted-numeric compare so
1.0.10 > 1.0.9); CHECK_UPDATE message handler; refreshUpdateBadge() sets a
toolbar badge via browser.action; a daily browser.alarms check plus on
startup/installed. New 'alarms' permission (non-prompting).
- popup: an 'Update available — vX' banner with an Update button that opens the
signed XPI (web root, /api stripped like OPEN_ARTIST_PAGE) → Firefox's native
install prompt. Never blocks the popup on a failed check.
No backend changes (endpoint already exists). Bump 1.0.8→1.0.9 so this ships;
from here on updates surface themselves instead of needing a manual reinstall.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,69 @@ browser.runtime.onInstalled.addListener(() => ensureInitialized());
|
||||
browser.runtime.onStartup.addListener(() => ensureInitialized());
|
||||
ensureInitialized().catch(e => console.error('init failed:', e));
|
||||
|
||||
// ---- Extension self-update check (#1489) ----
|
||||
// Installed per-instance from the operator's FC host, so Firefox's static
|
||||
// update_url can't apply (each instance has a different host). Instead ask the
|
||||
// configured backend for the latest published version and nudge the operator to
|
||||
// reinstall the freshly-signed XPI — surfaced as a popup banner (on demand) and
|
||||
// a toolbar badge (daily). /api/extension/manifest is public and returns
|
||||
// {version, latest_url, sha256}; the XPI is served from the web root (not /api).
|
||||
|
||||
function versionIsNewer(candidate, current) {
|
||||
// Dotted numeric compare so 1.0.10 > 1.0.9 (a plain string compare wouldn't).
|
||||
const a = String(candidate).split('.').map(n => parseInt(n, 10) || 0);
|
||||
const b = String(current).split('.').map(n => parseInt(n, 10) || 0);
|
||||
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
||||
if ((a[i] || 0) !== (b[i] || 0)) return (a[i] || 0) > (b[i] || 0);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function checkForUpdateInfo() {
|
||||
await ensureInitialized();
|
||||
if (!api.isConfigured()) return { updateAvailable: false, configured: false };
|
||||
let info;
|
||||
try {
|
||||
info = await api.getExtensionManifest();
|
||||
} catch (e) {
|
||||
return { updateAvailable: false, error: e.message };
|
||||
}
|
||||
const currentVersion = browser.runtime.getManifest().version;
|
||||
const latestVersion = info && info.version ? info.version : null;
|
||||
// latest_url is served from the web root; strip the /api suffix off baseUrl
|
||||
// (same transform as OPEN_ARTIST_PAGE).
|
||||
const base = (api.baseUrl || '').replace(/\/+$/, '').replace(/\/api$/, '');
|
||||
return {
|
||||
updateAvailable: !!latestVersion && versionIsNewer(latestVersion, currentVersion),
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
xpiUrl: info && info.latest_url ? `${base}${info.latest_url}` : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshUpdateBadge() {
|
||||
let r;
|
||||
try { r = await checkForUpdateInfo(); } catch { return; }
|
||||
try {
|
||||
await browser.action.setBadgeText({ text: r.updateAvailable ? '↑' : '' });
|
||||
if (r.updateAvailable) {
|
||||
await browser.action.setBadgeBackgroundColor({ color: '#F4BA7A' });
|
||||
await browser.action.setTitle({ title: `FabledCurator — update available (v${r.latestVersion})` });
|
||||
} else {
|
||||
await browser.action.setTitle({ title: 'FabledCurator' });
|
||||
}
|
||||
} catch { /* action API unavailable — non-fatal */ }
|
||||
}
|
||||
|
||||
// Daily proactive check (needs the "alarms" permission). create() is idempotent
|
||||
// by name, so re-running it on each event-page load is safe.
|
||||
browser.alarms.create('fc-update-check', { periodInMinutes: 24 * 60, delayInMinutes: 1 });
|
||||
browser.alarms.onAlarm.addListener((alarm) => {
|
||||
if (alarm.name === 'fc-update-check') refreshUpdateBadge();
|
||||
});
|
||||
browser.runtime.onStartup.addListener(() => refreshUpdateBadge());
|
||||
browser.runtime.onInstalled.addListener(() => refreshUpdateBadge());
|
||||
|
||||
// ---- Discord token capture via webRequest ----
|
||||
|
||||
browser.webRequest.onBeforeSendHeaders.addListener(
|
||||
@@ -298,6 +361,9 @@ browser.runtime.onMessage.addListener(async (msg) => {
|
||||
}
|
||||
}
|
||||
|
||||
case 'CHECK_UPDATE':
|
||||
return await checkForUpdateInfo();
|
||||
|
||||
default:
|
||||
return { error: `Unknown message type: ${msg.type}` };
|
||||
}
|
||||
|
||||
@@ -89,6 +89,12 @@ class FabledCuratorAPI {
|
||||
const qs = new URLSearchParams({ url }).toString();
|
||||
return this.request('GET', `/extension/probe?${qs}`);
|
||||
}
|
||||
// Latest published extension version on this instance — drives the in-app
|
||||
// update prompt. Public endpoint (no key needed, but request() sends it
|
||||
// harmlessly). Returns {version, xpi_url, latest_url, sha256}.
|
||||
getExtensionManifest() {
|
||||
return this.request('GET', '/extension/manifest');
|
||||
}
|
||||
|
||||
// Connection test = the cheapest read with auth.
|
||||
testConnection() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "FabledCurator",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"description": "Export cookies from supported platforms to FabledCurator and add creators as sources in one click.",
|
||||
|
||||
"browser_specific_settings": {
|
||||
@@ -22,7 +22,8 @@
|
||||
"tabs",
|
||||
"activeTab",
|
||||
"webRequest",
|
||||
"webRequestBlocking"
|
||||
"webRequestBlocking",
|
||||
"alarms"
|
||||
],
|
||||
|
||||
"host_permissions": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "fabledcurator-extension",
|
||||
"version": "1.0.8",
|
||||
"version": "1.0.9",
|
||||
"private": true,
|
||||
"description": "Firefox extension for FabledCurator",
|
||||
"scripts": {
|
||||
|
||||
@@ -72,6 +72,17 @@ body {
|
||||
.btn.block { display: block; width: 100%; margin-top: 8px; }
|
||||
.btn.link { background: none; color: var(--on-surface-variant); padding: 4px; }
|
||||
.btn.link:hover { color: var(--accent); }
|
||||
.btn.small { padding: 6px 12px; font-size: 13px; }
|
||||
|
||||
/* In-app update prompt (accent-tinted so it reads as an actionable notice). */
|
||||
.update-banner {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
margin: 10px 10px 0; padding: 10px 12px;
|
||||
background: rgba(244, 186, 122, 0.12);
|
||||
border: 1px solid rgba(244, 186, 122, 0.4);
|
||||
border-radius: 6px;
|
||||
}
|
||||
#update-text { flex: 1; font-size: 13px; }
|
||||
|
||||
.source-row .play {
|
||||
background: none; border: none; color: var(--on-surface-variant);
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
</section>
|
||||
|
||||
<section id="main-content" class="main hidden">
|
||||
<div id="update-banner" class="update-banner hidden">
|
||||
<span id="update-text"></span>
|
||||
<button id="update-btn" class="btn primary small">Update</button>
|
||||
</div>
|
||||
|
||||
<nav class="tabs">
|
||||
<button class="tab active" data-tab="platforms">Platforms</button>
|
||||
<button class="tab" data-tab="sources">Sources</button>
|
||||
|
||||
@@ -14,6 +14,7 @@ async function init() {
|
||||
setupEventListeners();
|
||||
showPlatformsLoading();
|
||||
testConnectionIfNeeded();
|
||||
checkForUpdate();
|
||||
loadPlatformStatus().catch(e => showError(`Failed to load platforms: ${e.message}`));
|
||||
} catch (e) {
|
||||
showSetupRequired();
|
||||
@@ -63,6 +64,26 @@ function updateConnectionDot(connected) {
|
||||
d.title = connected ? 'Connected to FabledCurator' : 'Disconnected';
|
||||
}
|
||||
|
||||
// Nudge to reinstall when the configured instance publishes a newer signed XPI
|
||||
// (the extension is self-hosted, so there's no Firefox auto-update). Never
|
||||
// blocks the popup — a failed check just leaves the banner hidden.
|
||||
async function checkForUpdate() {
|
||||
try {
|
||||
const r = await browser.runtime.sendMessage({ type: 'CHECK_UPDATE' });
|
||||
if (r && r.updateAvailable && r.xpiUrl) showUpdateBanner(r);
|
||||
} catch { /* non-fatal */ }
|
||||
}
|
||||
|
||||
function showUpdateBanner(r) {
|
||||
document.getElementById('update-text').textContent =
|
||||
`Update available — v${r.latestVersion} (installed v${r.currentVersion})`;
|
||||
// Opening the signed XPI triggers Firefox's native install prompt.
|
||||
document.getElementById('update-btn').addEventListener('click', () => {
|
||||
browser.tabs.create({ url: r.xpiUrl });
|
||||
});
|
||||
document.getElementById('update-banner').classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function loadPlatformStatus() {
|
||||
const status = await browser.runtime.sendMessage({ type: 'GET_PLATFORM_STATUS' });
|
||||
const c = document.getElementById('platforms-list');
|
||||
|
||||
Reference in New Issue
Block a user