diff --git a/extension/background/index.ts b/extension/background/index.ts index 636e3fb5..0c42b95d 100644 --- a/extension/background/index.ts +++ b/extension/background/index.ts @@ -32,7 +32,10 @@ import { UI_CONSTANTS, APPROVAL_CONSTANTS, CHAIN_ID, + DISPLAY_MODES, + RUNTIME_MESSAGE_TYPES, } from '../shared/constants'; +import type { DisplayMode, ApprovalType } from '../shared/constants'; import { getEffectiveRpcConfig } from '../shared/rpc-config'; import type { RpcConfig } from '../shared/rpc-config'; import type { @@ -42,6 +45,12 @@ import type { SignRawTxRequest, WalletTransaction, } from '../shared/types'; +import { SIDE_PANEL_DEFAULT_PATH, APPROVAL_PROVIDER_METHODS } from '../shared/side-panel'; +import { + buildPendingApprovalSessionSnapshot, + persistPendingApprovalSession, + loadPendingApprovalSession, +} from '../shared/pending-approvals-session'; const vault = new Vault(); let lastActivity = Date.now(); @@ -50,11 +59,18 @@ let manuallyLocked = false; // Track if user manually locked (don't auto-unlock) let approvalWindowId: number | null = null; // Track the approval popup window for reuse let isCreatingWindow = false; // Prevent race condition when creating window let currentRequestId: string | null = null; // Currently displayed request +let currentRequestType: ApprovalType | null = null; // Type of currently displayed request let requestQueue: Array<{ id: string; type: 'connect' | 'transaction' | 'sign-message' | 'sign-raw-tx'; }> = []; // Queued requests +/** + * In-memory display mode cache so the user-gesture side panel hook can run + * synchronously inside the onMessage listener (any await loses the gesture). + */ +let cachedDisplayMode: DisplayMode = DISPLAY_MODES.POPUP; + /** * In-memory cache of approved origins * Loaded from storage on startup, persisted on changes @@ -242,6 +258,60 @@ function cancelPendingRequest(requestId: string, code?: number, message?: string }); if (currentRequestId == requestId) { currentRequestId = null; + currentRequestType = null; + } + void syncPendingApprovalsSession(); +} + +function orphanedProviderResponse(_response: unknown): void { + // The content-script callback is gone after a service worker restart. +} + +async function syncPendingApprovalsSession(): Promise { + const snapshot = buildPendingApprovalSessionSnapshot( + pendingRequests, + currentRequestId, + currentRequestType, + requestQueue, + isRequestExpired + ); + await persistPendingApprovalSession(snapshot); +} + +async function restorePendingApprovalsSession(): Promise { + const snapshot = await loadPendingApprovalSession(); + if (!snapshot) { + return; + } + + pendingRequests.clear(); + + for (const [id, entry] of Object.entries(snapshot.pending)) { + if (isRequestExpired(entry.request.timestamp)) { + continue; + } + + pendingRequests.set(id, { + request: entry.request, + origin: entry.origin, + tabId: entry.tabId, + sendResponse: orphanedProviderResponse, + }); + } + + requestQueue = snapshot.queue.filter(item => pendingRequests.has(item.id)); + currentRequestId = + snapshot.currentRequestId && pendingRequests.has(snapshot.currentRequestId) + ? snapshot.currentRequestId + : null; + currentRequestType = currentRequestId ? snapshot.currentRequestType : null; + + if ( + currentRequestId && + currentRequestType && + (await getDisplayMode()) === DISPLAY_MODES.SIDE_PANEL + ) { + await notifyApprovalPending(currentRequestId, currentRequestType); } } @@ -423,6 +493,7 @@ interface PendingRequest { sendResponse: (response: any) => void; origin: string; needsUnlock?: boolean; // Flag indicating request is waiting for wallet unlock + tabId?: number; } const pendingRequests = new Map(); @@ -463,6 +534,83 @@ function isTransactionRequest( return 'to' in request; } +async function getDisplayMode(): Promise { + if (!chrome.sidePanel) { + return DISPLAY_MODES.POPUP; + } + + const stored = await chrome.storage.local.get([STORAGE_KEYS.DISPLAY_MODE]); + const mode = stored[STORAGE_KEYS.DISPLAY_MODE]; + return mode === DISPLAY_MODES.SIDE_PANEL ? DISPLAY_MODES.SIDE_PANEL : DISPLAY_MODES.POPUP; +} + +async function applyDisplayMode(mode: DisplayMode): Promise { + const effectiveMode = + mode === DISPLAY_MODES.SIDE_PANEL && chrome.sidePanel + ? DISPLAY_MODES.SIDE_PANEL + : DISPLAY_MODES.POPUP; + + cachedDisplayMode = effectiveMode; + + if (effectiveMode === DISPLAY_MODES.SIDE_PANEL) { + await chrome.action.setPopup({ popup: '' }); + await chrome.sidePanel!.setPanelBehavior({ openPanelOnActionClick: true }); + await chrome.sidePanel!.setOptions({ path: SIDE_PANEL_DEFAULT_PATH, enabled: true }); + } else { + await chrome.action.setPopup({ popup: 'popup/index.html' }); + if (chrome.sidePanel) { + await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: false }); + } + } +} + +async function notifyApprovalPending(requestId: string, type: ApprovalType): Promise { + try { + await chrome.runtime.sendMessage({ + type: RUNTIME_MESSAGE_TYPES.APPROVAL_PENDING, + requestId, + approvalType: type, + }); + } catch { + // Side panel may not be open yet; it will query on mount via GET_PENDING_APPROVAL + } +} + +async function openSidePanelForApproval(tabId?: number): Promise { + if (!chrome.sidePanel) { + return; + } + + try { + if (tabId !== undefined) { + await chrome.sidePanel.setOptions({ + tabId, + path: SIDE_PANEL_DEFAULT_PATH, + enabled: true, + }); + await chrome.sidePanel.open({ tabId }); + return; + } + + const currentWindow = await chrome.windows.getLastFocused({ populate: false }); + if (currentWindow.id !== undefined) { + await chrome.sidePanel.open({ windowId: currentWindow.id }); + } + } catch (error) { + // Panel may already be open, or open() may lack a user gesture — routing still works via message. + console.warn('[Background] sidePanel.open failed (panel may already be open):', error); + } +} + +async function routeApprovalToSidePanel( + requestId: string, + type: ApprovalType, + tabId?: number +): Promise { + await openSidePanelForApproval(tabId); + await notifyApprovalPending(requestId, type); +} + async function createPopupWithFallback(opts: any): Promise { // First try: with left/top try { @@ -480,10 +628,7 @@ async function createPopupWithFallback(opts: any): Promise { * Uses MetaMask pattern: single popup window for all approval requests * Queues requests if user is currently viewing another request */ -async function createApprovalPopup( - requestId: string, - type: 'connect' | 'transaction' | 'sign-message' | 'sign-raw-tx' -) { +async function createApprovalPopup(requestId: string, type: ApprovalType, tabId?: number) { // If user is currently viewing a different request, queue this one if (currentRequestId !== null && currentRequestId !== requestId) { // Check if already in queue to prevent duplicates @@ -491,11 +636,20 @@ async function createApprovalPopup( if (!alreadyQueued) { requestQueue.push({ id: requestId, type }); } + void syncPendingApprovalsSession(); return; } // Mark this request as currently displayed currentRequestId = requestId; + currentRequestType = type; + + const displayMode = await getDisplayMode(); + if (displayMode === DISPLAY_MODES.SIDE_PANEL) { + await routeApprovalToSidePanel(requestId, type, tabId); + void syncPendingApprovalsSession(); + return; + } let hashPrefix: string; if (type === 'connect') { @@ -532,7 +686,7 @@ async function createApprovalPopup( // Prevent race condition: if window is being created, wait and retry if (isCreatingWindow) { await new Promise(resolve => setTimeout(resolve, 100)); - return createApprovalPopup(requestId, type); + return createApprovalPopup(requestId, type, tabId); } // Create new approval window @@ -580,6 +734,8 @@ async function createApprovalPopup( } finally { isCreatingWindow = false; } + + void syncPendingApprovalsSession(); } /** @@ -590,6 +746,7 @@ function processNextRequest() { if (currentRequestId !== null) { cancelPendingRequest(currentRequestId); } + currentRequestType = null; if (requestQueue.length > 0) { while (true) { @@ -597,10 +754,12 @@ function processNextRequest() { if (!pendingRequests.has(next.id)) { continue; } - createApprovalPopup(next.id, next.type); + createApprovalPopup(next.id, next.type, pendingRequests.get(next.id)?.tabId); break; } } + + void syncPendingApprovalsSession(); } /** @@ -747,8 +906,11 @@ const initPromise = (async () => { await loadApprovedOrigins(); await vault.init(); // Load encrypted vault header to detect vault existence + await restorePendingApprovalsSession(); await restoreUnlockSession(); // Rehydrate unlock state if still within auto-lock window + await applyDisplayMode(await getDisplayMode()); + // Only schedule alarm if auto-lock is enabled, otherwise ensure any stale alarm is cleared if (autoLockMinutes > 0) { scheduleAlarm(); @@ -757,6 +919,10 @@ const initPromise = (async () => { } })(); +chrome.runtime.onInstalled.addListener(async () => { + await applyDisplayMode(await getDisplayMode()); +}); + // Clean up approval window ID when window is closed chrome.windows.onRemoved.addListener(windowId => { if (windowId === approvalWindowId) { @@ -790,10 +956,41 @@ function isFromPopup(sender: chrome.runtime.MessageSender): boolean { return url.startsWith(`chrome-extension://${extensionId}/`); } +/** + * Synchronous user-gesture hook: open the side panel for approval-bearing + * provider requests while the gesture from the dApp click is still active. + * Must run before any await in the listener — awaiting consumes the gesture. + */ +function maybeOpenSidePanelOnGesture(msg: any, sender: chrome.runtime.MessageSender): void { + if (cachedDisplayMode !== DISPLAY_MODES.SIDE_PANEL || !chrome.sidePanel) { + return; + } + + const tabId = sender.tab?.id; + if (tabId === undefined || isFromPopup(sender)) { + return; + } + + const method = msg?.payload?.method; + if ( + typeof method !== 'string' || + (!APPROVAL_PROVIDER_METHODS.has(method) && method !== 'nock_signRawTx') + ) { + return; + } + + chrome.sidePanel.open({ tabId }).catch(() => { + // Gesture may have been consumed or panel already open; background + // routing via APPROVAL_PENDING still delivers the request. + }); +} + /** * Handle messages from content script and popup */ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { + maybeOpenSidePanelOnGesture(msg, _sender); + (async () => { await initPromise; await ensureSessionRestored(); @@ -850,10 +1047,13 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { void sendBridgedResponse(response); }, origin: connectRequest.origin, + tabId: _sender.tab?.id, }); + void syncPendingApprovalsSession(); + // Create approval popup - await createApprovalPopup(connectRequestId, 'connect'); + await createApprovalPopup(connectRequestId, 'connect', _sender.tab?.id); // Response will be sent when user approves/rejects return; @@ -905,10 +1105,13 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { void sendBridgedResponse(response); }, origin: signRequest.origin, + tabId: _sender.tab?.id, }); + void syncPendingApprovalsSession(); + // Create approval popup - await createApprovalPopup(newSignRequestId, 'sign-message'); + await createApprovalPopup(newSignRequestId, 'sign-message', _sender.tab?.id); // Response will be sent when user approves/rejects return; @@ -958,10 +1161,13 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { void sendBridgedResponse(response); }, origin: signRawTxRequest.origin, + tabId: _sender.tab?.id, }); + void syncPendingApprovalsSession(); + // Create approval popup - await createApprovalPopup(signRawTxId, 'sign-raw-tx'); + await createApprovalPopup(signRawTxId, 'sign-raw-tx', _sender.tab?.id); // Response will be sent when user approves/rejects return; @@ -1013,10 +1219,13 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { void sendBridgedResponse(response); }, origin: txRequest.origin, + tabId: _sender.tab?.id, }); + void syncPendingApprovalsSession(); + // Create approval popup - await createApprovalPopup(txRequestId, 'transaction'); + await createApprovalPopup(txRequestId, 'transaction', _sender.tab?.id); // Response will be sent when user approves/rejects return; @@ -1064,6 +1273,36 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { sendResponse({ ok: true }); return; + case INTERNAL_METHODS.GET_DISPLAY_MODE: + sendResponse({ mode: await getDisplayMode() }); + return; + + case INTERNAL_METHODS.SET_DISPLAY_MODE: { + const requestedMode = payload.params?.[0]; + if (requestedMode !== DISPLAY_MODES.POPUP && requestedMode !== DISPLAY_MODES.SIDE_PANEL) { + sendResponse({ error: ERROR_CODES.INVALID_PARAMS }); + return; + } + + const resolvedMode = + requestedMode === DISPLAY_MODES.SIDE_PANEL && chrome.sidePanel + ? DISPLAY_MODES.SIDE_PANEL + : DISPLAY_MODES.POPUP; + + await chrome.storage.local.set({ [STORAGE_KEYS.DISPLAY_MODE]: resolvedMode }); + await applyDisplayMode(resolvedMode); + sendResponse({ success: true, mode: resolvedMode }); + return; + } + + case INTERNAL_METHODS.GET_PENDING_APPROVAL: + if (currentRequestId && currentRequestType && pendingRequests.has(currentRequestId)) { + sendResponse({ requestId: currentRequestId, approvalType: currentRequestType }); + } else { + sendResponse(null); + } + return; + case INTERNAL_METHODS.UNLOCK: const unlockResult = await vault.unlock(payload.params?.[0]); // password sendResponse(unlockResult); diff --git a/extension/manifest.json b/extension/manifest.json index 8feb01f3..b7bbfa09 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -24,7 +24,10 @@ "service_worker": "background/index.ts", "type": "module" }, - "permissions": ["storage", "alarms"], + "permissions": ["storage", "alarms", "sidePanel"], + "side_panel": { + "default_path": "sidepanel/index.html" + }, "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; font-src 'self'" }, diff --git a/extension/popup/Router.tsx b/extension/popup/Router.tsx index 05e4ded0..b4533d56 100644 --- a/extension/popup/Router.tsx +++ b/extension/popup/Router.tsx @@ -30,6 +30,7 @@ import { TransactionApprovalScreen } from './screens/approvals/TransactionApprov import { SignRawTxScreen } from './screens/approvals/SignRawTxScreen'; import { SettingsScreen } from './screens/SettingsScreen'; import { ThemeSettingsScreen } from './screens/ThemeSettingsScreen'; +import { DisplayModeScreen } from './screens/DisplayModeScreen'; import { LockTimeScreen } from './screens/LockTimeScreen'; import { KeySettingsPasswordScreen } from './screens/KeySettingsPasswordScreen'; import { RpcSettingsScreen } from './screens/RpcSettingsScreen'; @@ -84,6 +85,8 @@ export function Router() { return ; case 'theme-settings': return ; + case 'display-mode': + return ; case 'lock-time': return ; case 'key-settings': diff --git a/extension/popup/assets/display-mode-icon.svg b/extension/popup/assets/display-mode-icon.svg new file mode 100644 index 00000000..2aab7c12 --- /dev/null +++ b/extension/popup/assets/display-mode-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/extension/popup/assets/wallet-icon-bolt.svg b/extension/popup/assets/wallet-icon-bolt.svg new file mode 100644 index 00000000..c28760ef --- /dev/null +++ b/extension/popup/assets/wallet-icon-bolt.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-cat-eye.svg b/extension/popup/assets/wallet-icon-cat-eye.svg new file mode 100644 index 00000000..a04cd3a1 --- /dev/null +++ b/extension/popup/assets/wallet-icon-cat-eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-fibonacci.svg b/extension/popup/assets/wallet-icon-fibonacci.svg new file mode 100644 index 00000000..61900b4d --- /dev/null +++ b/extension/popup/assets/wallet-icon-fibonacci.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-gaze-down.svg b/extension/popup/assets/wallet-icon-gaze-down.svg new file mode 100644 index 00000000..def3a29c --- /dev/null +++ b/extension/popup/assets/wallet-icon-gaze-down.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-gaze-left.svg b/extension/popup/assets/wallet-icon-gaze-left.svg new file mode 100644 index 00000000..4c7c12a6 --- /dev/null +++ b/extension/popup/assets/wallet-icon-gaze-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-gaze-right.svg b/extension/popup/assets/wallet-icon-gaze-right.svg new file mode 100644 index 00000000..d1614736 --- /dev/null +++ b/extension/popup/assets/wallet-icon-gaze-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-gem.svg b/extension/popup/assets/wallet-icon-gem.svg new file mode 100644 index 00000000..c3488688 --- /dev/null +++ b/extension/popup/assets/wallet-icon-gem.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-heart.svg b/extension/popup/assets/wallet-icon-heart.svg new file mode 100644 index 00000000..da9b57cd --- /dev/null +++ b/extension/popup/assets/wallet-icon-heart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-keyhole.svg b/extension/popup/assets/wallet-icon-keyhole.svg new file mode 100644 index 00000000..9126b362 --- /dev/null +++ b/extension/popup/assets/wallet-icon-keyhole.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-moon.svg b/extension/popup/assets/wallet-icon-moon.svg new file mode 100644 index 00000000..1cd469b0 --- /dev/null +++ b/extension/popup/assets/wallet-icon-moon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-nock-n-round.svg b/extension/popup/assets/wallet-icon-nock-n-round.svg new file mode 100644 index 00000000..1d1cdf97 --- /dev/null +++ b/extension/popup/assets/wallet-icon-nock-n-round.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-nock-n.svg b/extension/popup/assets/wallet-icon-nock-n.svg new file mode 100644 index 00000000..39e41d8e --- /dev/null +++ b/extension/popup/assets/wallet-icon-nock-n.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-north-star.svg b/extension/popup/assets/wallet-icon-north-star.svg new file mode 100644 index 00000000..9c39eb85 --- /dev/null +++ b/extension/popup/assets/wallet-icon-north-star.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-orbit.svg b/extension/popup/assets/wallet-icon-orbit.svg new file mode 100644 index 00000000..170156f1 --- /dev/null +++ b/extension/popup/assets/wallet-icon-orbit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-pinwheel-sun.svg b/extension/popup/assets/wallet-icon-pinwheel-sun.svg new file mode 100644 index 00000000..c282095e --- /dev/null +++ b/extension/popup/assets/wallet-icon-pinwheel-sun.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-quatrefoil-eye.svg b/extension/popup/assets/wallet-icon-quatrefoil-eye.svg new file mode 100644 index 00000000..6ffd6fcc --- /dev/null +++ b/extension/popup/assets/wallet-icon-quatrefoil-eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-ring.svg b/extension/popup/assets/wallet-icon-ring.svg new file mode 100644 index 00000000..58ad8a52 --- /dev/null +++ b/extension/popup/assets/wallet-icon-ring.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-seal-eye.svg b/extension/popup/assets/wallet-icon-seal-eye.svg new file mode 100644 index 00000000..248b5b83 --- /dev/null +++ b/extension/popup/assets/wallet-icon-seal-eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-seal-fibonacci.svg b/extension/popup/assets/wallet-icon-seal-fibonacci.svg new file mode 100644 index 00000000..2c687caf --- /dev/null +++ b/extension/popup/assets/wallet-icon-seal-fibonacci.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-snowflake.svg b/extension/popup/assets/wallet-icon-snowflake.svg new file mode 100644 index 00000000..e40e4478 --- /dev/null +++ b/extension/popup/assets/wallet-icon-snowflake.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-spiral.svg b/extension/popup/assets/wallet-icon-spiral.svg new file mode 100644 index 00000000..16f1cceb --- /dev/null +++ b/extension/popup/assets/wallet-icon-spiral.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-squircle-eye.svg b/extension/popup/assets/wallet-icon-squircle-eye.svg new file mode 100644 index 00000000..965d6a90 --- /dev/null +++ b/extension/popup/assets/wallet-icon-squircle-eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-style-10.svg b/extension/popup/assets/wallet-icon-style-10.svg deleted file mode 100644 index 4b3cc77a..00000000 --- a/extension/popup/assets/wallet-icon-style-10.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-style-11.svg b/extension/popup/assets/wallet-icon-style-11.svg deleted file mode 100644 index de4b5a4e..00000000 --- a/extension/popup/assets/wallet-icon-style-11.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-style-12.svg b/extension/popup/assets/wallet-icon-style-12.svg deleted file mode 100644 index 742646b3..00000000 --- a/extension/popup/assets/wallet-icon-style-12.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-style-13.svg b/extension/popup/assets/wallet-icon-style-13.svg deleted file mode 100644 index 96e8dc42..00000000 --- a/extension/popup/assets/wallet-icon-style-13.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-style-15.svg b/extension/popup/assets/wallet-icon-style-15.svg deleted file mode 100644 index 88489601..00000000 --- a/extension/popup/assets/wallet-icon-style-15.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-style-4.svg b/extension/popup/assets/wallet-icon-style-4.svg deleted file mode 100644 index d2fe810f..00000000 --- a/extension/popup/assets/wallet-icon-style-4.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-style-5.svg b/extension/popup/assets/wallet-icon-style-5.svg deleted file mode 100644 index 893a774c..00000000 --- a/extension/popup/assets/wallet-icon-style-5.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-style-6.svg b/extension/popup/assets/wallet-icon-style-6.svg deleted file mode 100644 index 7654a851..00000000 --- a/extension/popup/assets/wallet-icon-style-6.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-style-7.svg b/extension/popup/assets/wallet-icon-style-7.svg deleted file mode 100644 index 6370838a..00000000 --- a/extension/popup/assets/wallet-icon-style-7.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-style-9.svg b/extension/popup/assets/wallet-icon-style-9.svg deleted file mode 100644 index 26734485..00000000 --- a/extension/popup/assets/wallet-icon-style-9.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/extension/popup/assets/wallet-icon-sun.svg b/extension/popup/assets/wallet-icon-sun.svg new file mode 100644 index 00000000..8d752998 --- /dev/null +++ b/extension/popup/assets/wallet-icon-sun.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/assets/wallet-icon-target.svg b/extension/popup/assets/wallet-icon-target.svg new file mode 100644 index 00000000..2516d61c --- /dev/null +++ b/extension/popup/assets/wallet-icon-target.svg @@ -0,0 +1,4 @@ + + + + diff --git a/extension/popup/components/AccountIcon.tsx b/extension/popup/components/AccountIcon.tsx index 0ed5b481..07675006 100644 --- a/extension/popup/components/AccountIcon.tsx +++ b/extension/popup/components/AccountIcon.tsx @@ -1,43 +1,11 @@ import { useEffect, useState } from 'react'; -// Import all wallet icon styles -import WalletStyle1 from '../assets/wallet-icon-style-1.svg'; -import WalletStyle2 from '../assets/wallet-icon-style-2.svg'; -import WalletStyle3 from '../assets/wallet-icon-style-3.svg'; -import WalletStyle4 from '../assets/wallet-icon-style-4.svg'; -import WalletStyle5 from '../assets/wallet-icon-style-5.svg'; -import WalletStyle6 from '../assets/wallet-icon-style-6.svg'; -import WalletStyle7 from '../assets/wallet-icon-style-7.svg'; -import WalletStyle8 from '../assets/wallet-icon-style-8.svg'; -import WalletStyle9 from '../assets/wallet-icon-style-9.svg'; -import WalletStyle10 from '../assets/wallet-icon-style-10.svg'; -import WalletStyle11 from '../assets/wallet-icon-style-11.svg'; -import WalletStyle12 from '../assets/wallet-icon-style-12.svg'; -import WalletStyle13 from '../assets/wallet-icon-style-13.svg'; -import WalletStyle14 from '../assets/wallet-icon-style-14.svg'; -import WalletStyle15 from '../assets/wallet-icon-style-15.svg'; - -const iconStyles = [ - { id: 1, icon: WalletStyle1 }, - { id: 2, icon: WalletStyle2 }, - { id: 3, icon: WalletStyle3 }, - { id: 4, icon: WalletStyle4 }, - { id: 5, icon: WalletStyle5 }, - { id: 6, icon: WalletStyle6 }, - { id: 7, icon: WalletStyle7 }, - { id: 8, icon: WalletStyle8 }, - { id: 9, icon: WalletStyle9 }, - { id: 10, icon: WalletStyle10 }, - { id: 11, icon: WalletStyle11 }, - { id: 12, icon: WalletStyle12 }, - { id: 13, icon: WalletStyle13 }, - { id: 14, icon: WalletStyle14 }, - { id: 15, icon: WalletStyle15 }, -]; +import { DEFAULT_WALLET_STYLE, normalizeIconStyleId } from '../../shared/walletStyles'; +import { WALLET_ICON_ASSETS } from './walletIconAssets'; interface AccountIconProps { - /** Icon style ID (1-15) */ - styleId?: number; + /** Icon style id (slug; legacy numeric ids are mapped automatically) */ + styleId?: number | string; /** Icon color (hex string) */ color?: string; /** CSS class names */ @@ -49,16 +17,18 @@ interface AccountIconProps { * Fetches the SVG and applies the color dynamically */ export function AccountIcon({ - styleId = 1, - color = '#FFC413', + styleId, + color = DEFAULT_WALLET_STYLE.iconColor, className = 'h-6 w-6', }: AccountIconProps) { const [svgContent, setSvgContent] = useState(''); + const iconId = normalizeIconStyleId(styleId); + useEffect(() => { - const selectedIcon = iconStyles.find(s => s.id === styleId) || iconStyles[0]; + const asset = WALLET_ICON_ASSETS[iconId]; - fetch(selectedIcon.icon) + fetch(asset) .then(res => res.text()) .then(text => { // Replace CSS var `--fill-0` with the chosen color @@ -72,7 +42,7 @@ export function AccountIcon({ `` ); }); - }, [styleId, color]); + }, [iconId, color]); return
; } diff --git a/extension/popup/components/ScreenContainer.tsx b/extension/popup/components/ScreenContainer.tsx index c949aece..535870e6 100644 --- a/extension/popup/components/ScreenContainer.tsx +++ b/extension/popup/components/ScreenContainer.tsx @@ -10,7 +10,7 @@ interface ScreenContainerProps { } export function ScreenContainer({ children, className = '' }: ScreenContainerProps) { - const baseClasses = 'w-[357px] h-[600px] p-4'; + const baseClasses = 'w-full h-full p-4'; const combinedClasses = className ? `${baseClasses} ${className}` : baseClasses; return
{children}
; diff --git a/extension/popup/components/walletIconAssets.ts b/extension/popup/components/walletIconAssets.ts new file mode 100644 index 00000000..2bf4fd6b --- /dev/null +++ b/extension/popup/components/walletIconAssets.ts @@ -0,0 +1,67 @@ +/** + * Maps wallet icon style ids (see shared/walletStyles.ts) to their SVG assets. + * Every asset uses the `var(--fill-0, #FFC413)` placeholder so it can be + * tinted with any account color at render time. + */ + +import Style1 from '../assets/wallet-icon-style-1.svg'; +import Style2 from '../assets/wallet-icon-style-2.svg'; +import Style3 from '../assets/wallet-icon-style-3.svg'; +import Style8 from '../assets/wallet-icon-style-8.svg'; +import Style14 from '../assets/wallet-icon-style-14.svg'; +import GazeLeft from '../assets/wallet-icon-gaze-left.svg'; +import GazeRight from '../assets/wallet-icon-gaze-right.svg'; +import GazeDown from '../assets/wallet-icon-gaze-down.svg'; +import SquircleEye from '../assets/wallet-icon-squircle-eye.svg'; +import SealEye from '../assets/wallet-icon-seal-eye.svg'; +import QuatrefoilEye from '../assets/wallet-icon-quatrefoil-eye.svg'; +import CatEye from '../assets/wallet-icon-cat-eye.svg'; +import Ring from '../assets/wallet-icon-ring.svg'; +import Target from '../assets/wallet-icon-target.svg'; +import Sun from '../assets/wallet-icon-sun.svg'; +import PinwheelSun from '../assets/wallet-icon-pinwheel-sun.svg'; +import Moon from '../assets/wallet-icon-moon.svg'; +import NorthStar from '../assets/wallet-icon-north-star.svg'; +import Orbit from '../assets/wallet-icon-orbit.svg'; +import Spiral from '../assets/wallet-icon-spiral.svg'; +import Fibonacci from '../assets/wallet-icon-fibonacci.svg'; +import SealFibonacci from '../assets/wallet-icon-seal-fibonacci.svg'; +import Snowflake from '../assets/wallet-icon-snowflake.svg'; +import Gem from '../assets/wallet-icon-gem.svg'; +import Heart from '../assets/wallet-icon-heart.svg'; +import Bolt from '../assets/wallet-icon-bolt.svg'; +import Keyhole from '../assets/wallet-icon-keyhole.svg'; +import NockN from '../assets/wallet-icon-nock-n.svg'; +import NockNRound from '../assets/wallet-icon-nock-n-round.svg'; + +export const WALLET_ICON_ASSETS: Record = { + 'style-1': Style1, + 'style-2': Style2, + 'style-3': Style3, + 'style-8': Style8, + 'style-14': Style14, + 'gaze-left': GazeLeft, + 'gaze-right': GazeRight, + 'gaze-down': GazeDown, + 'squircle-eye': SquircleEye, + 'seal-eye': SealEye, + 'quatrefoil-eye': QuatrefoilEye, + 'cat-eye': CatEye, + ring: Ring, + target: Target, + sun: Sun, + 'pinwheel-sun': PinwheelSun, + moon: Moon, + 'north-star': NorthStar, + orbit: Orbit, + spiral: Spiral, + fibonacci: Fibonacci, + 'seal-fibonacci': SealFibonacci, + snowflake: Snowflake, + gem: Gem, + heart: Heart, + bolt: Bolt, + keyhole: Keyhole, + 'nock-n': NockN, + 'nock-n-round': NockNRound, +}; diff --git a/extension/popup/hooks/useApprovalDetection.ts b/extension/popup/hooks/useApprovalDetection.ts index 9a54bd82..175d4e9b 100644 --- a/extension/popup/hooks/useApprovalDetection.ts +++ b/extension/popup/hooks/useApprovalDetection.ts @@ -1,20 +1,19 @@ /** - * useApprovalDetection - Detect and handle approval requests from URL hash + * useApprovalDetection - Detect and handle approval requests from URL hash or side panel messages * - * This hook monitors the URL hash for approval request parameters and automatically - * fetches the corresponding request data from the background script, then navigates - * to the appropriate approval screen when the wallet is unlocked. - * - * @param walletAddress - The current wallet address (null if not initialized) - * @param walletLocked - Whether the wallet is currently locked - * @param setPendingTransactionRequest - Function to set pending transaction request - * @param setPendingSignRequest - Function to set pending sign request - * @param navigate - Navigation function to switch screens + * This hook monitors the URL hash for approval request parameters (popup windows) and + * runtime messages / background queries (side panel), then navigates to the appropriate + * approval screen when the wallet is unlocked. */ -import { useEffect } from 'react'; +import { useEffect, useCallback, useRef } from 'react'; import { send } from '../utils/messaging'; -import { INTERNAL_METHODS, APPROVAL_CONSTANTS } from '../../shared/constants'; +import { + INTERNAL_METHODS, + APPROVAL_CONSTANTS, + RUNTIME_MESSAGE_TYPES, +} from '../../shared/constants'; +import type { ApprovalType } from '../../shared/constants'; import type { TransactionRequest, SignRequest, @@ -22,6 +21,7 @@ import type { SignRawTxRequest, } from '../../shared/types'; import type { Screen } from '../store'; +import { isSidePanel } from '../utils/displayContext'; interface UseApprovalDetectionProps { walletAddress: string | null; @@ -33,6 +33,19 @@ interface UseApprovalDetectionProps { navigate: (screen: Screen) => void; } +function getApprovalScreen(type: ApprovalType): Screen { + switch (type) { + case 'connect': + return 'connect-approval'; + case 'transaction': + return 'approve-transaction'; + case 'sign-message': + return 'sign-message'; + case 'sign-raw-tx': + return 'approve-sign-raw-tx'; + } +} + export function useApprovalDetection({ walletAddress, walletLocked, @@ -42,87 +55,168 @@ export function useApprovalDetection({ setPendingSignRawTxRequest, navigate, }: UseApprovalDetectionProps) { + const walletReady = walletAddress !== null; + const pendingSidePanelApproval = useRef<{ requestId: string; type: ApprovalType } | null>(null); + const walletReadyRef = useRef(walletReady); + walletReadyRef.current = walletReady; + + const handleApproval = useCallback( + async (requestId: string, type: ApprovalType) => { + const targetScreen = getApprovalScreen(type); + const lockedScreen: Screen = 'locked'; + + if (type === 'connect') { + const request = await send(INTERNAL_METHODS.GET_PENDING_CONNECTION, [ + requestId, + ]); + if (request && !('error' in request)) { + setPendingConnectRequest(request); + navigate(walletLocked ? lockedScreen : targetScreen); + } + return; + } + + if (type === 'transaction') { + const request = await send(INTERNAL_METHODS.GET_PENDING_TRANSACTION, [ + requestId, + ]); + if (request && !('error' in request)) { + setPendingTransactionRequest(request); + navigate(walletLocked ? lockedScreen : targetScreen); + } + return; + } + + if (type === 'sign-message') { + const request = await send(INTERNAL_METHODS.GET_PENDING_SIGN_REQUEST, [ + requestId, + ]); + if (request && !('error' in request)) { + setPendingSignRequest(request); + navigate(walletLocked ? lockedScreen : targetScreen); + } + return; + } + + const request = await send( + INTERNAL_METHODS.GET_PENDING_SIGN_RAW_TX_REQUEST, + [requestId] + ); + if (request && !('error' in request)) { + setPendingSignRawTxRequest(request); + navigate(walletLocked ? lockedScreen : targetScreen); + } + }, + [ + walletLocked, + setPendingConnectRequest, + setPendingTransactionRequest, + setPendingSignRequest, + setPendingSignRawTxRequest, + navigate, + ] + ); + + const handleApprovalRef = useRef(handleApproval); + handleApprovalRef.current = handleApproval; + + // Side panel: listen for approval messages immediately (may arrive before wallet init) useEffect(() => { - // Wait for wallet state to be initialized - if (walletAddress === null) return; + if (!isSidePanel()) return; - const hash = window.location.hash.slice(1); // Remove '#' + const handleRuntimeMessage = (message: { + type?: string; + requestId?: string; + approvalType?: ApprovalType; + }) => { + if ( + message?.type === RUNTIME_MESSAGE_TYPES.APPROVAL_PENDING && + message.requestId && + message.approvalType + ) { + if (walletReadyRef.current) { + void handleApprovalRef + .current(message.requestId, message.approvalType) + .catch(console.error); + } else { + pendingSidePanelApproval.current = { + requestId: message.requestId, + type: message.approvalType, + }; + } + } + }; - if (hash.startsWith(APPROVAL_CONSTANTS.CONNECT_HASH_PREFIX)) { - const requestId = hash.replace(APPROVAL_CONSTANTS.CONNECT_HASH_PREFIX, ''); + chrome.runtime.onMessage.addListener(handleRuntimeMessage); + return () => chrome.runtime.onMessage.removeListener(handleRuntimeMessage); + }, []); - // Fetch pending connect request from background - send(INTERNAL_METHODS.GET_PENDING_CONNECTION, [requestId]) - .then(request => { - if (request && !('error' in request)) { - setPendingConnectRequest(request); - // Navigate to approval screen if unlocked, or locked screen if locked - if (!walletLocked) { - navigate('connect-approval'); - } else { - navigate('locked'); - } + // Side panel: process queued or pending approvals once wallet is ready + useEffect(() => { + if (!isSidePanel() || !walletReady) return; + + const fetchPendingApproval = () => { + send<{ requestId: string; approvalType: ApprovalType } | null>( + INTERNAL_METHODS.GET_PENDING_APPROVAL + ) + .then(pending => { + if (pending?.requestId && pending.approvalType) { + void handleApproval(pending.requestId, pending.approvalType).catch(console.error); } }) .catch(console.error); - } else if (hash.startsWith(APPROVAL_CONSTANTS.TRANSACTION_HASH_PREFIX)) { - const requestId = hash.replace(APPROVAL_CONSTANTS.TRANSACTION_HASH_PREFIX, ''); + }; + + const queued = pendingSidePanelApproval.current; + if (queued) { + pendingSidePanelApproval.current = null; + void handleApproval(queued.requestId, queued.type).catch(console.error); + return; + } - // Fetch pending transaction request from background - send(INTERNAL_METHODS.GET_PENDING_TRANSACTION, [requestId]) - .then(request => { - if (request && !('error' in request)) { - setPendingTransactionRequest(request); - // Navigate to approval screen if unlocked, or locked screen if locked - if (!walletLocked) { - navigate('approve-transaction'); - } else { - navigate('locked'); - } + fetchPendingApproval(); + }, [walletReady, handleApproval]); + + // Side panel: re-check when panel becomes visible (e.g. reopened or refocused) + useEffect(() => { + if (!isSidePanel() || !walletReady) return; + + const handleVisibilityChange = () => { + if (document.visibilityState !== 'visible') return; + + send<{ requestId: string; approvalType: ApprovalType } | null>( + INTERNAL_METHODS.GET_PENDING_APPROVAL + ) + .then(pending => { + if (pending?.requestId && pending.approvalType) { + void handleApproval(pending.requestId, pending.approvalType).catch(console.error); } }) .catch(console.error); + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => document.removeEventListener('visibilitychange', handleVisibilityChange); + }, [walletReady, handleApproval]); + + // Popup windows: route via URL hash + useEffect(() => { + if (!walletReady || isSidePanel()) return; + + const hash = window.location.hash.slice(1); // Remove '#' + + if (hash.startsWith(APPROVAL_CONSTANTS.CONNECT_HASH_PREFIX)) { + const requestId = hash.replace(APPROVAL_CONSTANTS.CONNECT_HASH_PREFIX, ''); + void handleApproval(requestId, 'connect').catch(console.error); + } else if (hash.startsWith(APPROVAL_CONSTANTS.TRANSACTION_HASH_PREFIX)) { + const requestId = hash.replace(APPROVAL_CONSTANTS.TRANSACTION_HASH_PREFIX, ''); + void handleApproval(requestId, 'transaction').catch(console.error); } else if (hash.startsWith(APPROVAL_CONSTANTS.SIGN_MESSAGE_HASH_PREFIX)) { const requestId = hash.replace(APPROVAL_CONSTANTS.SIGN_MESSAGE_HASH_PREFIX, ''); - - // Fetch pending sign request from background - send(INTERNAL_METHODS.GET_PENDING_SIGN_REQUEST, [requestId]) - .then(request => { - if (request && !('error' in request)) { - setPendingSignRequest(request); - // Navigate to approval screen if unlocked, or locked screen if locked - if (!walletLocked) { - navigate('sign-message'); - } else { - navigate('locked'); - } - } - }) - .catch(console.error); + void handleApproval(requestId, 'sign-message').catch(console.error); } else if (hash.startsWith(APPROVAL_CONSTANTS.SIGN_RAW_TX_HASH_PREFIX)) { const requestId = hash.replace(APPROVAL_CONSTANTS.SIGN_RAW_TX_HASH_PREFIX, ''); - - // Fetch pending sign raw tx request from background - send(INTERNAL_METHODS.GET_PENDING_SIGN_RAW_TX_REQUEST, [requestId]) - .then(request => { - if (request && !('error' in request)) { - setPendingSignRawTxRequest(request); - // Navigate to approval screen if unlocked, or locked screen if locked - if (!walletLocked) { - navigate('approve-sign-raw-tx'); - } else { - navigate('locked'); - } - } - }) - .catch(console.error); + void handleApproval(requestId, 'sign-raw-tx').catch(console.error); } - }, [ - walletAddress, - walletLocked, - setPendingConnectRequest, - setPendingTransactionRequest, - setPendingSignRequest, - navigate, - ]); + }, [walletReady, handleApproval]); } diff --git a/extension/popup/screens/AboutScreen.tsx b/extension/popup/screens/AboutScreen.tsx index b64146ad..72608297 100644 --- a/extension/popup/screens/AboutScreen.tsx +++ b/extension/popup/screens/AboutScreen.tsx @@ -31,7 +31,7 @@ export function AboutScreen() { return (
{/* Header */} @@ -57,7 +57,7 @@ export function AboutScreen() { {/* Content */} -
+
Iris
diff --git a/extension/popup/screens/AddWalletStartScreen.tsx b/extension/popup/screens/AddWalletStartScreen.tsx index cbb0f77c..af149fd8 100644 --- a/extension/popup/screens/AddWalletStartScreen.tsx +++ b/extension/popup/screens/AddWalletStartScreen.tsx @@ -10,7 +10,7 @@ export function AddWalletStartScreen() { const { navigate } = useStore(); return ( -
+
-
+
diff --git a/extension/popup/screens/DisplayModeScreen.tsx b/extension/popup/screens/DisplayModeScreen.tsx new file mode 100644 index 00000000..264d5c50 --- /dev/null +++ b/extension/popup/screens/DisplayModeScreen.tsx @@ -0,0 +1,192 @@ +import { useEffect, useState } from 'react'; +import { useStore } from '../store'; +import { send } from '../utils/messaging'; +import { INTERNAL_METHODS, DISPLAY_MODES } from '../../shared/constants'; +import type { DisplayMode } from '../../shared/constants'; +import { ChevronLeftIcon } from '../components/icons/ChevronLeftIcon'; +import { isSidePanel, isSidePanelSupported } from '../utils/displayContext'; +import { SIDE_PANEL_DEFAULT_PATH } from '../../shared/side-panel'; + +export function DisplayModeScreen() { + const { navigate } = useStore(); + const [mode, setMode] = useState(DISPLAY_MODES.POPUP); + const [isSaving, setIsSaving] = useState(false); + const [showPopupHint, setShowPopupHint] = useState(false); + + useEffect(() => { + if (!isSidePanelSupported()) { + navigate('settings'); + } + }, [navigate]); + + useEffect(() => { + send<{ mode: DisplayMode }>(INTERNAL_METHODS.GET_DISPLAY_MODE) + .then(response => { + if (response?.mode) { + setMode(response.mode); + } + }) + .catch(console.error); + }, []); + + function handleBack() { + navigate('settings'); + } + + async function handleModeSelect(nextMode: DisplayMode) { + if (nextMode === mode || isSaving) return; + + setIsSaving(true); + setShowPopupHint(false); + + try { + const response = await send<{ success?: boolean; mode?: DisplayMode }>( + INTERNAL_METHODS.SET_DISPLAY_MODE, + [nextMode] + ); + + const resolvedMode = response?.mode ?? nextMode; + setMode(resolvedMode); + + if (resolvedMode === DISPLAY_MODES.SIDE_PANEL && !isSidePanel()) { + try { + const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); + if (activeTab?.id !== undefined) { + await chrome.sidePanel.setOptions({ + tabId: activeTab.id, + path: SIDE_PANEL_DEFAULT_PATH, + enabled: true, + }); + await chrome.sidePanel.open({ tabId: activeTab.id }); + } else { + const currentWindow = await chrome.windows.getCurrent(); + if (currentWindow.id !== undefined) { + await chrome.sidePanel.open({ windowId: currentWindow.id }); + } + } + window.close(); + } catch (error) { + console.error('[DisplayMode] Failed to open side panel:', error); + } + return; + } + + if (resolvedMode === DISPLAY_MODES.POPUP && isSidePanel()) { + setShowPopupHint(true); + } + } catch (error) { + console.error('[DisplayMode] Failed to update display mode:', error); + } finally { + setIsSaving(false); + } + } + + const Option = ({ + label, + description, + selected, + onClick, + disabled = false, + }: { + label: string; + description: string; + selected?: boolean; + onClick?: () => void; + disabled?: boolean; + }) => ( + + ); + + if (!isSidePanelSupported()) { + return null; + } + + return ( +
+
+ +

Display mode

+
+
+ + {showPopupHint && ( +
+ Pop-up mode is active. Click the Iris icon in the toolbar to open the wallet in a pop-up + window. +
+ )} + +
+
+
+ ); +} diff --git a/extension/popup/screens/HomeScreen.tsx b/extension/popup/screens/HomeScreen.tsx index c27eb124..906104dd 100644 --- a/extension/popup/screens/HomeScreen.tsx +++ b/extension/popup/screens/HomeScreen.tsx @@ -435,7 +435,7 @@ export function HomeScreen() { return (
diff --git a/extension/popup/screens/KeySettingsPasswordScreen.tsx b/extension/popup/screens/KeySettingsPasswordScreen.tsx index ffe264f3..9f26b9e2 100644 --- a/extension/popup/screens/KeySettingsPasswordScreen.tsx +++ b/extension/popup/screens/KeySettingsPasswordScreen.tsx @@ -55,7 +55,7 @@ export function KeySettingsPasswordScreen() { return (
{/* Header */} diff --git a/extension/popup/screens/LockTimeScreen.tsx b/extension/popup/screens/LockTimeScreen.tsx index 52d7bfbe..c8d937a9 100644 --- a/extension/popup/screens/LockTimeScreen.tsx +++ b/extension/popup/screens/LockTimeScreen.tsx @@ -85,7 +85,7 @@ export function LockTimeScreen() { return (
{/* Header */} diff --git a/extension/popup/screens/ReceiveScreen.tsx b/extension/popup/screens/ReceiveScreen.tsx index ca202018..000e525c 100644 --- a/extension/popup/screens/ReceiveScreen.tsx +++ b/extension/popup/screens/ReceiveScreen.tsx @@ -29,7 +29,7 @@ export function ReceiveScreen() { return (
{/* Header */} @@ -53,7 +53,10 @@ export function ReceiveScreen() { {/* Content */} -
+
{/* Intro */}
diff --git a/extension/popup/screens/RecoveryPhraseScreen.tsx b/extension/popup/screens/RecoveryPhraseScreen.tsx index 9b069172..7055c5ea 100644 --- a/extension/popup/screens/RecoveryPhraseScreen.tsx +++ b/extension/popup/screens/RecoveryPhraseScreen.tsx @@ -52,7 +52,7 @@ export function RecoveryPhraseScreen() { if (!isRevealed) { return (
{/* Header */} @@ -133,7 +133,7 @@ export function RecoveryPhraseScreen() { // Secret phrase display view return (
{/* Header */} diff --git a/extension/popup/screens/RpcSettingsScreen.tsx b/extension/popup/screens/RpcSettingsScreen.tsx index 9b6aefa2..9c06c70e 100644 --- a/extension/popup/screens/RpcSettingsScreen.tsx +++ b/extension/popup/screens/RpcSettingsScreen.tsx @@ -163,7 +163,7 @@ export function RpcSettingsScreen() { return (
{/* Header */} diff --git a/extension/popup/screens/SendReviewScreen.tsx b/extension/popup/screens/SendReviewScreen.tsx index cfd4d9c1..cd07f3e8 100644 --- a/extension/popup/screens/SendReviewScreen.tsx +++ b/extension/popup/screens/SendReviewScreen.tsx @@ -103,7 +103,7 @@ export function SendReviewScreen() { return (
{/* Header */} @@ -130,7 +130,7 @@ export function SendReviewScreen() { {/* Content */}
diff --git a/extension/popup/screens/SendScreen.tsx b/extension/popup/screens/SendScreen.tsx index 04736c12..63f0bd6f 100644 --- a/extension/popup/screens/SendScreen.tsx +++ b/extension/popup/screens/SendScreen.tsx @@ -453,7 +453,7 @@ export function SendScreen() { return (
{/* Header */} diff --git a/extension/popup/screens/SendSubmittedScreen.tsx b/extension/popup/screens/SendSubmittedScreen.tsx index 3c3b88c9..7bc8d880 100644 --- a/extension/popup/screens/SendSubmittedScreen.tsx +++ b/extension/popup/screens/SendSubmittedScreen.tsx @@ -104,10 +104,7 @@ export function SendSubmittedScreen() { } return ( -
+
{/* Header */}
{/* Content */} -
+
{/* Success Section */}
diff --git a/extension/popup/screens/SettingsScreen.tsx b/extension/popup/screens/SettingsScreen.tsx index e15ab17e..8e7179b2 100644 --- a/extension/popup/screens/SettingsScreen.tsx +++ b/extension/popup/screens/SettingsScreen.tsx @@ -1,6 +1,7 @@ import { useStore } from '../store'; import IrisLogo from '../assets/iris-logo.svg'; import ThemeIcon from '../assets/theme-icon.svg'; +import DisplayModeIcon from '../assets/display-mode-icon.svg'; import RpcSettingsIcon from '../assets/rpc-settings-icon.svg'; import KeyIcon from '../assets/key-icon.svg'; import ClockIcon from '../assets/clock-icon.svg'; @@ -9,6 +10,7 @@ import { CloseIcon } from '../components/icons/CloseIcon'; import { ChevronRightIcon } from '../components/icons/ChevronRightIcon'; import AboutIcon from '../assets/settings-gear-icon.svg'; import { version } from '../../../package-lock.json'; +import { isSidePanelSupported } from '../utils/displayContext'; export function SettingsScreen() { const { navigate } = useStore(); @@ -19,6 +21,9 @@ export function SettingsScreen() { function handleThemeSettings() { navigate('theme-settings'); } + function handleDisplayMode() { + navigate('display-mode'); + } function handleKeySettings() { navigate('key-settings'); } @@ -65,7 +70,7 @@ export function SettingsScreen() { return (
{/* Header */} @@ -91,10 +96,13 @@ export function SettingsScreen() {
{/* Content */} -
+
{/* Menu */}
+ {isSidePanelSupported() && ( + + )} diff --git a/extension/popup/screens/SwapReviewScreen.tsx b/extension/popup/screens/SwapReviewScreen.tsx index 7e35205f..a2dbd32c 100644 --- a/extension/popup/screens/SwapReviewScreen.tsx +++ b/extension/popup/screens/SwapReviewScreen.tsx @@ -128,7 +128,7 @@ export function SwapReviewScreen() { return (
{/* Header - matches Figma */} diff --git a/extension/popup/screens/ThemeSettingsScreen.tsx b/extension/popup/screens/ThemeSettingsScreen.tsx index 175886e9..bd505736 100644 --- a/extension/popup/screens/ThemeSettingsScreen.tsx +++ b/extension/popup/screens/ThemeSettingsScreen.tsx @@ -60,7 +60,7 @@ export function ThemeSettingsScreen() { return (
{/* Header */} diff --git a/extension/popup/screens/TransactionDetailsScreen.tsx b/extension/popup/screens/TransactionDetailsScreen.tsx index 8fc90b1c..f9499a03 100644 --- a/extension/popup/screens/TransactionDetailsScreen.tsx +++ b/extension/popup/screens/TransactionDetailsScreen.tsx @@ -52,7 +52,7 @@ export function TransactionDetailsScreen() { if (!selectedTransaction) { return (
@@ -227,7 +227,7 @@ export function TransactionDetailsScreen() { return (
{/* Header */} @@ -262,7 +262,7 @@ export function TransactionDetailsScreen() { {/* Content */}
diff --git a/extension/popup/screens/V0MigrationFundsScreen.tsx b/extension/popup/screens/V0MigrationFundsScreen.tsx index 44de3f22..1ddc9f6b 100644 --- a/extension/popup/screens/V0MigrationFundsScreen.tsx +++ b/extension/popup/screens/V0MigrationFundsScreen.tsx @@ -117,7 +117,7 @@ export function V0MigrationFundsScreen() { return (
diff --git a/extension/popup/screens/V0MigrationIntroScreen.tsx b/extension/popup/screens/V0MigrationIntroScreen.tsx index 6abf107d..f0e8ec60 100644 --- a/extension/popup/screens/V0MigrationIntroScreen.tsx +++ b/extension/popup/screens/V0MigrationIntroScreen.tsx @@ -16,7 +16,7 @@ export function V0MigrationIntroScreen() { return (
{/* Header - matches other migration screens */} diff --git a/extension/popup/screens/V0MigrationReviewScreen.tsx b/extension/popup/screens/V0MigrationReviewScreen.tsx index 676759dc..f9aa19ba 100644 --- a/extension/popup/screens/V0MigrationReviewScreen.tsx +++ b/extension/popup/screens/V0MigrationReviewScreen.tsx @@ -152,7 +152,7 @@ export function V0MigrationReviewScreen() { return (
diff --git a/extension/popup/screens/V0MigrationSetupScreen.tsx b/extension/popup/screens/V0MigrationSetupScreen.tsx index d0671997..98ee4418 100644 --- a/extension/popup/screens/V0MigrationSetupScreen.tsx +++ b/extension/popup/screens/V0MigrationSetupScreen.tsx @@ -111,7 +111,7 @@ export function V0MigrationSetupScreen() { } return ( -
+
{/* Header - same as onboarding ImportScreen */}
-
+
{/* Icon and instructions - same as ImportScreen */} diff --git a/extension/popup/screens/ViewSecretPhraseScreen.tsx b/extension/popup/screens/ViewSecretPhraseScreen.tsx index 611097cd..9a534f82 100644 --- a/extension/popup/screens/ViewSecretPhraseScreen.tsx +++ b/extension/popup/screens/ViewSecretPhraseScreen.tsx @@ -65,7 +65,7 @@ export function ViewSecretPhraseScreen() { return (
{/* Header */} diff --git a/extension/popup/screens/WalletPermissionsScreen.tsx b/extension/popup/screens/WalletPermissionsScreen.tsx index 31723a73..334ba6b4 100644 --- a/extension/popup/screens/WalletPermissionsScreen.tsx +++ b/extension/popup/screens/WalletPermissionsScreen.tsx @@ -45,7 +45,7 @@ export function WalletPermissionsScreen() { return (
{/* Header */} diff --git a/extension/popup/screens/WalletSettingsScreen.tsx b/extension/popup/screens/WalletSettingsScreen.tsx index 050cb626..c4ac7015 100644 --- a/extension/popup/screens/WalletSettingsScreen.tsx +++ b/extension/popup/screens/WalletSettingsScreen.tsx @@ -149,7 +149,7 @@ export function WalletSettingsScreen() { return (
{/* Header */} @@ -182,7 +182,7 @@ export function WalletSettingsScreen() { {/* Content */}
diff --git a/extension/popup/screens/WalletStylingScreen.tsx b/extension/popup/screens/WalletStylingScreen.tsx index 9d138171..7d1d76e5 100644 --- a/extension/popup/screens/WalletStylingScreen.tsx +++ b/extension/popup/screens/WalletStylingScreen.tsx @@ -1,27 +1,17 @@ import { useState, useEffect, useRef } from 'react'; import { useStore } from '../store'; import { send } from '../utils/messaging'; -import { INTERNAL_METHODS, ACCOUNT_COLORS } from '../../shared/constants'; +import { INTERNAL_METHODS } from '../../shared/constants'; +import { + ACCOUNT_COLORS, + DEFAULT_WALLET_STYLE, + WALLET_ICONS, + normalizeIconStyleId, +} from '../../shared/walletStyles'; +import { WALLET_ICON_ASSETS } from '../components/walletIconAssets'; import { ChevronLeftIcon } from '../components/icons/ChevronLeftIcon'; import { ChevronRightIcon } from '../components/icons/ChevronRightIcon'; -// Icons -import WalletStyle1 from '../assets/wallet-icon-style-1.svg'; -import WalletStyle2 from '../assets/wallet-icon-style-2.svg'; -import WalletStyle3 from '../assets/wallet-icon-style-3.svg'; -import WalletStyle4 from '../assets/wallet-icon-style-4.svg'; -import WalletStyle5 from '../assets/wallet-icon-style-5.svg'; -import WalletStyle6 from '../assets/wallet-icon-style-6.svg'; -import WalletStyle7 from '../assets/wallet-icon-style-7.svg'; -import WalletStyle8 from '../assets/wallet-icon-style-8.svg'; -import WalletStyle9 from '../assets/wallet-icon-style-9.svg'; -import WalletStyle10 from '../assets/wallet-icon-style-10.svg'; -import WalletStyle11 from '../assets/wallet-icon-style-11.svg'; -import WalletStyle12 from '../assets/wallet-icon-style-12.svg'; -import WalletStyle13 from '../assets/wallet-icon-style-13.svg'; -import WalletStyle14 from '../assets/wallet-icon-style-14.svg'; -import WalletStyle15 from '../assets/wallet-icon-style-15.svg'; - export function WalletStylingScreen() { const { navigate, wallet, refreshWalletAccounts, settingsAccountAddress } = useStore(); @@ -35,40 +25,27 @@ export function WalletStylingScreen() { wallet.accounts[0]; // Load initial values from current account or use defaults - const [selectedStyle, setSelectedStyle] = useState(currentAccount?.iconStyleId || 1); - const [selectedColor, setSelectedColor] = useState(currentAccount?.iconColor || '#FFC413'); + const [selectedStyle, setSelectedStyle] = useState( + normalizeIconStyleId(currentAccount?.iconStyleId) + ); + const [selectedColor, setSelectedColor] = useState( + currentAccount?.iconColor || DEFAULT_WALLET_STYLE.iconColor + ); const [svgContent, setSvgContent] = useState(''); // Track if we're scrolled to the end (false = at start, true = at end) const [isScrolledRight, setIsScrolledRight] = useState(false); const colorScrollRef = useRef(null); - const iconStyles = [ - { id: 1, icon: WalletStyle1 }, - { id: 2, icon: WalletStyle2 }, - { id: 3, icon: WalletStyle3 }, - { id: 4, icon: WalletStyle4 }, - { id: 5, icon: WalletStyle5 }, - { id: 6, icon: WalletStyle6 }, - { id: 7, icon: WalletStyle7 }, - { id: 8, icon: WalletStyle8 }, - { id: 9, icon: WalletStyle9 }, - { id: 10, icon: WalletStyle10 }, - { id: 11, icon: WalletStyle11 }, - { id: 12, icon: WalletStyle12 }, - { id: 13, icon: WalletStyle13 }, - { id: 14, icon: WalletStyle14 }, - { id: 15, icon: WalletStyle15 }, - ]; - - // Use shared color constants + // Shared icon registry (picker order: similar styles grouped) and color palette + const iconStyles = WALLET_ICONS.map(icon => ({ id: icon.id, icon: WALLET_ICON_ASSETS[icon.id] })); const colors = ACCOUNT_COLORS; // Sync state when current account changes useEffect(() => { if (currentAccount) { - setSelectedStyle(currentAccount.iconStyleId || 1); - const color = currentAccount.iconColor || '#FFC413'; + setSelectedStyle(normalizeIconStyleId(currentAccount.iconStyleId)); + const color = currentAccount.iconColor || DEFAULT_WALLET_STYLE.iconColor; setSelectedColor(color); } }, [currentAccount?.address, currentAccount?.iconStyleId, currentAccount?.iconColor]); @@ -89,7 +66,7 @@ export function WalletStylingScreen() { }, [selectedStyle, selectedColor]); // Persist styling changes - async function handleStyleChange(styleId: number) { + async function handleStyleChange(styleId: string) { if (!currentAccount) return; setSelectedStyle(styleId); @@ -149,7 +126,7 @@ export function WalletStylingScreen() { return (
{/* Header */} @@ -173,7 +150,7 @@ export function WalletStylingScreen() {
{/* Content */} -
+
{/* Preview */}
diff --git a/extension/popup/screens/approvals/ConnectApprovalScreen.tsx b/extension/popup/screens/approvals/ConnectApprovalScreen.tsx index a4e9a3ab..999f54b6 100644 --- a/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +++ b/extension/popup/screens/approvals/ConnectApprovalScreen.tsx @@ -5,6 +5,7 @@ import { truncateAddress } from '../../utils/format'; import { send } from '../../utils/messaging'; import { INTERNAL_METHODS } from '../../../shared/constants'; import { useAutoRejectOnClose } from '../../hooks/useAutoRejectOnClose'; +import { closeAfterApproval } from '../../utils/displayContext'; export function ConnectApprovalScreen() { const { navigate, pendingConnectRequest, setPendingConnectRequest, wallet } = useStore(); @@ -22,13 +23,13 @@ export function ConnectApprovalScreen() { async function handleReject() { await send(INTERNAL_METHODS.REJECT_CONNECTION, [id]); setPendingConnectRequest(null); - window.close(); + closeAfterApproval(navigate); } async function handleConnect() { await send(INTERNAL_METHODS.APPROVE_CONNECTION, [id]); setPendingConnectRequest(null); - window.close(); + closeAfterApproval(navigate); } const bg = 'var(--color-bg)'; @@ -40,10 +41,7 @@ export function ConnectApprovalScreen() { return (
-
+
{/* Header */}

diff --git a/extension/popup/screens/approvals/SignMessageScreen.tsx b/extension/popup/screens/approvals/SignMessageScreen.tsx index ce69fa7a..92e69b2f 100644 --- a/extension/popup/screens/approvals/SignMessageScreen.tsx +++ b/extension/popup/screens/approvals/SignMessageScreen.tsx @@ -5,6 +5,7 @@ import { truncateAddress } from '../../utils/format'; import { send } from '../../utils/messaging'; import { INTERNAL_METHODS } from '../../../shared/constants'; import { useAutoRejectOnClose } from '../../hooks/useAutoRejectOnClose'; +import { closeAfterApproval } from '../../utils/displayContext'; export function SignMessageScreen() { const { navigate, pendingSignRequest, setPendingSignRequest, wallet } = useStore(); @@ -21,13 +22,13 @@ export function SignMessageScreen() { async function handleDecline() { await send(INTERNAL_METHODS.REJECT_SIGN_MESSAGE, [id]); setPendingSignRequest(null); - window.close(); + closeAfterApproval(navigate); } async function handleSign() { await send(INTERNAL_METHODS.APPROVE_SIGN_MESSAGE, [id]); setPendingSignRequest(null); - window.close(); + closeAfterApproval(navigate); } const bg = 'var(--color-bg)'; @@ -38,10 +39,7 @@ export function SignMessageScreen() { return (
-
+
{/* Header */}

diff --git a/extension/popup/screens/approvals/SignRawTxScreen.tsx b/extension/popup/screens/approvals/SignRawTxScreen.tsx index db7a7818..de18945e 100644 --- a/extension/popup/screens/approvals/SignRawTxScreen.tsx +++ b/extension/popup/screens/approvals/SignRawTxScreen.tsx @@ -4,6 +4,7 @@ import { INTERNAL_METHODS, APPROVAL_CONSTANTS } from '../../../shared/constants' import { send } from '../../utils/messaging'; import { SignRawTxRequest } from '../../../shared/types'; import { useAutoRejectOnClose } from '../../hooks/useAutoRejectOnClose'; +import { closeAfterApproval } from '../../utils/displayContext'; import { AccountIcon } from '../../components/AccountIcon'; import { SiteIcon } from '../../components/SiteIcon'; import { truncateAddress } from '../../utils/format'; @@ -97,13 +98,13 @@ export function SignRawTxScreen() { async function handleDecline() { await send(INTERNAL_METHODS.REJECT_SIGN_RAW_TX, [id]); setPendingSignRawTxRequest(null); - window.close(); + closeAfterApproval(navigate); } async function handleSign() { await send(INTERNAL_METHODS.APPROVE_SIGN_RAW_TX, [id]); setPendingSignRawTxRequest(null); - window.close(); + closeAfterApproval(navigate); } // Network fee from native rawTx. RawTxV1.spends is a ZMap @@ -137,10 +138,7 @@ export function SignRawTxScreen() { return (
-
+
{/* Header */}

diff --git a/extension/popup/screens/approvals/TransactionApprovalScreen.tsx b/extension/popup/screens/approvals/TransactionApprovalScreen.tsx index 753211e7..fc08ba5a 100644 --- a/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +++ b/extension/popup/screens/approvals/TransactionApprovalScreen.tsx @@ -7,6 +7,7 @@ import { send } from '../../utils/messaging'; import { INTERNAL_METHODS, NOCK_TO_NICKS } from '../../../shared/constants'; import { formatNock, formatNick } from '../../../shared/currency'; import { useAutoRejectOnClose } from '../../hooks/useAutoRejectOnClose'; +import { closeAfterApproval } from '../../utils/displayContext'; export function TransactionApprovalScreen() { const { navigate, pendingTransactionRequest, setPendingTransactionRequest, wallet } = useStore(); @@ -28,13 +29,13 @@ export function TransactionApprovalScreen() { async function handleReject() { await send(INTERNAL_METHODS.REJECT_TRANSACTION, [id]); setPendingTransactionRequest(null); - window.close(); + closeAfterApproval(navigate); } async function handleApprove() { await send(INTERNAL_METHODS.APPROVE_TRANSACTION, [id]); setPendingTransactionRequest(null); - window.close(); + closeAfterApproval(navigate); } const bg = 'var(--color-bg)'; @@ -45,10 +46,7 @@ export function TransactionApprovalScreen() { return (
-
+
{/* Header */}

diff --git a/extension/popup/screens/onboarding/BackupScreen.tsx b/extension/popup/screens/onboarding/BackupScreen.tsx index f64c351a..61de11d7 100644 --- a/extension/popup/screens/onboarding/BackupScreen.tsx +++ b/extension/popup/screens/onboarding/BackupScreen.tsx @@ -30,7 +30,7 @@ export function BackupScreen() { if (!onboardingMnemonic) { // Should never happen, but handle gracefully return ( -
+
No mnemonic found. Please restart onboarding.