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.
|
- `X-Dewemoji-Tier` can be read when present.
|
||||||
- UI remains stable when header is absent.
|
- 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.
|
1. Insert/copy actions still work.
|
||||||
2. Settings panel opens and saves.
|
2. Settings panel opens and saves.
|
||||||
3. No new console syntax/runtime errors.
|
3. No new console syntax/runtime errors.
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"<all_urls>",
|
"<all_urls>",
|
||||||
|
"https://dewemoji.backoffice.biz.id/*",
|
||||||
"https://api.dewemoji.com/*"
|
"https://api.dewemoji.com/*"
|
||||||
],
|
],
|
||||||
"web_accessible_resources": [
|
"web_accessible_resources": [
|
||||||
|
|||||||
40
panel.html
40
panel.html
@@ -19,7 +19,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="hdr">
|
<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">
|
<div class="bar">
|
||||||
<input id="q" class="inp" type="text" placeholder="Search (e.g. love)" aria-label="Search emojis" />
|
<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>
|
<button id="clear" class="btn icon" title="Clear" aria-label="Clear">✕</button>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
<!-- Settings sheet -->
|
<!-- Settings sheet -->
|
||||||
<div id="sheet-backdrop" class="backdrop" hidden></div>
|
<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">
|
<div class="sheet-head">
|
||||||
<h3 id="settings-title">Settings</h3>
|
<h3 id="settings-title">Settings</h3>
|
||||||
<button id="sheet-close" class="btn icon" title="Close" aria-label="Close">✕</button>
|
<button id="sheet-close" class="btn icon" title="Close" aria-label="Close">✕</button>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<div class="sheet-body">
|
<div class="sheet-body">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab active" data-tab="general">General</button>
|
<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>
|
||||||
|
|
||||||
<div id="tab-general" class="tabpane active">
|
<div id="tab-general" class="tabpane active">
|
||||||
@@ -72,12 +72,12 @@
|
|||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="actionMode" value="insert">
|
<input type="radio" name="actionMode" value="insert">
|
||||||
<span>Insert to input</span>
|
<span>Insert to input</span>
|
||||||
<span class="tag pro">Pro</span>
|
<span class="tag free">Available</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="radio">
|
<label class="radio">
|
||||||
<input type="radio" name="actionMode" value="auto">
|
<input type="radio" name="actionMode" value="auto">
|
||||||
<span>Automatic</span>
|
<span>Automatic</span>
|
||||||
<span class="tag pro">Pro</span>
|
<span class="tag free">Available</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint muted">Automatic tries to insert at the last caret; if not possible, it copies instead.</p>
|
<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>
|
||||||
|
|
||||||
<div id="tab-pro" class="tabpane">
|
<div id="tab-pro" class="tabpane">
|
||||||
<!-- License (first) -->
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="lbl">License</label>
|
<label class="lbl">Account</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<input id="license-key" class="inp" placeholder="Enter license key" type="password" autocomplete="off">
|
<input id="account-email" class="inp" placeholder="Email" type="email" autocomplete="username">
|
||||||
<button id="license-activate" class="btn">Activate</button>
|
|
||||||
<button id="license-deactivate" class="btn ghost">Deactivate</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="margin-top:6px;">
|
<div class="row" style="margin-top:6px;">
|
||||||
<button id="license-edit" class="link" style="width: fit-content">Change key</button>
|
<input id="account-password" class="inp" placeholder="Password" type="password" autocomplete="current-password">
|
||||||
<button id="license-cancel-edit" class="link" style="display:none;width: fit-content">Cancel</button>
|
</div>
|
||||||
<span id="license-status" class="muted">
|
<div class="row">
|
||||||
Free mode — Pro features locked.
|
<button id="account-login" class="btn">Connect</button>
|
||||||
<br><a href="https://dewemoji.com/pricing" target="_blank" class="cta-link" style="margin-left:6px; text-decoration: underline; font-weight: 500;">
|
<button id="account-logout" class="btn ghost">Logout</button>
|
||||||
🔓 Upgrade to Pro
|
</div>
|
||||||
</a>
|
<div class="row" style="margin-top:6px;">
|
||||||
</span>
|
<span id="account-status" class="muted">Not connected. Public keywords only.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,14 +106,13 @@
|
|||||||
|
|
||||||
<!-- Diagnostics -->
|
<!-- Diagnostics -->
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="lbl">Diagnostics</label>
|
<label class="lbl">Extension Health</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="diag-run" class="btn">Run diagnostics</button>
|
<button id="diag-run" class="btn">Run health check</button>
|
||||||
<button id="diag-force" class="btn ghost">Force token refresh</button>
|
|
||||||
<span id="diag-spin" class="muted" style="display:none;">Running…</span>
|
<span id="diag-spin" class="muted" style="display:none;">Running…</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="diag-out" class="diagbox muted"></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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
380
panel.js
380
panel.js
@@ -1,10 +1,12 @@
|
|||||||
// === License & mode (stub) ===
|
// Feature access is now free in extension UI. Billing applies to private keywords on backend.
|
||||||
// Flip this to true for local testing of Pro features if you want:
|
let licenseValid = true;
|
||||||
let licenseValid = false; // <- default: Free
|
|
||||||
|
|
||||||
// Persistent settings
|
// Persistent settings
|
||||||
let actionMode = 'copy'; // 'copy' | 'insert' | 'auto'
|
let actionMode = 'copy'; // 'copy' | 'insert' | 'auto'
|
||||||
let licenseKeyCurrent = '';
|
let licenseKeyCurrent = '';
|
||||||
|
let authApiKey = '';
|
||||||
|
let authTier = 'guest'; // guest | free | personal
|
||||||
|
let authEmail = '';
|
||||||
|
|
||||||
// Tone settings (persisted in sync storage)
|
// Tone settings (persisted in sync storage)
|
||||||
let toneLock = false; // if true, always use preferred tone
|
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 licenseDeactivateBtn = document.getElementById('license-deactivate');
|
||||||
const licenseEditBtn = document.getElementById('license-edit');
|
const licenseEditBtn = document.getElementById('license-edit');
|
||||||
const licenseCancelEditBtn = document.getElementById('license-cancel-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 ---
|
// --- Branded confirm modal helper ---
|
||||||
function showConfirmModal(opts = {}) {
|
function showConfirmModal(opts = {}) {
|
||||||
@@ -107,18 +114,17 @@ function setLicenseBusy(on, label){
|
|||||||
}
|
}
|
||||||
|
|
||||||
const diagRunBtn = document.getElementById('diag-run');
|
const diagRunBtn = document.getElementById('diag-run');
|
||||||
const diagForceBtn = document.getElementById('diag-force');
|
|
||||||
const diagSpin = document.getElementById('diag-spin');
|
const diagSpin = document.getElementById('diag-spin');
|
||||||
const diagOut = document.getElementById('diag-out');
|
const diagOut = document.getElementById('diag-out');
|
||||||
|
|
||||||
const API = {
|
const API = {
|
||||||
base: "https://api.dewemoji.com/v1",
|
base: "https://dewemoji.backoffice.biz.id/v1",
|
||||||
list: "/emojis"
|
list: "/emojis"
|
||||||
};
|
};
|
||||||
API.cats = "/categories";
|
API.cats = "/categories";
|
||||||
let PAGE_LIMIT = 20; // Free default
|
let PAGE_LIMIT = 50;
|
||||||
function refreshPageLimit(){
|
function refreshPageLimit(){
|
||||||
PAGE_LIMIT = licenseValid ? 50 : 20; // Pro gets bigger pages
|
PAGE_LIMIT = 50;
|
||||||
}
|
}
|
||||||
refreshPageLimit();
|
refreshPageLimit();
|
||||||
const FRONTEND_ID = 'ext-v1';
|
const FRONTEND_ID = 'ext-v1';
|
||||||
@@ -580,6 +586,7 @@ function debounced(fn, delay=250){ clearTimeout(timer); timer = setTimeout(fn, d
|
|||||||
// === P0: Usage & Cache ===
|
// === P0: Usage & Cache ===
|
||||||
const CLIENT_FREE_DAILY_LIMIT = 30; // can be tuned; Pro => unlimited
|
const CLIENT_FREE_DAILY_LIMIT = 30; // can be tuned; Pro => unlimited
|
||||||
const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
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 QUERY_CACHE = new Map(); // in-memory cache: key => {ts, data}
|
||||||
const PREFETCHING = new Set(); // tracks in-flight prefetch keys (sig|page)
|
const PREFETCHING = new Set(); // tracks in-flight prefetch keys (sig|page)
|
||||||
|
|
||||||
@@ -614,33 +621,23 @@ function signatureFor(qVal, catVal, subVal){
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getPersistedCache(){
|
async function getPersistedCache(){
|
||||||
|
if (!ENABLE_RESULT_CACHE) return {};
|
||||||
const got = await chrome.storage.local.get(['searchCache']);
|
const got = await chrome.storage.local.get(['searchCache']);
|
||||||
return got.searchCache || {}; // { key: { ts, data } }
|
return got.searchCache || {}; // { key: { ts, data } }
|
||||||
}
|
}
|
||||||
async function savePersistedCache(cacheObj){
|
async function savePersistedCache(cacheObj){
|
||||||
|
if (!ENABLE_RESULT_CACHE) return;
|
||||||
await chrome.storage.local.set({ searchCache: cacheObj });
|
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(){
|
async function buildHeaders(){
|
||||||
const base = { 'X-Dewemoji-Frontend': FRONTEND_ID };
|
const base = { 'X-Dewemoji-Frontend': FRONTEND_ID };
|
||||||
try {
|
try {
|
||||||
if (chrome?.runtime?.id) base['X-Extension-Id'] = chrome.runtime.id;
|
if (chrome?.runtime?.id) base['X-Extension-Id'] = chrome.runtime.id;
|
||||||
} catch {}
|
} catch {}
|
||||||
if (licenseValid && licenseKeyCurrent) {
|
if (authApiKey) {
|
||||||
try {
|
base['Authorization'] = `Bearer ${authApiKey}`;
|
||||||
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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
@@ -700,6 +697,7 @@ async function fetchJsonWithFallback(params, headers, { quiet = false } = {}) {
|
|||||||
|
|
||||||
|
|
||||||
async function prefetchNextIfNeeded(currentSig){
|
async function prefetchNextIfNeeded(currentSig){
|
||||||
|
if (!ENABLE_RESULT_CACHE) return;
|
||||||
try {
|
try {
|
||||||
// only prefetch when there are more results and we know the next page index
|
// only prefetch when there are more results and we know the next page index
|
||||||
const nextPage = page + 1;
|
const nextPage = page + 1;
|
||||||
@@ -747,27 +745,15 @@ async function fetchPage(reset=false) {
|
|||||||
if (reset) { page = 1; items = []; list.innerHTML = ""; }
|
if (reset) { page = 1; items = []; list.innerHTML = ""; }
|
||||||
if (reset) { PREFETCHING.clear(); }
|
if (reset) { PREFETCHING.clear(); }
|
||||||
if (reset) { autoLoadBusy = false; }
|
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
|
// Build signature and check limits only for page 1
|
||||||
const sig = signatureFor(q.value, cat.value, sub.value);
|
const sig = signatureFor(q.value, cat.value, sub.value);
|
||||||
|
|
||||||
// Gate on soft cap for Free (Pro unlimited)
|
// Client-side cap disabled; backend is source of truth for rate/sanity limits.
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showSpinner();
|
showSpinner();
|
||||||
@@ -776,12 +762,11 @@ async function fetchPage(reset=false) {
|
|||||||
const key = `${sig}|${page}`;
|
const key = `${sig}|${page}`;
|
||||||
|
|
||||||
// 1) Try memory cache
|
// 1) Try memory cache
|
||||||
const mem = QUERY_CACHE.get(key);
|
const mem = ENABLE_RESULT_CACHE ? QUERY_CACHE.get(key) : null;
|
||||||
if (mem && isFresh(mem.ts)) {
|
if (ENABLE_RESULT_CACHE && mem && isFresh(mem.ts)) {
|
||||||
const data = mem.data;
|
const data = mem.data;
|
||||||
total = data.total || 0;
|
total = data.total || 0;
|
||||||
for (const e of (data.items || [])) { items.push(e); renderCard(e); }
|
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);
|
const currentSig = signatureFor(q.value, cat.value, sub.value);
|
||||||
prefetchNextIfNeeded(currentSig).catch(()=>{});
|
prefetchNextIfNeeded(currentSig).catch(()=>{});
|
||||||
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
|
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
|
||||||
@@ -790,19 +775,21 @@ async function fetchPage(reset=false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2) Try persisted cache
|
// 2) Try persisted cache
|
||||||
|
let ent = null;
|
||||||
|
if (ENABLE_RESULT_CACHE) {
|
||||||
const persisted = await getPersistedCache();
|
const persisted = await getPersistedCache();
|
||||||
const ent = persisted[key];
|
ent = persisted[key] || null;
|
||||||
if (ent && isFresh(ent.ts)) {
|
if (ent && isFresh(ent.ts)) {
|
||||||
total = ent.data.total || 0;
|
total = ent.data.total || 0;
|
||||||
for (const e of (ent.data.items || [])) { items.push(e); renderCard(e); }
|
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
|
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);
|
const currentSig = signatureFor(q.value, cat.value, sub.value);
|
||||||
prefetchNextIfNeeded(currentSig).catch(()=>{});
|
prefetchNextIfNeeded(currentSig).catch(()=>{});
|
||||||
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
|
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
|
||||||
updateFooter();
|
updateFooter();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3) Network fetch
|
// 3) Network fetch
|
||||||
const params = new URLSearchParams({ page: String(page), limit: String(PAGE_LIMIT) });
|
const params = new URLSearchParams({ page: String(page), limit: String(PAGE_LIMIT) });
|
||||||
@@ -836,34 +823,27 @@ async function fetchPage(reset=false) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count usage only on successful network responses for Free, page 1, and when not cached
|
// Client-side usage counter disabled.
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to caches
|
// Save to caches
|
||||||
|
if (ENABLE_RESULT_CACHE) {
|
||||||
const record = { ts: Date.now(), data };
|
const record = { ts: Date.now(), data };
|
||||||
QUERY_CACHE.set(key, record);
|
QUERY_CACHE.set(key, record);
|
||||||
const persisted2 = await getPersistedCache();
|
const persisted2 = await getPersistedCache();
|
||||||
persisted2[key] = record;
|
persisted2[key] = record;
|
||||||
// prune old entries occasionally (simple heuristic)
|
|
||||||
const now = Date.now();
|
|
||||||
for (const k of Object.keys(persisted2)) {
|
for (const k of Object.keys(persisted2)) {
|
||||||
if (!persisted2[k] || !isFresh(persisted2[k].ts)) delete persisted2[k];
|
if (!persisted2[k] || !isFresh(persisted2[k].ts)) delete persisted2[k];
|
||||||
}
|
}
|
||||||
await savePersistedCache(persisted2);
|
await savePersistedCache(persisted2);
|
||||||
|
}
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
for (const e of (data.items || [])) { items.push(e); renderCard(e); }
|
for (const e of (data.items || [])) { items.push(e); renderCard(e); }
|
||||||
// Kick off background prefetch for the next page (cached only)
|
// Kick off background prefetch for the next page (cached only)
|
||||||
|
if (ENABLE_RESULT_CACHE) {
|
||||||
const currentSig = signatureFor(q.value, cat.value, sub.value);
|
const currentSig = signatureFor(q.value, cat.value, sub.value);
|
||||||
prefetchNextIfNeeded(currentSig).catch(()=>{});
|
prefetchNextIfNeeded(currentSig).catch(()=>{});
|
||||||
|
}
|
||||||
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
|
try { ensureSentinel(); setupAutoLoadObserver(); } catch(_) {}
|
||||||
updateFooter();
|
updateFooter();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1253,34 +1233,32 @@ loadCategories().catch(console.error);
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
const data = await chrome.storage.local.get(['licenseValid', 'licenseKey', 'actionMode']);
|
const data = await chrome.storage.local.get(['actionMode', 'authApiKey', 'authTier', 'authEmail']);
|
||||||
licenseValid = !!data.licenseValid;
|
licenseValid = true;
|
||||||
refreshPageLimit();
|
refreshPageLimit();
|
||||||
actionMode = data.actionMode || 'copy';
|
actionMode = data.actionMode || 'copy';
|
||||||
if (data.licenseKey) licenseKeyEl && (licenseKeyEl.value = data.licenseKey);
|
authApiKey = data.authApiKey || '';
|
||||||
licenseKeyCurrent = data.licenseKey || '';
|
authTier = data.authTier || 'guest';
|
||||||
|
authEmail = data.authEmail || '';
|
||||||
|
if (accountEmailEl && authEmail) accountEmailEl.value = authEmail;
|
||||||
applyLicenseUI();
|
applyLicenseUI();
|
||||||
applyModeUI();
|
applyModeUI();
|
||||||
|
updateAccountUI();
|
||||||
setStatusTag();
|
setStatusTag();
|
||||||
// tone settings from sync storage
|
// tone settings from sync storage
|
||||||
await getPreferredToneIndex();
|
await getPreferredToneIndex();
|
||||||
await getToneLock();
|
await getToneLock();
|
||||||
renderToneSettingsSection();
|
renderToneSettingsSection();
|
||||||
// ensure license field starts in non-edit mode
|
|
||||||
setLicenseEditMode(false);
|
setLicenseEditMode(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSettings() {
|
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 ---
|
// --- Helper to render Free status with CTA ---
|
||||||
function freeStatusHTML(){
|
function freeStatusHTML(){
|
||||||
return 'Free mode — Pro features locked.' +
|
return 'All extension features are available. Connect account to use private keywords.';
|
||||||
'<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>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setLicenseEditMode(on) {
|
function setLicenseEditMode(on) {
|
||||||
@@ -1309,47 +1287,51 @@ function setLicenseEditMode(on) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyLicenseUI() {
|
function applyLicenseUI() {
|
||||||
// enable/disable Pro radios
|
// Extension features are available for everyone in current model.
|
||||||
const radios = modeGroup.querySelectorAll('input[type="radio"][name="actionMode"]');
|
const radios = modeGroup.querySelectorAll('input[type="radio"][name="actionMode"]');
|
||||||
radios.forEach(r => {
|
radios.forEach(r => {
|
||||||
const isPro = (r.value === 'insert' || r.value === 'auto');
|
r.disabled = false;
|
||||||
if (isPro) {
|
r.closest('.radio')?.setAttribute('aria-disabled', 'false');
|
||||||
r.disabled = !licenseValid;
|
|
||||||
r.closest('.radio').setAttribute('aria-disabled', !licenseValid ? 'true' : '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}"]`) ||
|
const target = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`) ||
|
||||||
modeGroup.querySelector('input[type="radio"][value="auto"]');
|
modeGroup.querySelector('input[type="radio"][value="copy"]');
|
||||||
if (target) target.checked = true;
|
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';
|
|
||||||
}
|
|
||||||
setStatusTag();
|
setStatusTag();
|
||||||
// --- PATCH: Refresh tone section after license state changes ---
|
|
||||||
try { renderToneSettingsSection(); } catch(_) {}
|
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() {
|
function applyModeUI() {
|
||||||
// Ensure the selected radio matches actionMode (if not locked)
|
// Ensure the selected radio matches actionMode (if not locked)
|
||||||
const input = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`);
|
const input = modeGroup.querySelector(`input[type="radio"][value="${actionMode}"]`);
|
||||||
@@ -1520,125 +1502,80 @@ if (tabsWrap) {
|
|||||||
modeGroup?.addEventListener('change', async (e) => {
|
modeGroup?.addEventListener('change', async (e) => {
|
||||||
if (e.target && e.target.name === 'actionMode') {
|
if (e.target && e.target.name === 'actionMode') {
|
||||||
const val = e.target.value;
|
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();
|
await saveSettings();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
licenseActivateBtn?.addEventListener('click', async () => {
|
accountLoginBtn?.addEventListener('click', async () => {
|
||||||
const key = (licenseKeyEl?.value || '').trim();
|
if (authApiKey) {
|
||||||
if (!key) { showToast('Enter a license key'); return; }
|
updateAccountUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const email = (accountEmailEl?.value || '').trim();
|
||||||
|
const password = (accountPasswordEl?.value || '').trim();
|
||||||
|
if (!email || !password) {
|
||||||
|
showToast('Enter email and password');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLicenseBusy(true, 'Verifying…');
|
accountLoginBtn.disabled = true;
|
||||||
// Call your API /license/verify (works for Gumroad and Mayar)
|
accountLoginBtn.textContent = 'Connecting...';
|
||||||
const acct = await accountId();
|
const res = await fetch(`${API.base}/user/login`, {
|
||||||
const res = await fetch('https://api.dewemoji.com/v1/license/verify', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
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(() => ({}));
|
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;
|
authApiKey = data?.api_key || '';
|
||||||
refreshPageLimit();
|
authTier = String(data?.user?.tier || 'free');
|
||||||
licenseKeyCurrent = key;
|
authEmail = String(data?.user?.email || email);
|
||||||
await chrome.storage.local.set({ licenseValid: true, licenseKey: key, lastLicenseCheck: Date.now() });
|
await saveSettings();
|
||||||
|
updateAccountUI();
|
||||||
applyLicenseUI();
|
|
||||||
setLicenseEditMode(false);
|
|
||||||
setStatusTag();
|
setStatusTag();
|
||||||
// PATCH: Immediately re-render tone section to enable controls after activation
|
if (accountPasswordEl) accountPasswordEl.value = '';
|
||||||
renderToneSettingsSection();
|
showToast('Account connected');
|
||||||
showToast('License activated ✓');
|
|
||||||
|
|
||||||
// Refresh results so Pro headers/tier take effect immediately
|
|
||||||
fetchPage(true).catch(console.error);
|
fetchPage(true).catch(console.error);
|
||||||
setLicenseBusy(false);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLicenseBusy(false);
|
showToast(`Connect failed${e?.message ? ': ' + e.message : ''}`);
|
||||||
licenseValid = false;
|
} finally {
|
||||||
await chrome.storage.local.set({ licenseValid: false });
|
accountLoginBtn.disabled = false;
|
||||||
applyLicenseUI();
|
accountLoginBtn.textContent = 'Connect';
|
||||||
setStatusTag();
|
|
||||||
showToast(`Activation failed${e?.message ? ': ' + e.message : ''}`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
licenseEditBtn?.addEventListener('click', () => {
|
accountLogoutBtn?.addEventListener('click', async () => {
|
||||||
setLicenseEditMode(true);
|
if (!authApiKey) return;
|
||||||
});
|
try {
|
||||||
|
|
||||||
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({
|
const ok = await showConfirmModal({
|
||||||
title: 'Deactivate Dewemoji Pro?',
|
title: 'Disconnect account?',
|
||||||
message: 'Deactivate Pro on this device?',
|
message: 'You can still use public keywords after logout.',
|
||||||
okText: 'Deactivate',
|
okText: 'Disconnect',
|
||||||
cancelText: 'Cancel'
|
cancelText: 'Cancel',
|
||||||
});
|
});
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
setLicenseBusy(true, 'Deactivating…');
|
await fetch(`${API.base}/user/logout`, {
|
||||||
try {
|
method: 'POST',
|
||||||
// (Optional) TODO: call your API /license/deactivate here, e.g.:
|
headers: { 'Authorization': `Bearer ${authApiKey}` },
|
||||||
// const acct = await accountId();
|
}).catch(() => null);
|
||||||
// await fetch('https://YOUR-API/license/deactivate', {
|
} finally {
|
||||||
// method: 'POST', headers: { 'Content-Type': 'application/json' },
|
authApiKey = '';
|
||||||
// body: JSON.stringify({ key: (licenseKeyEl?.value || '').trim(), account_id: acct.id })
|
authTier = 'guest';
|
||||||
// });
|
await saveSettings();
|
||||||
licenseValid = false;
|
updateAccountUI();
|
||||||
refreshPageLimit();
|
setStatusTag();
|
||||||
await chrome.storage.local.set({ licenseValid: false, licenseKey: '' });
|
showToast('Account disconnected');
|
||||||
if (licenseKeyEl) licenseKeyEl.value = '';
|
|
||||||
applyLicenseUI();
|
|
||||||
// PATCH: Immediately re-render tone section to disable controls after deactivation
|
|
||||||
renderToneSettingsSection();
|
|
||||||
licenseKeyCurrent = '';
|
|
||||||
setLicenseEditMode(false);
|
|
||||||
showToast('License deactivated');
|
|
||||||
fetchPage(true).catch(console.error);
|
fetchPage(true).catch(console.error);
|
||||||
setLicenseBusy(false);
|
|
||||||
} catch (e) {
|
|
||||||
setLicenseBusy(false);
|
|
||||||
showToast('Could not deactivate');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderDiag(obj) {
|
async function renderDiag(obj) {
|
||||||
const lines = [];
|
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'}`);
|
lines.push(`Content script loaded: ${obj ? 'yes' : 'no'}`);
|
||||||
if (!obj) return lines.join('\n');
|
if (!obj) return lines.join('\n');
|
||||||
|
|
||||||
@@ -1649,17 +1586,11 @@ licenseDeactivateBtn?.addEventListener('click', async () => {
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runDiagnostics({ forceToken = false } = {}) {
|
async function runDiagnostics() {
|
||||||
diagOut.textContent = '';
|
diagOut.textContent = '';
|
||||||
diagSpin.style.display = 'inline';
|
diagSpin.style.display = 'inline';
|
||||||
|
|
||||||
try {
|
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 });
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
if (!tab?.id) { diagOut.textContent = 'No active tab.'; return; }
|
if (!tab?.id) { diagOut.textContent = 'No active tab.'; return; }
|
||||||
|
|
||||||
@@ -1675,8 +1606,7 @@ licenseDeactivateBtn?.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
diagRunBtn?.addEventListener('click', () => runDiagnostics({ forceToken: false }));
|
diagRunBtn?.addEventListener('click', () => runDiagnostics());
|
||||||
diagForceBtn?.addEventListener('click', () => runDiagnostics({ forceToken: true }));
|
|
||||||
|
|
||||||
async function performEmojiAction(glyph) {
|
async function performEmojiAction(glyph) {
|
||||||
// Free users always copy
|
// Free users always copy
|
||||||
@@ -1704,14 +1634,18 @@ async function performEmojiAction(glyph) {
|
|||||||
|
|
||||||
async function setStatusTag(){
|
async function setStatusTag(){
|
||||||
const dewemojiStatusTag = document.getElementById('dewemoji-status');
|
const dewemojiStatusTag = document.getElementById('dewemoji-status');
|
||||||
if(licenseValid){
|
if(authTier === 'personal'){
|
||||||
dewemojiStatusTag.innerText = 'Pro';
|
dewemojiStatusTag.innerText = 'Personal';
|
||||||
dewemojiStatusTag.classList.add('pro');
|
dewemojiStatusTag.classList.add('pro');
|
||||||
dewemojiStatusTag.classList.remove('free');
|
dewemojiStatusTag.classList.remove('free');
|
||||||
}else{
|
}else if(authTier === 'free'){
|
||||||
dewemojiStatusTag.innerText = 'Free';
|
dewemojiStatusTag.innerText = 'Free';
|
||||||
dewemojiStatusTag.classList.add('free');
|
dewemojiStatusTag.classList.add('free');
|
||||||
dewemojiStatusTag.classList.remove('pro');
|
dewemojiStatusTag.classList.remove('pro');
|
||||||
|
}else{
|
||||||
|
dewemojiStatusTag.innerText = 'Guest';
|
||||||
|
dewemojiStatusTag.classList.add('free');
|
||||||
|
dewemojiStatusTag.classList.remove('pro');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setStatusTag().catch(() => {});
|
setStatusTag().catch(() => {});
|
||||||
@@ -1901,8 +1835,8 @@ function renderCard(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
card.addEventListener('click', async () => {
|
card.addEventListener('click', async () => {
|
||||||
// For non‑Pro, or non‑toneable, or families → perform action immediately
|
// For non-toneable or families → perform action immediately
|
||||||
if (!licenseValid || !isToneableByPolicy(e) || isFamilyEntry(e)) {
|
if (!isToneableByPolicy(e) || isFamilyEntry(e)) {
|
||||||
performEmojiAction(baseGlyph).catch(console.error);
|
performEmojiAction(baseGlyph).catch(console.error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1926,41 +1860,7 @@ if (window.twemoji) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(async function verifyLicenseOnBoot(){
|
(async function verifyLicenseOnBoot(){
|
||||||
try {
|
// Legacy license flow removed. Account auth is loaded via loadSettings().
|
||||||
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 {}
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Re-render tone section when opening settings (in case lock/pref changed during session)
|
// Re-render tone section when opening settings (in case lock/pref changed during session)
|
||||||
|
|||||||
Reference in New Issue
Block a user