Checkpoint before Personal-auth refactor
0
.gitignore
vendored
Normal file → Executable file
0
assets/icon-128.png
Normal file → Executable file
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
0
assets/icon-16.png
Normal file → Executable file
|
Before Width: | Height: | Size: 798 B After Width: | Height: | Size: 798 B |
0
assets/icon-32.png
Normal file → Executable file
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
0
assets/icon-48.png
Normal file → Executable file
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
86
background.js
Normal file → Executable file
@@ -1,11 +1,95 @@
|
|||||||
// Let Chrome open the panel when the toolbar icon is clicked
|
// Let Chrome open the panel when the toolbar icon is clicked
|
||||||
// Guard: some Chromium variants may not expose sidePanel
|
// Guard: some Chromium variants may not expose sidePanel
|
||||||
const hasSidePanel = !!chrome.sidePanel?.setOptions;
|
const hasSidePanel = !!chrome.sidePanel?.setOptions;
|
||||||
|
const GCM_PROJECT_NUMBER = '1088502361802';
|
||||||
|
const EXT_TOKEN_KEY = 'dewemojiExtToken';
|
||||||
|
const GCM_LOCK_KEY = 'dewemojiExtTokenLockTs';
|
||||||
|
const GCM_LAST_ERR_KEY = 'dewemojiExtTokenLastError';
|
||||||
|
const GCM_LAST_ERR_TS_KEY = 'dewemojiExtTokenLastErrorTs';
|
||||||
|
const GCM_LOCK_WINDOW_MS = 60000;
|
||||||
|
let gcmRegisterPromise = null;
|
||||||
|
let gcmInMemoryLockTs = 0;
|
||||||
|
|
||||||
|
async function storeExtensionToken(token) {
|
||||||
|
try { await chrome.storage.local.set({ [EXT_TOKEN_KEY]: token }); } catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStoredToken() {
|
||||||
|
try {
|
||||||
|
const got = await chrome.storage.local.get([EXT_TOKEN_KEY]);
|
||||||
|
return got[EXT_TOKEN_KEY] || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerGcmToken() {
|
||||||
|
if (gcmRegisterPromise) return gcmRegisterPromise;
|
||||||
|
if (!chrome?.gcm?.register) return Promise.resolve(null);
|
||||||
|
gcmRegisterPromise = new Promise((resolve) => {
|
||||||
|
(async () => {
|
||||||
|
if (gcmInMemoryLockTs && Date.now() - gcmInMemoryLockTs < GCM_LOCK_WINDOW_MS) {
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
// Avoid repeated register attempts within a short window
|
||||||
|
const lock = await chrome.storage.local.get([GCM_LOCK_KEY]);
|
||||||
|
const lastTs = Number(lock[GCM_LOCK_KEY] || 0);
|
||||||
|
if (Date.now() - lastTs < GCM_LOCK_WINDOW_MS) {
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
const now = Date.now();
|
||||||
|
gcmInMemoryLockTs = now;
|
||||||
|
await chrome.storage.local.set({ [GCM_LOCK_KEY]: now });
|
||||||
|
})().then(() => {
|
||||||
|
chrome.gcm.register([GCM_PROJECT_NUMBER], async (registrationId) => {
|
||||||
|
if (chrome.runtime.lastError || !registrationId) {
|
||||||
|
const msg = chrome.runtime.lastError?.message || 'no registrationId';
|
||||||
|
try { await chrome.storage.local.set({ [GCM_LAST_ERR_KEY]: msg, [GCM_LAST_ERR_TS_KEY]: Date.now() }); } catch {}
|
||||||
|
if (!/asynchronous operation is pending/i.test(msg)) {
|
||||||
|
console.warn('GCM register failed', msg);
|
||||||
|
}
|
||||||
|
// back off on pending errors
|
||||||
|
if (/asynchronous operation is pending/i.test(msg)) {
|
||||||
|
const now = Date.now();
|
||||||
|
gcmInMemoryLockTs = now;
|
||||||
|
try { await chrome.storage.local.set({ [GCM_LOCK_KEY]: now }); } catch {}
|
||||||
|
}
|
||||||
|
return resolve(null);
|
||||||
|
}
|
||||||
|
try { await chrome.storage.local.remove([GCM_LAST_ERR_KEY, GCM_LAST_ERR_TS_KEY]); } catch {}
|
||||||
|
await storeExtensionToken(registrationId);
|
||||||
|
console.log('GCM token stored', registrationId.slice(0, 8) + '…');
|
||||||
|
resolve(registrationId);
|
||||||
|
});
|
||||||
|
}).catch(() => resolve(null));
|
||||||
|
}).finally(() => {
|
||||||
|
gcmRegisterPromise = null;
|
||||||
|
});
|
||||||
|
return gcmRegisterPromise;
|
||||||
|
}
|
||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
if (hasSidePanel) chrome.sidePanel.setPanelBehavior?.({ openPanelOnActionClick: true });
|
if (hasSidePanel) chrome.sidePanel.setPanelBehavior?.({ openPanelOnActionClick: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Intentionally do NOT auto-register on startup to avoid concurrent GCM calls.
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
||||||
|
if (msg?.type !== 'dewemoji_get_ext_token') return;
|
||||||
|
(async () => {
|
||||||
|
const force = !!msg.force;
|
||||||
|
let token = await getStoredToken();
|
||||||
|
if (!token || force) {
|
||||||
|
await registerGcmToken();
|
||||||
|
// best-effort wait for storage to be populated
|
||||||
|
await new Promise(r => setTimeout(r, 300));
|
||||||
|
token = await getStoredToken();
|
||||||
|
}
|
||||||
|
sendResponse({ ok: !!token, token });
|
||||||
|
})();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// If you still want to ensure the correct path on click, setOptions ONLY:
|
// If you still want to ensure the correct path on click, setOptions ONLY:
|
||||||
chrome.action.onClicked.addListener(async () => {
|
chrome.action.onClicked.addListener(async () => {
|
||||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||||
@@ -45,4 +129,4 @@ chrome.commands.onCommand.addListener(async (command) => {
|
|||||||
|
|
||||||
await chrome.sidePanel.setOptions({ tabId: tab.id, path: "panel.html" });
|
await chrome.sidePanel.setOptions({ tabId: tab.id, path: "panel.html" });
|
||||||
await chrome.sidePanel.open({ tabId: tab.id });
|
await chrome.sidePanel.open({ tabId: tab.id });
|
||||||
});
|
});
|
||||||
|
|||||||
0
content.js
Normal file → Executable file
82
extension-test-walkthrough.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Dewemoji Chrome Extension Test Walkthrough
|
||||||
|
|
||||||
|
This checklist verifies extension behavior against current backend flow.
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
- Extension loaded in Chrome (unpacked).
|
||||||
|
- Backend reachable at `https://api.dewemoji.com/v1`.
|
||||||
|
- Backend deployed with extension search route:
|
||||||
|
- `GET /v1/extension/search`
|
||||||
|
- Browser DevTools available for network/console checks.
|
||||||
|
|
||||||
|
## Test 1: Startup and Reload
|
||||||
|
1. Open `chrome://extensions`.
|
||||||
|
2. Enable `Developer mode`.
|
||||||
|
3. Click `Reload` on Dewemoji extension.
|
||||||
|
4. Open extension side panel.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- No fatal startup error in extension error page.
|
||||||
|
- Panel renders and can request data.
|
||||||
|
|
||||||
|
## Test 2: Search Request Path
|
||||||
|
1. Open panel DevTools.
|
||||||
|
2. Search any keyword, e.g. `happy`.
|
||||||
|
3. Check network request URL.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- First attempt uses `/v1/extension/search`.
|
||||||
|
- If endpoint is missing on server, extension falls back to `/v1/emojis`.
|
||||||
|
- User still gets results.
|
||||||
|
|
||||||
|
## Test 3: Categories Load
|
||||||
|
1. In panel, inspect category dropdown content.
|
||||||
|
2. Confirm categories loaded from API.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- Categories render from API payload.
|
||||||
|
- If categories API fails, extension still shows fallback categories.
|
||||||
|
|
||||||
|
## Test 4: Pagination and Load More
|
||||||
|
1. Run a broad query (e.g. `face`).
|
||||||
|
2. Scroll / load next page.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- Additional pages append without duplicates.
|
||||||
|
- No hard failure when prefetch endpoint returns 404.
|
||||||
|
|
||||||
|
## Test 5: Offline Guard
|
||||||
|
1. Turn off internet.
|
||||||
|
2. Search an uncached query.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- Graceful failure message shown.
|
||||||
|
- No panel crash.
|
||||||
|
|
||||||
|
## Test 6: Tier Header Observation
|
||||||
|
1. Search while signed out (free behavior).
|
||||||
|
2. Search while using valid auth if applicable.
|
||||||
|
3. Inspect response headers.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- `X-Dewemoji-Tier` can be read when present.
|
||||||
|
- UI remains stable when header is absent.
|
||||||
|
|
||||||
|
## Test 7: Regression Smoke
|
||||||
|
1. Insert/copy actions still work.
|
||||||
|
2. Settings panel opens and saves.
|
||||||
|
3. No new console syntax/runtime errors.
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
- Existing non-search features keep working.
|
||||||
|
|
||||||
|
## Debug Commands
|
||||||
|
- Verify route on backend:
|
||||||
|
- `php artisan route:list --path=v1/extension`
|
||||||
|
- Clear backend cache after deploy:
|
||||||
|
- `php artisan optimize:clear`
|
||||||
|
- `php artisan config:cache`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Preferred endpoint is `/v1/extension/search`.
|
||||||
|
- Compatibility fallback to `/v1/emojis` is intentional for mixed deployments.
|
||||||
5
manifest.json
Normal file → Executable file
@@ -7,7 +7,8 @@
|
|||||||
"storage",
|
"storage",
|
||||||
"scripting",
|
"scripting",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"sidePanel"
|
"sidePanel",
|
||||||
|
"gcm"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"<all_urls>",
|
"<all_urls>",
|
||||||
@@ -62,4 +63,4 @@
|
|||||||
"description": "Toggle Emoji Side Panel"
|
"description": "Toggle Emoji Side Panel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
panel.html
Normal file → Executable file
@@ -107,11 +107,12 @@
|
|||||||
|
|
||||||
<!-- Tone settings are injected here by panel.js -->
|
<!-- Tone settings are injected here by panel.js -->
|
||||||
|
|
||||||
<!-- Diagnostics (hidden for debugging) -->
|
<!-- Diagnostics -->
|
||||||
<div class="field" style="display:none;">
|
<div class="field">
|
||||||
<label class="lbl">Diagnostics</label>
|
<label class="lbl">Diagnostics</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="diag-run" class="btn">Run diagnostics</button>
|
<button id="diag-run" class="btn">Run diagnostics</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>
|
||||||
@@ -141,4 +142,4 @@
|
|||||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||||
<script src="panel.js?ver=1.0.1"></script>
|
<script src="panel.js?ver=1.0.1"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
171
panel.js
Normal file → Executable file
@@ -107,6 +107,7 @@ 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');
|
||||||
|
|
||||||
@@ -153,11 +154,53 @@ function categoryComparator(a, b) {
|
|||||||
return a.localeCompare(b, undefined, { sensitivity: "base" }); // both not preferred → A–Z
|
return a.localeCompare(b, undefined, { sensitivity: "base" }); // both not preferred → A–Z
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Extension verification token (GCM registration) ===
|
||||||
|
const EXT_TOKEN_KEY = 'dewemojiExtToken';
|
||||||
|
const EXT_TOKEN_COOLDOWN_MS = 60000;
|
||||||
|
let extTokenPromise = null;
|
||||||
|
let extTokenLastAttempt = 0;
|
||||||
|
|
||||||
|
async function getExtensionToken(force=false) {
|
||||||
|
if (extTokenPromise) return extTokenPromise;
|
||||||
|
extTokenPromise = (async () => {
|
||||||
|
try {
|
||||||
|
const got = await chrome.storage.local.get([EXT_TOKEN_KEY]);
|
||||||
|
if (got[EXT_TOKEN_KEY] && !force) return got[EXT_TOKEN_KEY];
|
||||||
|
if (!force && extTokenLastAttempt && Date.now() - extTokenLastAttempt < EXT_TOKEN_COOLDOWN_MS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
extTokenLastAttempt = Date.now();
|
||||||
|
const res = await chrome.runtime.sendMessage({ type: 'dewemoji_get_ext_token', force });
|
||||||
|
if (res?.token) {
|
||||||
|
await chrome.storage.local.set({ [EXT_TOKEN_KEY]: res.token });
|
||||||
|
return res.token;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Token retrieval failed:', err);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})().finally(() => {
|
||||||
|
extTokenPromise = null;
|
||||||
|
});
|
||||||
|
return extTokenPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getExtensionTokenCached() {
|
||||||
|
try {
|
||||||
|
const got = await chrome.storage.local.get([EXT_TOKEN_KEY]);
|
||||||
|
return got[EXT_TOKEN_KEY] || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadCategories() {
|
async function loadCategories() {
|
||||||
// Try live endpoint; fall back silently to local list (no subs)
|
// Try live endpoint; fall back silently to local list (no subs)
|
||||||
let ok = false;
|
let ok = false;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API.base}${API.cats}`);
|
const headers = await buildHeaders();
|
||||||
|
const res = await fetch(`${API.base}${API.cats}`, { headers });
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
// Normalize payloads:
|
// Normalize payloads:
|
||||||
@@ -579,9 +622,12 @@ async function savePersistedCache(cacheObj){
|
|||||||
}
|
}
|
||||||
function isFresh(ts){ return (Date.now() - ts) < CACHE_TTL_MS; }
|
function isFresh(ts){ return (Date.now() - ts) < CACHE_TTL_MS; }
|
||||||
|
|
||||||
function buildHeaders(){
|
async function buildHeaders(){
|
||||||
const base = { 'X-Dewemoji-Frontend': FRONTEND_ID };
|
const base = { 'X-Dewemoji-Frontend': FRONTEND_ID };
|
||||||
if (licenseValid && licenseKeyCurrent) {
|
try {
|
||||||
|
if (chrome?.runtime?.id) base['X-Extension-Id'] = chrome.runtime.id;
|
||||||
|
} catch {}
|
||||||
|
if (licenseValid && licenseKeyCurrent) {
|
||||||
try {
|
try {
|
||||||
const acctPromise = accountId();
|
const acctPromise = accountId();
|
||||||
return acctPromise.then(acct => ({
|
return acctPromise.then(acct => ({
|
||||||
@@ -596,9 +642,63 @@ function buildHeaders(){
|
|||||||
return Promise.resolve({ ...base, 'Authorization': `Bearer ${licenseKeyCurrent}`, 'X-License-Key': licenseKeyCurrent });
|
return Promise.resolve({ ...base, 'Authorization': `Bearer ${licenseKeyCurrent}`, 'X-License-Key': licenseKeyCurrent });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.resolve(base);
|
return base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getListPaths(){
|
||||||
|
// Try newest route first, then legacy-compatible fallbacks.
|
||||||
|
return ['/extension/search', '/emojis', '/search'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApiBases(){
|
||||||
|
const list = [
|
||||||
|
API.base,
|
||||||
|
'https://dewemoji.backoffice.biz.id/v1',
|
||||||
|
'https://api.dewemoji.com/v1',
|
||||||
|
'http://127.0.0.1:8000/v1',
|
||||||
|
];
|
||||||
|
const seen = new Set();
|
||||||
|
const out = [];
|
||||||
|
for (const raw of list) {
|
||||||
|
const base = String(raw || '').trim().replace(/\/+$/, '');
|
||||||
|
if (!base || seen.has(base)) continue;
|
||||||
|
seen.add(base);
|
||||||
|
out.push(base);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJsonWithFallback(params, headers, { quiet = false } = {}) {
|
||||||
|
let lastRes = null;
|
||||||
|
const tried = [];
|
||||||
|
const paths = getListPaths();
|
||||||
|
const bases = getApiBases();
|
||||||
|
|
||||||
|
for (const base of bases) {
|
||||||
|
for (const path of paths) {
|
||||||
|
const url = `${base}${path}?${params.toString()}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, { cache: 'no-store', headers });
|
||||||
|
tried.push(`${base}${path}:${res.status}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.json().catch(() => null);
|
||||||
|
// Lock to first healthy base to keep future requests fast.
|
||||||
|
API.base = base;
|
||||||
|
return { res, data, path, base };
|
||||||
|
}
|
||||||
|
lastRes = res;
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err && err.message ? err.message : 'network_error';
|
||||||
|
tried.push(`${base}${path}:${msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quiet) return { res: lastRes, data: null, path: null, base: null };
|
||||||
|
throw new Error(`API ${lastRes ? lastRes.status : 0} (${tried.join(' | ')})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function prefetchNextIfNeeded(currentSig){
|
async function prefetchNextIfNeeded(currentSig){
|
||||||
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
|
||||||
@@ -622,15 +722,10 @@ async function prefetchNextIfNeeded(currentSig){
|
|||||||
if (q.value.trim()) params.set('q', q.value.trim());
|
if (q.value.trim()) params.set('q', q.value.trim());
|
||||||
if (cat.value.trim()) params.set('category', cat.value.trim());
|
if (cat.value.trim()) params.set('category', cat.value.trim());
|
||||||
if (sub.value.trim()) params.set('subcategory', sub.value.trim());
|
if (sub.value.trim()) params.set('subcategory', sub.value.trim());
|
||||||
const url = `${API.base}${API.list}?${params.toString()}`;
|
|
||||||
|
|
||||||
// headers (with optional license/account)
|
|
||||||
const headers = await buildHeaders();
|
const headers = await buildHeaders();
|
||||||
|
|
||||||
// fetch quietly; avoid throwing UI errors — prefetch is best-effort
|
// fetch quietly; avoid throwing UI errors — prefetch is best-effort
|
||||||
const res = await fetch(url, { cache: 'no-store', headers });
|
const { data } = await fetchJsonWithFallback(params, headers, { quiet: true });
|
||||||
if (!res.ok) { PREFETCHING.delete(key); return; }
|
|
||||||
const data = await res.json().catch(()=>null);
|
|
||||||
if (!data || !Array.isArray(data.items)) { PREFETCHING.delete(key); return; }
|
if (!data || !Array.isArray(data.items)) { PREFETCHING.delete(key); return; }
|
||||||
|
|
||||||
const record = { ts: Date.now(), data };
|
const record = { ts: Date.now(), data };
|
||||||
@@ -638,7 +733,9 @@ async function prefetchNextIfNeeded(currentSig){
|
|||||||
const persisted2 = await getPersistedCache();
|
const persisted2 = await getPersistedCache();
|
||||||
persisted2[key] = record;
|
persisted2[key] = record;
|
||||||
// prune stale
|
// prune stale
|
||||||
for (const k of Object.keys(persisted2)) { if (!persisted2[k] || !isFresh(persisted2[k].ts)) delete persisted2[k]; }
|
for (const k of Object.keys(persisted2)) {
|
||||||
|
if (!persisted2[k] || !isFresh(persisted2[k].ts)) delete persisted2[k];
|
||||||
|
}
|
||||||
await savePersistedCache(persisted2);
|
await savePersistedCache(persisted2);
|
||||||
} catch { /* silent */ }
|
} catch { /* silent */ }
|
||||||
finally { PREFETCHING.delete(`${currentSig}|${page+1}`); }
|
finally { PREFETCHING.delete(`${currentSig}|${page+1}`); }
|
||||||
@@ -712,12 +809,10 @@ async function fetchPage(reset=false) {
|
|||||||
if (q.value.trim()) params.set('q', q.value.trim());
|
if (q.value.trim()) params.set('q', q.value.trim());
|
||||||
if (cat.value.trim()) params.set('category', cat.value.trim());
|
if (cat.value.trim()) params.set('category', cat.value.trim());
|
||||||
if (sub.value.trim()) params.set('subcategory', sub.value.trim());
|
if (sub.value.trim()) params.set('subcategory', sub.value.trim());
|
||||||
const url = `${API.base}${API.list}?${params.toString()}`;
|
const headers = await buildHeaders();
|
||||||
|
|
||||||
// (usage increment moved to after successful fetch)
|
// (usage increment moved to after successful fetch)
|
||||||
|
|
||||||
const headers = await buildHeaders();
|
|
||||||
|
|
||||||
// Optional nicety: offline guard (early exit if no network AND no fresh cache)
|
// Optional nicety: offline guard (early exit if no network AND no fresh cache)
|
||||||
if (!navigator.onLine) {
|
if (!navigator.onLine) {
|
||||||
const hasFreshMem = mem && isFresh(mem.ts);
|
const hasFreshMem = mem && isFresh(mem.ts);
|
||||||
@@ -727,11 +822,9 @@ async function fetchPage(reset=false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(url, { cache: 'no-store', headers });
|
const { res, data } = await fetchJsonWithFallback(params, headers);
|
||||||
|
if (!res || !data) throw new Error('API 0');
|
||||||
if (!res.ok) throw new Error(`API ${res.status}`);
|
|
||||||
lastServerTier = res.headers.get('X-Dewemoji-Tier'); // 'pro' for Pro; null for Free/whitelist
|
lastServerTier = res.headers.get('X-Dewemoji-Tier'); // 'pro' for Pro; null for Free/whitelist
|
||||||
const data = await res.json();
|
|
||||||
total = data.total || 0;
|
total = data.total || 0;
|
||||||
|
|
||||||
if (page === 1 && (!Array.isArray(data.items) || data.items.length === 0)) {
|
if (page === 1 && (!Array.isArray(data.items) || data.items.length === 0)) {
|
||||||
@@ -1525,11 +1618,30 @@ licenseDeactivateBtn?.addEventListener('click', 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');
|
||||||
|
|
||||||
lines.push(`Active editable type: ${obj.activeType ?? 'none'}`);
|
lines.push(`Active editable type: ${obj.activeType ?? 'none'}`);
|
||||||
lines.push(`Has caret/selection: ${obj.hasRange ? 'yes' : 'no'}`);
|
lines.push(`Has caret/selection: ${obj.hasRange ? 'yes' : 'no'}`);
|
||||||
lines.push(`Last insert result: ${obj.lastInsertOK === null ? 'n/a' : obj.lastInsertOK ? 'success' : 'failed'}`);
|
lines.push(`Last insert result: ${obj.lastInsertOK === null ? 'n/a' : obj.lastInsertOK ? 'success' : 'failed'}`);
|
||||||
@@ -1537,25 +1649,34 @@ licenseDeactivateBtn?.addEventListener('click', async () => {
|
|||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
diagRunBtn?.addEventListener('click', async () => {
|
async function runDiagnostics({ forceToken = false } = {}) {
|
||||||
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; }
|
||||||
|
|
||||||
const ready = await ensureContentScript(tab.id);
|
const ready = await ensureContentScript(tab.id);
|
||||||
if (!ready) { diagOut.textContent = renderDiag(null); return; }
|
if (!ready) { diagOut.textContent = await renderDiag(null); return; }
|
||||||
|
|
||||||
const info = await chrome.tabs.sendMessage(tab.id, { type: 'dewemoji_diag' });
|
const info = await chrome.tabs.sendMessage(tab.id, { type: 'dewemoji_diag' });
|
||||||
diagOut.textContent = renderDiag(info || null);
|
diagOut.textContent = await renderDiag(info || null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
diagOut.textContent = `Error: ${e?.message || e}`;
|
diagOut.textContent = `Error: ${e?.message || e}`;
|
||||||
} finally {
|
} finally {
|
||||||
diagSpin.style.display = 'none';
|
diagSpin.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
diagRunBtn?.addEventListener('click', () => runDiagnostics({ forceToken: false }));
|
||||||
|
diagForceBtn?.addEventListener('click', () => runDiagnostics({ forceToken: true }));
|
||||||
|
|
||||||
async function performEmojiAction(glyph) {
|
async function performEmojiAction(glyph) {
|
||||||
// Free users always copy
|
// Free users always copy
|
||||||
@@ -1852,4 +1973,4 @@ openSheet = function(){
|
|||||||
document.getElementById('tab-general')?.classList.add('active');
|
document.getElementById('tab-general')?.classList.add('active');
|
||||||
document.getElementById('tab-pro')?.classList.remove('active');
|
document.getElementById('tab-pro')?.classList.remove('active');
|
||||||
renderToneSettingsSection();
|
renderToneSettingsSection();
|
||||||
};
|
};
|
||||||
|
|||||||