diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/assets/icon-128.png b/assets/icon-128.png old mode 100644 new mode 100755 diff --git a/assets/icon-16.png b/assets/icon-16.png old mode 100644 new mode 100755 diff --git a/assets/icon-32.png b/assets/icon-32.png old mode 100644 new mode 100755 diff --git a/assets/icon-48.png b/assets/icon-48.png old mode 100644 new mode 100755 diff --git a/background.js b/background.js old mode 100644 new mode 100755 index 9b16f2f..e6a2d62 --- a/background.js +++ b/background.js @@ -1,11 +1,95 @@ // Let Chrome open the panel when the toolbar icon is clicked // Guard: some Chromium variants may not expose sidePanel 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(() => { 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: chrome.action.onClicked.addListener(async () => { 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.open({ tabId: tab.id }); -}); \ No newline at end of file +}); diff --git a/content.js b/content.js old mode 100644 new mode 100755 diff --git a/extension-test-walkthrough.md b/extension-test-walkthrough.md new file mode 100644 index 0000000..98a602c --- /dev/null +++ b/extension-test-walkthrough.md @@ -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. diff --git a/manifest.json b/manifest.json old mode 100644 new mode 100755 index 9761280..b287fb2 --- a/manifest.json +++ b/manifest.json @@ -7,7 +7,8 @@ "storage", "scripting", "activeTab", - "sidePanel" + "sidePanel", + "gcm" ], "host_permissions": [ "", @@ -62,4 +63,4 @@ "description": "Toggle Emoji Side Panel" } } -} \ No newline at end of file +} diff --git a/panel.html b/panel.html old mode 100644 new mode 100755 index f523b0c..c01769d --- a/panel.html +++ b/panel.html @@ -107,11 +107,12 @@ - -