Refactor extension to account-auth flow and health-only diagnostics
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>",
|
||||
"https://dewemoji.backoffice.biz.id/*",
|
||||
"https://api.dewemoji.com/*"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
|
||||
40
panel.html
40
panel.html
@@ -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
428
panel.js
@@ -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 non‑Pro, or non‑toneable, 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)
|
||||
|
||||
Reference in New Issue
Block a user