Refactor extension to account-auth flow and health-only diagnostics

This commit is contained in:
Dwindi Ramadhana
2026-02-18 17:52:13 +07:00
parent 901f81b7a9
commit bcbbab7922
4 changed files with 202 additions and 287 deletions

View File

@@ -62,7 +62,25 @@ Expected:
- `X-Dewemoji-Tier` can be read when present.
- UI remains stable when header is absent.
## Test 7: Regression Smoke
## Test 7: Account Connect (New)
1. Open Settings -> Account tab.
2. Enter email and password for a Dewemoji account.
3. Click `Connect`.
Expected:
- Status shows `Connected as <email> (Free|Personal)`.
- Header `Authorization: Bearer ...` is sent on search requests.
- Top tag changes to `Free` or `Personal` (not `Pro`).
## Test 8: Account Disconnect
1. In Settings -> Account tab, click `Logout`.
Expected:
- Account status returns to `Not connected`.
- Top tag changes to `Guest`.
- Search still works for public keywords.
## Test 9: Regression Smoke
1. Insert/copy actions still work.
2. Settings panel opens and saves.
3. No new console syntax/runtime errors.

View File

@@ -12,6 +12,7 @@
],
"host_permissions": [
"<all_urls>",
"https://dewemoji.backoffice.biz.id/*",
"https://api.dewemoji.com/*"
],
"web_accessible_resources": [

View File

@@ -19,7 +19,7 @@
</head>
<body>
<header class="hdr">
<h1 class="ttl">Find Your Emoji <span id="dewemoji-status" class="tag free">Free</span></h1>
<h1 class="ttl">Find Your Emoji <span id="dewemoji-status" class="tag free">Guest</span></h1>
<div class="bar">
<input id="q" class="inp" type="text" placeholder="Search (e.g. love)" aria-label="Search emojis" />
<button id="clear" class="btn icon" title="Clear" aria-label="Clear"></button>
@@ -47,7 +47,7 @@
<!-- Settings sheet -->
<div id="sheet-backdrop" class="backdrop" hidden></div>
<aside id="settings-sheet" class="sheet" hidden role="dialog" aria-modal="true" aria-labelledby="settings-title" aria-describedby="license-status">
<aside id="settings-sheet" class="sheet" hidden role="dialog" aria-modal="true" aria-labelledby="settings-title" aria-describedby="account-status">
<div class="sheet-head">
<h3 id="settings-title">Settings</h3>
<button id="sheet-close" class="btn icon" title="Close" aria-label="Close"></button>
@@ -56,7 +56,7 @@
<div class="sheet-body">
<div class="tabs">
<button class="tab active" data-tab="general">General</button>
<button class="tab" data-tab="pro">Pro</button>
<button class="tab" data-tab="pro">Account</button>
</div>
<div id="tab-general" class="tabpane active">
@@ -72,12 +72,12 @@
<label class="radio">
<input type="radio" name="actionMode" value="insert">
<span>Insert to input</span>
<span class="tag pro">Pro</span>
<span class="tag free">Available</span>
</label>
<label class="radio">
<input type="radio" name="actionMode" value="auto">
<span>Automatic</span>
<span class="tag pro">Pro</span>
<span class="tag free">Available</span>
</label>
</div>
<p class="hint muted">Automatic tries to insert at the last caret; if not possible, it copies instead.</p>
@@ -85,23 +85,20 @@
</div>
<div id="tab-pro" class="tabpane">
<!-- License (first) -->
<div class="field">
<label class="lbl">License</label>
<label class="lbl">Account</label>
<div class="row">
<input id="license-key" class="inp" placeholder="Enter license key" type="password" autocomplete="off">
<button id="license-activate" class="btn">Activate</button>
<button id="license-deactivate" class="btn ghost">Deactivate</button>
<input id="account-email" class="inp" placeholder="Email" type="email" autocomplete="username">
</div>
<div class="row" style="margin-top:6px;">
<button id="license-edit" class="link" style="width: fit-content">Change key</button>
<button id="license-cancel-edit" class="link" style="display:none;width: fit-content">Cancel</button>
<span id="license-status" class="muted">
Free mode — Pro features locked.
<br><a href="https://dewemoji.com/pricing" target="_blank" class="cta-link" style="margin-left:6px; text-decoration: underline; font-weight: 500;">
🔓 Upgrade to Pro
</a>
</span>
<input id="account-password" class="inp" placeholder="Password" type="password" autocomplete="current-password">
</div>
<div class="row">
<button id="account-login" class="btn">Connect</button>
<button id="account-logout" class="btn ghost">Logout</button>
</div>
<div class="row" style="margin-top:6px;">
<span id="account-status" class="muted">Not connected. Public keywords only.</span>
</div>
</div>
@@ -109,14 +106,13 @@
<!-- Diagnostics -->
<div class="field">
<label class="lbl">Diagnostics</label>
<label class="lbl">Extension Health</label>
<div class="row">
<button id="diag-run" class="btn">Run diagnostics</button>
<button id="diag-force" class="btn ghost">Force token refresh</button>
<button id="diag-run" class="btn">Run health check</button>
<span id="diag-spin" class="muted" style="display:none;">Running…</span>
</div>
<div id="diag-out" class="diagbox muted"></div>
<p class="hint muted">Tip: Click in a text box on the page first, then run diagnostics.</p>
<p class="hint muted">Tip: Click in a text box on the page first, then run health check.</p>
</div>
</div>
</div>

428
panel.js
View File

@@ -1,10 +1,12 @@
// === License & mode (stub) ===
// Flip this to true for local testing of Pro features if you want:
let licenseValid = false; // <- default: Free
// Feature access is now free in extension UI. Billing applies to private keywords on backend.
let licenseValid = true;
// Persistent settings
let actionMode = 'copy'; // 'copy' | 'insert' | 'auto'
let licenseKeyCurrent = '';
let authApiKey = '';
let authTier = 'guest'; // guest | free | personal
let authEmail = '';
// Tone settings (persisted in sync storage)
let toneLock = false; // if true, always use preferred tone
@@ -22,6 +24,11 @@ const licenseStatusEl = document.getElementById('license-status');
const licenseDeactivateBtn = document.getElementById('license-deactivate');
const licenseEditBtn = document.getElementById('license-edit');
const licenseCancelEditBtn = document.getElementById('license-cancel-edit');
const accountEmailEl = document.getElementById('account-email');
const accountPasswordEl = document.getElementById('account-password');
const accountLoginBtn = document.getElementById('account-login');
const accountLogoutBtn = document.getElementById('account-logout');
const accountStatusEl = document.getElementById('account-status');
// --- Branded confirm modal helper ---
function showConfirmModal(opts = {}) {
@@ -107,18 +114,17 @@ function setLicenseBusy(on, label){
}
const diagRunBtn = document.getElementById('diag-run');
const diagForceBtn = document.getElementById('diag-force');
const diagSpin = document.getElementById('diag-spin');
const diagOut = document.getElementById('diag-out');
const API = {
base: "https://api.dewemoji.com/v1",
base: "https://dewemoji.backoffice.biz.id/v1",
list: "/emojis"
};
API.cats = "/categories";
let PAGE_LIMIT = 20; // Free default
let PAGE_LIMIT = 50;
function refreshPageLimit(){
PAGE_LIMIT = licenseValid ? 50 : 20; // Pro gets bigger pages
PAGE_LIMIT = 50;
}
refreshPageLimit();
const FRONTEND_ID = 'ext-v1';
@@ -580,6 +586,7 @@ function debounced(fn, delay=250){ clearTimeout(timer); timer = setTimeout(fn, d
// === P0: Usage & Cache ===
const CLIENT_FREE_DAILY_LIMIT = 30; // can be tuned; Pro => unlimited
const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
const ENABLE_RESULT_CACHE = false; // always fetch latest from API
const QUERY_CACHE = new Map(); // in-memory cache: key => {ts, data}
const PREFETCHING = new Set(); // tracks in-flight prefetch keys (sig|page)
@@ -614,33 +621,23 @@ function signatureFor(qVal, catVal, subVal){
}
async function getPersistedCache(){
if (!ENABLE_RESULT_CACHE) return {};
const got = await chrome.storage.local.get(['searchCache']);
return got.searchCache || {}; // { key: { ts, data } }
}
async function savePersistedCache(cacheObj){
if (!ENABLE_RESULT_CACHE) return;
await chrome.storage.local.set({ searchCache: cacheObj });
}
function isFresh(ts){ return (Date.now() - ts) < CACHE_TTL_MS; }
function isFresh(ts){ return ENABLE_RESULT_CACHE && (Date.now() - ts) < CACHE_TTL_MS; }
async function buildHeaders(){
const base = { 'X-Dewemoji-Frontend': FRONTEND_ID };
try {
if (chrome?.runtime?.id) base['X-Extension-Id'] = chrome.runtime.id;
} catch {}
if (licenseValid && licenseKeyCurrent) {
try {
const acctPromise = accountId();
return acctPromise.then(acct => ({
...base,
// New preferred auth
'Authorization': `Bearer ${licenseKeyCurrent}`,
// Legacy headers kept for backward compatibility
'X-License-Key': licenseKeyCurrent,
'X-Account-Id': acct.id
})).catch(()=>base);
} catch {
return Promise.resolve({ ...base, 'Authorization': `Bearer ${licenseKeyCurrent}`, 'X-License-Key': licenseKeyCurrent });
}
if (authApiKey) {
base['Authorization'] = `Bearer ${authApiKey}`;
}
return base;
}
@@ -700,6 +697,7 @@ async function fetchJsonWithFallback(params, headers, { quiet = false } = {}) {
async function prefetchNextIfNeeded(currentSig){
if (!ENABLE_RESULT_CACHE) return;
try {
// only prefetch when there are more results and we know the next page index
const nextPage = page + 1;
@@ -747,27 +745,15 @@ async function fetchPage(reset=false) {
if (reset) { page = 1; items = []; list.innerHTML = ""; }
if (reset) { PREFETCHING.clear(); }
if (reset) { autoLoadBusy = false; }
if (reset && !ENABLE_RESULT_CACHE) {
QUERY_CACHE.clear();
try { await chrome.storage.local.remove(['searchCache']); } catch (_) {}
}
// Build signature and check limits only for page 1
const sig = signatureFor(q.value, cat.value, sub.value);
// Gate on soft cap for Free (Pro unlimited)
if (!licenseValid && page === 1) {
const usage = await getDailyUsage();
const limit = usage.limit ?? CLIENT_FREE_DAILY_LIMIT;
// Check persisted cache first; a cached result does not consume usage
const persisted = await getPersistedCache();
const entry = persisted[sig];
const hasFreshPersist = entry && isFresh(entry.ts);
const hasFreshMem = QUERY_CACHE.has(`${sig}|1`) && isFresh(QUERY_CACHE.get(`${sig}|1`).ts);
if (usage.used >= limit && !hasFreshPersist && !hasFreshMem) {
// At cap and no cache — block network to be fair to server & UX
showToast('Daily limit reached — Upgrade to Pro');
updateFooter();
return;
}
}
// Client-side cap disabled; backend is source of truth for rate/sanity limits.
try {
showSpinner();
@@ -776,12 +762,11 @@ async function fetchPage(reset=false) {
const key = `${sig}|${page}`;
// 1) Try memory cache
const mem = QUERY_CACHE.get(key);
if (mem && isFresh(mem.ts)) {
const mem = ENABLE_RESULT_CACHE ? QUERY_CACHE.get(key) : null;
if (ENABLE_RESULT_CACHE && mem && isFresh(mem.ts)) {
const data = mem.data;
total = data.total || 0;
for (const e of (data.items || [])) { items.push(e); renderCard(e); }
// Kick off background prefetch for the next page (cached only)
const currentSig = signatureFor(q.value, cat.value, sub.value);
prefetchNextIfNeeded(currentSig).catch(()=>{});
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
@@ -790,18 +775,20 @@ async function fetchPage(reset=false) {
}
// 2) Try persisted cache
const persisted = await getPersistedCache();
const ent = persisted[key];
if (ent && isFresh(ent.ts)) {
total = ent.data.total || 0;
for (const e of (ent.data.items || [])) { items.push(e); renderCard(e); }
QUERY_CACHE.set(key, { ts: ent.ts, data: ent.data }); // promote to mem
// Kick off background prefetch for the next page (cached only)
const currentSig = signatureFor(q.value, cat.value, sub.value);
prefetchNextIfNeeded(currentSig).catch(()=>{});
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
updateFooter();
return;
let ent = null;
if (ENABLE_RESULT_CACHE) {
const persisted = await getPersistedCache();
ent = persisted[key] || null;
if (ent && isFresh(ent.ts)) {
total = ent.data.total || 0;
for (const e of (ent.data.items || [])) { items.push(e); renderCard(e); }
QUERY_CACHE.set(key, { ts: ent.ts, data: ent.data }); // promote to mem
const currentSig = signatureFor(q.value, cat.value, sub.value);
prefetchNextIfNeeded(currentSig).catch(()=>{});
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
updateFooter();
return;
}
}
// 3) Network fetch
@@ -836,34 +823,27 @@ async function fetchPage(reset=false) {
return;
}
// Count usage only on successful network responses for Free, page 1, and when not cached
if (!licenseValid && page === 1) {
const cacheHas = !!(mem && isFresh(mem.ts)) || !!(ent && isFresh(ent.ts));
if (!cacheHas) {
const usage = await getDailyUsage();
const limit = usage.limit ?? CLIENT_FREE_DAILY_LIMIT;
usage.used = Math.min(limit, (usage.used || 0) + 1);
await setDailyUsage(usage);
}
}
// Client-side usage counter disabled.
// Save to caches
const record = { ts: Date.now(), data };
QUERY_CACHE.set(key, record);
const persisted2 = await getPersistedCache();
persisted2[key] = record;
// prune old entries occasionally (simple heuristic)
const now = Date.now();
for (const k of Object.keys(persisted2)) {
if (!persisted2[k] || !isFresh(persisted2[k].ts)) delete persisted2[k];
if (ENABLE_RESULT_CACHE) {
const record = { ts: Date.now(), data };
QUERY_CACHE.set(key, record);
const persisted2 = await getPersistedCache();
persisted2[key] = record;
for (const k of Object.keys(persisted2)) {
if (!persisted2[k] || !isFresh(persisted2[k].ts)) delete persisted2[k];
}
await savePersistedCache(persisted2);
}
await savePersistedCache(persisted2);
// Render
for (const e of (data.items || [])) { items.push(e); renderCard(e); }
// Kick off background prefetch for the next page (cached only)
const currentSig = signatureFor(q.value, cat.value, sub.value);
prefetchNextIfNeeded(currentSig).catch(()=>{});
if (ENABLE_RESULT_CACHE) {
const currentSig = signatureFor(q.value, cat.value, sub.value);
prefetchNextIfNeeded(currentSig).catch(()=>{});
}
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
updateFooter();
} catch (err) {
@@ -1253,34 +1233,32 @@ loadCategories().catch(console.error);
})();
async function loadSettings() {
const data = await chrome.storage.local.get(['licenseValid', 'licenseKey', 'actionMode']);
licenseValid = !!data.licenseValid;
const data = await chrome.storage.local.get(['actionMode', 'authApiKey', 'authTier', 'authEmail']);
licenseValid = true;
refreshPageLimit();
actionMode = data.actionMode || 'copy';
if (data.licenseKey) licenseKeyEl && (licenseKeyEl.value = data.licenseKey);
licenseKeyCurrent = data.licenseKey || '';
authApiKey = data.authApiKey || '';
authTier = data.authTier || 'guest';
authEmail = data.authEmail || '';
if (accountEmailEl && authEmail) accountEmailEl.value = authEmail;
applyLicenseUI();
applyModeUI();
updateAccountUI();
setStatusTag();
// tone settings from sync storage
await getPreferredToneIndex();
await getToneLock();
renderToneSettingsSection();
// ensure license field starts in non-edit mode
setLicenseEditMode(false);
}
async function saveSettings() {
await chrome.storage.local.set({ licenseValid, actionMode, licenseKey: licenseKeyEl?.value || '' });
await chrome.storage.local.set({ licenseValid: true, actionMode, authApiKey, authTier, authEmail });
}
// --- Helper to render Free status with CTA ---
function freeStatusHTML(){
return 'Free mode — Pro features locked.' +
'<br><a href="https://dewemoji.com/pricing" target="_blank" class="cta-link"' +
' style="margin-left:6px; text-decoration: underline; font-weight:500;">' +
'🔓 Upgrade to Pro' +
'</a>';
return 'All extension features are available. Connect account to use private keywords.';
}
function setLicenseEditMode(on) {
@@ -1309,47 +1287,51 @@ function setLicenseEditMode(on) {
}
function applyLicenseUI() {
// enable/disable Pro radios
// Extension features are available for everyone in current model.
const radios = modeGroup.querySelectorAll('input[type="radio"][name="actionMode"]');
radios.forEach(r => {
const isPro = (r.value === 'insert' || r.value === 'auto');
if (isPro) {
r.disabled = !licenseValid;
r.closest('.radio').setAttribute('aria-disabled', !licenseValid ? 'true' : 'false');
}
r.disabled = false;
r.closest('.radio')?.setAttribute('aria-disabled', 'false');
});
if (!licenseValid) {
// Force visible selection to 'copy' for clarity
const copy = modeGroup.querySelector('input[type="radio"][value="copy"]');
if (copy) copy.checked = true;
licenseStatusEl && (licenseStatusEl.innerHTML = freeStatusHTML());
} else {
// Restore selected mode if Pro; if current actionMode is pro, check it
const target = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`) ||
modeGroup.querySelector('input[type="radio"][value="auto"]');
if (target) target.checked = true;
licenseStatusEl && (licenseStatusEl.textContent = 'Pro active');
}
// Reset to non-edit view per license state
if (licenseValid) {
if (licenseKeyEl) licenseKeyEl.disabled = true;
if (licenseActivateBtn) { licenseActivateBtn.style.display = 'none'; licenseActivateBtn.textContent = 'Activate'; }
if (licenseDeactivateBtn) licenseDeactivateBtn.style.display = '';
if (licenseEditBtn) licenseEditBtn.style.display = '';
if (licenseCancelEditBtn) licenseCancelEditBtn.style.display = 'none';
} else {
if (licenseKeyEl) licenseKeyEl.disabled = false;
if (licenseActivateBtn) { licenseActivateBtn.style.display = ''; licenseActivateBtn.textContent = 'Activate'; }
if (licenseDeactivateBtn) licenseDeactivateBtn.style.display = 'none';
if (licenseEditBtn) licenseEditBtn.style.display = 'none';
if (licenseCancelEditBtn) licenseCancelEditBtn.style.display = 'none';
}
const target = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`) ||
modeGroup.querySelector('input[type="radio"][value="copy"]');
if (target) target.checked = true;
setStatusTag();
// --- PATCH: Refresh tone section after license state changes ---
try { renderToneSettingsSection(); } catch(_) {}
}
function updateAccountUI() {
if (!accountStatusEl) return;
if (!authApiKey) {
accountStatusEl.textContent = 'Not connected. Public keywords only.';
if (accountLoginBtn) {
accountLoginBtn.disabled = false;
accountLoginBtn.style.display = '';
accountLoginBtn.textContent = 'Connect';
}
if (accountLogoutBtn) {
accountLogoutBtn.disabled = true;
accountLogoutBtn.style.display = 'none';
}
if (accountEmailEl) accountEmailEl.disabled = false;
if (accountPasswordEl) accountPasswordEl.disabled = false;
return;
}
const tierLabel = authTier === 'personal' ? 'Personal' : 'Free';
accountStatusEl.textContent = `Connected as ${authEmail || 'user'} (${tierLabel})`;
if (accountLoginBtn) {
accountLoginBtn.disabled = true;
accountLoginBtn.style.display = 'none';
}
if (accountLogoutBtn) {
accountLogoutBtn.disabled = false;
accountLogoutBtn.style.display = '';
}
if (accountEmailEl) accountEmailEl.disabled = true;
if (accountPasswordEl) accountPasswordEl.disabled = true;
}
function applyModeUI() {
// Ensure the selected radio matches actionMode (if not locked)
const input = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`);
@@ -1520,125 +1502,80 @@ if (tabsWrap) {
modeGroup?.addEventListener('change', async (e) => {
if (e.target && e.target.name === 'actionMode') {
const val = e.target.value;
// If not licensed and user clicked a Pro option, bounce back to copy
if (!licenseValid && (val === 'insert' || val === 'auto')) {
showToast('Pro required for Insert/Automatic');
const copy = modeGroup.querySelector('input[value="copy"]');
if (copy) copy.checked = true;
actionMode = 'copy';
} else {
actionMode = val;
}
actionMode = val;
await saveSettings();
}
});
licenseActivateBtn?.addEventListener('click', async () => {
const key = (licenseKeyEl?.value || '').trim();
if (!key) { showToast('Enter a license key'); return; }
accountLoginBtn?.addEventListener('click', async () => {
if (authApiKey) {
updateAccountUI();
return;
}
const email = (accountEmailEl?.value || '').trim();
const password = (accountPasswordEl?.value || '').trim();
if (!email || !password) {
showToast('Enter email and password');
return;
}
try {
setLicenseBusy(true, 'Verifying…');
// Call your API /license/verify (works for Gumroad and Mayar)
const acct = await accountId();
const res = await fetch('https://api.dewemoji.com/v1/license/verify', {
accountLoginBtn.disabled = true;
accountLoginBtn.textContent = 'Connecting...';
const res = await fetch(`${API.base}/user/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, account_id: acct.id, version: chrome.runtime.getManifest().version })
body: JSON.stringify({ email, password }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.ok) throw new Error(data?.error || `Verify ${res.status}`);
if (!res.ok || !data?.ok) {
throw new Error(data?.error || `Login ${res.status}`);
}
licenseValid = true;
refreshPageLimit();
licenseKeyCurrent = key;
await chrome.storage.local.set({ licenseValid: true, licenseKey: key, lastLicenseCheck: Date.now() });
applyLicenseUI();
setLicenseEditMode(false);
authApiKey = data?.api_key || '';
authTier = String(data?.user?.tier || 'free');
authEmail = String(data?.user?.email || email);
await saveSettings();
updateAccountUI();
setStatusTag();
// PATCH: Immediately re-render tone section to enable controls after activation
renderToneSettingsSection();
showToast('License activated ✓');
// Refresh results so Pro headers/tier take effect immediately
if (accountPasswordEl) accountPasswordEl.value = '';
showToast('Account connected');
fetchPage(true).catch(console.error);
setLicenseBusy(false);
} catch (e) {
setLicenseBusy(false);
licenseValid = false;
await chrome.storage.local.set({ licenseValid: false });
applyLicenseUI();
setStatusTag();
showToast(`Activation failed${e?.message ? ': ' + e.message : ''}`);
showToast(`Connect failed${e?.message ? ': ' + e.message : ''}`);
} finally {
accountLoginBtn.disabled = false;
accountLoginBtn.textContent = 'Connect';
}
});
licenseEditBtn?.addEventListener('click', () => {
setLicenseEditMode(true);
});
licenseCancelEditBtn?.addEventListener('click', async () => {
const data = await chrome.storage.local.get(['licenseKey']);
if (licenseKeyEl) licenseKeyEl.value = data.licenseKey || '';
setLicenseEditMode(false);
});
licenseDeactivateBtn?.addEventListener('click', async () => {
if (!licenseValid) return;
const ok = await showConfirmModal({
title: 'Deactivate Dewemoji Pro?',
message: 'Deactivate Pro on this device?',
okText: 'Deactivate',
cancelText: 'Cancel'
});
if (!ok) return;
setLicenseBusy(true, 'Deactivating…');
accountLogoutBtn?.addEventListener('click', async () => {
if (!authApiKey) return;
try {
// (Optional) TODO: call your API /license/deactivate here, e.g.:
// const acct = await accountId();
// await fetch('https://YOUR-API/license/deactivate', {
// method: 'POST', headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ key: (licenseKeyEl?.value || '').trim(), account_id: acct.id })
// });
licenseValid = false;
refreshPageLimit();
await chrome.storage.local.set({ licenseValid: false, licenseKey: '' });
if (licenseKeyEl) licenseKeyEl.value = '';
applyLicenseUI();
// PATCH: Immediately re-render tone section to disable controls after deactivation
renderToneSettingsSection();
licenseKeyCurrent = '';
setLicenseEditMode(false);
showToast('License deactivated');
const ok = await showConfirmModal({
title: 'Disconnect account?',
message: 'You can still use public keywords after logout.',
okText: 'Disconnect',
cancelText: 'Cancel',
});
if (!ok) return;
await fetch(`${API.base}/user/logout`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${authApiKey}` },
}).catch(() => null);
} finally {
authApiKey = '';
authTier = 'guest';
await saveSettings();
updateAccountUI();
setStatusTag();
showToast('Account disconnected');
fetchPage(true).catch(console.error);
setLicenseBusy(false);
} catch (e) {
setLicenseBusy(false);
showToast('Could not deactivate');
}
});
async function renderDiag(obj) {
const lines = [];
const token = await getExtensionTokenCached();
lines.push(`GCM Token: ${token ? '✅ Received' : '❌ Missing'}`);
if (token) lines.push(`Token ID: ${token.slice(0, 10)}`);
try {
const lock = await chrome.storage.local.get(['dewemojiExtTokenLockTs']);
const ts = lock?.dewemojiExtTokenLockTs;
if (ts) {
const ageSec = Math.max(0, Math.round((Date.now() - ts) / 1000));
lines.push(`GCM Lock: ${ageSec}s ago`);
}
} catch {}
try {
const err = await chrome.storage.local.get(['dewemojiExtTokenLastError','dewemojiExtTokenLastErrorTs']);
if (err?.dewemojiExtTokenLastError) {
const ts = err?.dewemojiExtTokenLastErrorTs;
const ageSec = ts ? Math.max(0, Math.round((Date.now() - ts) / 1000)) : null;
lines.push(`GCM Error: ${err.dewemojiExtTokenLastError}${ageSec != null ? ` (${ageSec}s ago)` : ''}`);
}
} catch {}
lines.push(`Content script loaded: ${obj ? 'yes' : 'no'}`);
if (!obj) return lines.join('\n');
@@ -1649,17 +1586,11 @@ licenseDeactivateBtn?.addEventListener('click', async () => {
return lines.join('\n');
}
async function runDiagnostics({ forceToken = false } = {}) {
async function runDiagnostics() {
diagOut.textContent = '';
diagSpin.style.display = 'inline';
try {
if (forceToken) {
const timeoutMs = 5000;
const tokenPromise = getExtensionToken(true);
const timeoutPromise = new Promise((_, rej) => setTimeout(() => rej(new Error('Token refresh timed out')), timeoutMs));
await Promise.race([tokenPromise, timeoutPromise]);
}
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab?.id) { diagOut.textContent = 'No active tab.'; return; }
@@ -1675,8 +1606,7 @@ licenseDeactivateBtn?.addEventListener('click', async () => {
}
}
diagRunBtn?.addEventListener('click', () => runDiagnostics({ forceToken: false }));
diagForceBtn?.addEventListener('click', () => runDiagnostics({ forceToken: true }));
diagRunBtn?.addEventListener('click', () => runDiagnostics());
async function performEmojiAction(glyph) {
// Free users always copy
@@ -1704,14 +1634,18 @@ async function performEmojiAction(glyph) {
async function setStatusTag(){
const dewemojiStatusTag = document.getElementById('dewemoji-status');
if(licenseValid){
dewemojiStatusTag.innerText = 'Pro';
if(authTier === 'personal'){
dewemojiStatusTag.innerText = 'Personal';
dewemojiStatusTag.classList.add('pro');
dewemojiStatusTag.classList.remove('free');
}else{
}else if(authTier === 'free'){
dewemojiStatusTag.innerText = 'Free';
dewemojiStatusTag.classList.add('free');
dewemojiStatusTag.classList.remove('pro');
}else{
dewemojiStatusTag.innerText = 'Guest';
dewemojiStatusTag.classList.add('free');
dewemojiStatusTag.classList.remove('pro');
}
}
setStatusTag().catch(() => {});
@@ -1901,8 +1835,8 @@ function renderCard(e) {
}
card.addEventListener('click', async () => {
// For nonPro, or nontoneable, or families → perform action immediately
if (!licenseValid || !isToneableByPolicy(e) || isFamilyEntry(e)) {
// For non-toneable or families → perform action immediately
if (!isToneableByPolicy(e) || isFamilyEntry(e)) {
performEmojiAction(baseGlyph).catch(console.error);
return;
}
@@ -1926,41 +1860,7 @@ if (window.twemoji) {
}
(async function verifyLicenseOnBoot(){
try {
const { licenseKey, lastLicenseCheck, licenseValid: storedValid } = await chrome.storage.local.get(['licenseKey','lastLicenseCheck','licenseValid']);
if (!licenseKey) return;
const day = 24*60*60*1000;
// If licenseValid is true and last check is within a day, skip checking and set UI immediately
if (storedValid && (Date.now() - (lastLicenseCheck||0) < day)) {
licenseValid = true;
refreshPageLimit();
applyLicenseUI();
setStatusTag();
licenseStatusEl && (licenseStatusEl.textContent = 'Pro active');
return;
}
// Otherwise, show "Checking license…" while verifying
licenseStatusEl && (licenseStatusEl.textContent = 'Checking license…');
const acct = await accountId();
const res = await fetch('https://api.dewemoji.com/v1/license/verify', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: licenseKey, account_id: acct.id, version: chrome.runtime.getManifest().version })
});
const data = await res.json().catch(() => ({}));
const ok = !!data.ok;
await chrome.storage.local.set({ licenseValid: ok, lastLicenseCheck: Date.now() });
licenseValid = ok;
refreshPageLimit();
applyLicenseUI();
setStatusTag();
if (licenseStatusEl) {
if (licenseValid) {
licenseStatusEl.textContent = 'Pro active';
} else {
licenseStatusEl.innerHTML = freeStatusHTML();
}
}
} catch {}
// Legacy license flow removed. Account auth is loaded via loadSettings().
})();
// Re-render tone section when opening settings (in case lock/pref changed during session)