From 1b7602be391f9762b4d721c40101a5b127a52a9c Mon Sep 17 00:00:00 2001 From: thomasurbit <99678438+thomasurbit@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:03:18 +0200 Subject: [PATCH 1/8] feat: add popup / side panel display mode toggle Let users choose between the toolbar pop-up and Chrome side panel via Settings > Display mode. Side panel reuses the same React app with fluid layout; dApp approvals route to the panel with popup-window fallback. Co-authored-by: Cursor --- extension/background/index.ts | 131 +++++++++++- extension/manifest.json | 5 +- extension/popup/Router.tsx | 3 + extension/popup/assets/display-mode-icon.svg | 4 + .../popup/components/ScreenContainer.tsx | 2 +- extension/popup/hooks/useApprovalDetection.ts | 199 +++++++++++------- extension/popup/screens/AboutScreen.tsx | 4 +- .../popup/screens/AddWalletStartScreen.tsx | 4 +- extension/popup/screens/DisplayModeScreen.tsx | 179 ++++++++++++++++ extension/popup/screens/HomeScreen.tsx | 2 +- .../screens/KeySettingsPasswordScreen.tsx | 2 +- extension/popup/screens/LockTimeScreen.tsx | 2 +- extension/popup/screens/ReceiveScreen.tsx | 4 +- .../popup/screens/RecoveryPhraseScreen.tsx | 4 +- extension/popup/screens/RpcSettingsScreen.tsx | 2 +- extension/popup/screens/SendReviewScreen.tsx | 4 +- extension/popup/screens/SendScreen.tsx | 2 +- .../popup/screens/SendSubmittedScreen.tsx | 4 +- extension/popup/screens/SettingsScreen.tsx | 12 +- extension/popup/screens/SwapReviewScreen.tsx | 2 +- extension/popup/screens/SwapScreen.tsx | 2 +- .../popup/screens/ThemeSettingsScreen.tsx | 2 +- .../screens/TransactionDetailsScreen.tsx | 6 +- .../popup/screens/V0MigrationFundsScreen.tsx | 2 +- .../popup/screens/V0MigrationIntroScreen.tsx | 2 +- .../popup/screens/V0MigrationReviewScreen.tsx | 2 +- .../popup/screens/V0MigrationSetupScreen.tsx | 4 +- .../popup/screens/ViewSecretPhraseScreen.tsx | 2 +- .../popup/screens/WalletPermissionsScreen.tsx | 2 +- .../popup/screens/WalletSettingsScreen.tsx | 4 +- .../popup/screens/WalletStylingScreen.tsx | 4 +- .../approvals/ConnectApprovalScreen.tsx | 7 +- .../screens/approvals/SignMessageScreen.tsx | 7 +- .../screens/approvals/SignRawTxScreen.tsx | 7 +- .../approvals/TransactionApprovalScreen.tsx | 7 +- .../popup/screens/onboarding/BackupScreen.tsx | 4 +- .../popup/screens/onboarding/CreateScreen.tsx | 4 +- .../popup/screens/onboarding/ImportScreen.tsx | 8 +- .../onboarding/ImportSuccessScreen.tsx | 4 +- .../screens/onboarding/ResumeBackupScreen.tsx | 4 +- .../popup/screens/onboarding/StartScreen.tsx | 4 +- .../screens/onboarding/SuccessScreen.tsx | 4 +- .../popup/screens/onboarding/VerifyScreen.tsx | 6 +- .../popup/screens/system/LockedScreen.tsx | 2 +- extension/popup/store.ts | 1 + extension/popup/styles.css | 20 ++ extension/popup/utils/displayContext.ts | 20 ++ extension/shared/constants.ts | 28 +++ extension/sidepanel/index.html | 13 ++ extension/sidepanel/index.tsx | 19 ++ 50 files changed, 615 insertions(+), 157 deletions(-) create mode 100644 extension/popup/assets/display-mode-icon.svg create mode 100644 extension/popup/screens/DisplayModeScreen.tsx create mode 100644 extension/popup/utils/displayContext.ts create mode 100644 extension/sidepanel/index.html create mode 100644 extension/sidepanel/index.tsx diff --git a/extension/background/index.ts b/extension/background/index.ts index 7ecf01e2..844aea88 100644 --- a/extension/background/index.ts +++ b/extension/background/index.ts @@ -31,7 +31,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 { @@ -49,6 +52,7 @@ 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'; @@ -240,6 +244,7 @@ function cancelPendingRequest(requestId: string, code?: number, message?: string }); if (currentRequestId == requestId) { currentRequestId = null; + currentRequestType = null; } } @@ -421,6 +426,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(); @@ -461,6 +467,65 @@ 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; + + if (effectiveMode === DISPLAY_MODES.SIDE_PANEL) { + await chrome.action.setPopup({ popup: '' }); + await chrome.sidePanel!.setPanelBehavior({ openPanelOnActionClick: true }); + await chrome.sidePanel!.setOptions({ path: 'sidepanel/index.html', 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 false; + } + + try { + if (tabId !== undefined) { + await chrome.sidePanel.open({ tabId }); + } else { + const currentWindow = await chrome.windows.getLastFocused({ populate: false }); + if (currentWindow.id !== undefined) { + await chrome.sidePanel.open({ windowId: currentWindow.id }); + } + } + return true; + } catch (error) { + console.warn('[Background] sidePanel.open failed, falling back to popup window:', error); + return false; + } +} + async function createPopupWithFallback(opts: any): Promise { // First try: with left/top try { @@ -480,7 +545,8 @@ async function createPopupWithFallback(opts: any): Promise { */ async function createApprovalPopup( requestId: string, - type: 'connect' | 'transaction' | 'sign-message' | 'sign-raw-tx' + type: ApprovalType, + tabId?: number ) { // If user is currently viewing a different request, queue this one if (currentRequestId !== null && currentRequestId !== requestId) { @@ -494,6 +560,16 @@ async function createApprovalPopup( // Mark this request as currently displayed currentRequestId = requestId; + currentRequestType = type; + + const displayMode = await getDisplayMode(); + if (displayMode === DISPLAY_MODES.SIDE_PANEL) { + const opened = await openSidePanelForApproval(tabId); + if (opened) { + await notifyApprovalPending(requestId, type); + return; + } + } let hashPrefix: string; if (type === 'connect') { @@ -530,7 +606,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 @@ -588,6 +664,7 @@ function processNextRequest() { if (currentRequestId !== null) { cancelPendingRequest(currentRequestId); } + currentRequestType = null; if (requestQueue.length > 0) { while (true) { @@ -595,7 +672,7 @@ function processNextRequest() { if (!pendingRequests.has(next.id)) { continue; } - createApprovalPopup(next.id, next.type); + createApprovalPopup(next.id, next.type, pendingRequests.get(next.id)?.tabId); break; } } @@ -687,6 +764,8 @@ const initPromise = (async () => { await vault.init(); // Load encrypted vault header to detect vault existence 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(); @@ -695,6 +774,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) { @@ -788,10 +871,11 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { void sendBridgedResponse(response); }, origin: connectRequest.origin, + tabId: _sender.tab?.id, }); // Create approval popup - await createApprovalPopup(connectRequestId, 'connect'); + await createApprovalPopup(connectRequestId, 'connect', _sender.tab?.id); // Response will be sent when user approves/rejects return; @@ -843,10 +927,11 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { void sendBridgedResponse(response); }, origin: signRequest.origin, + tabId: _sender.tab?.id, }); // 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; @@ -896,10 +981,11 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { void sendBridgedResponse(response); }, origin: signRawTxRequest.origin, + tabId: _sender.tab?.id, }); // 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; @@ -951,10 +1037,11 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { void sendBridgedResponse(response); }, origin: txRequest.origin, + tabId: _sender.tab?.id, }); // Create approval popup - await createApprovalPopup(txRequestId, 'transaction'); + await createApprovalPopup(txRequestId, 'transaction', _sender.tab?.id); // Response will be sent when user approves/rejects return; @@ -1002,6 +1089,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 6868d215..4439c48c 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..38e26515 --- /dev/null +++ b/extension/popup/assets/display-mode-icon.svg @@ -0,0 +1,4 @@ + + + + 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/hooks/useApprovalDetection.ts b/extension/popup/hooks/useApprovalDetection.ts index 9a54bd82..c55ab587 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 } 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,6 +55,63 @@ export function useApprovalDetection({ setPendingSignRawTxRequest, navigate, }: UseApprovalDetectionProps) { + 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, + ] + ); + useEffect(() => { // Wait for wallet state to be initialized if (walletAddress === null) return; @@ -50,79 +120,48 @@ export function useApprovalDetection({ if (hash.startsWith(APPROVAL_CONSTANTS.CONNECT_HASH_PREFIX)) { const requestId = hash.replace(APPROVAL_CONSTANTS.CONNECT_HASH_PREFIX, ''); - - // 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'); - } - } - }) - .catch(console.error); + 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, ''); - - // 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'); - } - } - }) - .catch(console.error); + 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, - ]); + }, [walletAddress, handleApproval]); + + useEffect(() => { + if (!isSidePanel() || walletAddress === null) return; + + const handleRuntimeMessage = (message: { + type?: string; + requestId?: string; + approvalType?: ApprovalType; + }) => { + if ( + message?.type === RUNTIME_MESSAGE_TYPES.APPROVAL_PENDING && + message.requestId && + message.approvalType + ) { + void handleApproval(message.requestId, message.approvalType).catch(console.error); + } + }; + + chrome.runtime.onMessage.addListener(handleRuntimeMessage); + + 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); + + return () => chrome.runtime.onMessage.removeListener(handleRuntimeMessage); + }, [walletAddress, 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..a4682035 --- /dev/null +++ b/extension/popup/screens/DisplayModeScreen.tsx @@ -0,0 +1,179 @@ +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'; + +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.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 c3a66c21..1203cb36 100644 --- a/extension/popup/screens/HomeScreen.tsx +++ b/extension/popup/screens/HomeScreen.tsx @@ -436,7 +436,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..b179981d 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,7 @@ 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 7f1617d0..bd041f87 100644 --- a/extension/popup/screens/SendReviewScreen.tsx +++ b/extension/popup/screens/SendReviewScreen.tsx @@ -100,7 +100,7 @@ export function SendReviewScreen() { return (
{/* Header */} @@ -127,7 +127,7 @@ export function SendReviewScreen() { {/* Content */}
diff --git a/extension/popup/screens/SendScreen.tsx b/extension/popup/screens/SendScreen.tsx index ae68b91d..11b5fade 100644 --- a/extension/popup/screens/SendScreen.tsx +++ b/extension/popup/screens/SendScreen.tsx @@ -421,7 +421,7 @@ export function SendScreen() { return (
{/* Header */} diff --git a/extension/popup/screens/SendSubmittedScreen.tsx b/extension/popup/screens/SendSubmittedScreen.tsx index 3c3b88c9..f33a7bbb 100644 --- a/extension/popup/screens/SendSubmittedScreen.tsx +++ b/extension/popup/screens/SendSubmittedScreen.tsx @@ -105,7 +105,7 @@ export function SendSubmittedScreen() { return (
{/* Header */} @@ -132,7 +132,7 @@ export function SendSubmittedScreen() { {/* 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 ef476cc2..75334897 100644 --- a/extension/popup/screens/SwapReviewScreen.tsx +++ b/extension/popup/screens/SwapReviewScreen.tsx @@ -125,7 +125,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 659fec11..5057a318 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 95e6b26d..c95ad974 100644 --- a/extension/popup/screens/V0MigrationSetupScreen.tsx +++ b/extension/popup/screens/V0MigrationSetupScreen.tsx @@ -150,7 +150,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..c3d54710 100644 --- a/extension/popup/screens/WalletStylingScreen.tsx +++ b/extension/popup/screens/WalletStylingScreen.tsx @@ -149,7 +149,7 @@ export function WalletStylingScreen() { return (
{/* Header */} @@ -173,7 +173,7 @@ export function WalletStylingScreen() {
{/* Content */} -
+
{/* Preview */}
diff --git a/extension/popup/screens/approvals/ConnectApprovalScreen.tsx b/extension/popup/screens/approvals/ConnectApprovalScreen.tsx index a4e9a3ab..b1c37bb7 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)'; @@ -42,7 +43,7 @@ export function ConnectApprovalScreen() {
{/* Header */}
diff --git a/extension/popup/screens/approvals/SignMessageScreen.tsx b/extension/popup/screens/approvals/SignMessageScreen.tsx index ce69fa7a..5a3528a7 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)'; @@ -40,7 +41,7 @@ export function SignMessageScreen() {
{/* Header */}
diff --git a/extension/popup/screens/approvals/SignRawTxScreen.tsx b/extension/popup/screens/approvals/SignRawTxScreen.tsx index db7a7818..9c5c4b20 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 @@ -139,7 +140,7 @@ export function SignRawTxScreen() {
{/* Header */}
diff --git a/extension/popup/screens/approvals/TransactionApprovalScreen.tsx b/extension/popup/screens/approvals/TransactionApprovalScreen.tsx index 753211e7..426c066d 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)'; @@ -47,7 +48,7 @@ export function TransactionApprovalScreen() {
{/* 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.
{showPopupHint && ( -
- Pop-up mode is active. Click the Iris icon in the toolbar to open the wallet in a pop-up window. +
+ 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/ReceiveScreen.tsx b/extension/popup/screens/ReceiveScreen.tsx index b179981d..000e525c 100644 --- a/extension/popup/screens/ReceiveScreen.tsx +++ b/extension/popup/screens/ReceiveScreen.tsx @@ -53,7 +53,10 @@ export function ReceiveScreen() { {/* Content */} -
+
{/* Intro */}
diff --git a/extension/popup/screens/SendSubmittedScreen.tsx b/extension/popup/screens/SendSubmittedScreen.tsx index f33a7bbb..7bc8d880 100644 --- a/extension/popup/screens/SendSubmittedScreen.tsx +++ b/extension/popup/screens/SendSubmittedScreen.tsx @@ -104,10 +104,7 @@ export function SendSubmittedScreen() { } return ( -
+
{/* Header */}
-
+
{/* Header */}

diff --git a/extension/popup/screens/approvals/SignMessageScreen.tsx b/extension/popup/screens/approvals/SignMessageScreen.tsx index 5a3528a7..92e69b2f 100644 --- a/extension/popup/screens/approvals/SignMessageScreen.tsx +++ b/extension/popup/screens/approvals/SignMessageScreen.tsx @@ -39,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 9c5c4b20..de18945e 100644 --- a/extension/popup/screens/approvals/SignRawTxScreen.tsx +++ b/extension/popup/screens/approvals/SignRawTxScreen.tsx @@ -138,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 426c066d..fc08ba5a 100644 --- a/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +++ b/extension/popup/screens/approvals/TransactionApprovalScreen.tsx @@ -46,10 +46,7 @@ export function TransactionApprovalScreen() { return (
-
+
{/* Header */}

diff --git a/extension/shared/pending-approvals-session.ts b/extension/shared/pending-approvals-session.ts index f56ce58f..97ceccf5 100644 --- a/extension/shared/pending-approvals-session.ts +++ b/extension/shared/pending-approvals-session.ts @@ -1,11 +1,6 @@ import { SESSION_STORAGE_KEYS } from './constants'; import type { ApprovalType } from './constants'; -import type { - ConnectRequest, - SignRequest, - TransactionRequest, - SignRawTxRequest, -} from './types'; +import type { ConnectRequest, SignRequest, TransactionRequest, SignRawTxRequest } from './types'; type PersistableRequest = ConnectRequest | SignRequest | TransactionRequest; @@ -60,8 +55,7 @@ export function buildPendingApprovalSessionSnapshot( const queue = requestQueue.filter(item => pending[item.id]); - const activeCurrentId = - currentRequestId && pending[currentRequestId] ? currentRequestId : null; + const activeCurrentId = currentRequestId && pending[currentRequestId] ? currentRequestId : null; const activeCurrentType = activeCurrentId ? currentRequestType : null; if (!activeCurrentId && queue.length === 0 && Object.keys(pending).length === 0) { From d411da4042be681674f0f8988fc14ffe9bbb2653 Mon Sep 17 00:00:00 2001 From: thomasurbit <99678438+thomasurbit@users.noreply.github.com> Date: Thu, 11 Jun 2026 22:39:53 +0200 Subject: [PATCH 7/8] fix: open side panel via synchronous gesture hook in background chrome.sidePanel and chrome.tabs are unavailable in content scripts, so the previous content-script open call never ran. Open the panel synchronously in the onMessage listener instead, where Chrome preserves the dApp click gesture (any await consumes it), using an in-memory display mode cache. Co-authored-by: Cursor --- extension/background/index.ts | 41 +++++++++++++++++++++++++++++++++- extension/content/index.ts | 22 ------------------ extension/shared/side-panel.ts | 33 +-------------------------- 3 files changed, 41 insertions(+), 55 deletions(-) diff --git a/extension/background/index.ts b/extension/background/index.ts index 12220110..0c42b95d 100644 --- a/extension/background/index.ts +++ b/extension/background/index.ts @@ -45,7 +45,7 @@ import type { SignRawTxRequest, WalletTransaction, } from '../shared/types'; -import { SIDE_PANEL_DEFAULT_PATH } from '../shared/side-panel'; +import { SIDE_PANEL_DEFAULT_PATH, APPROVAL_PROVIDER_METHODS } from '../shared/side-panel'; import { buildPendingApprovalSessionSnapshot, persistPendingApprovalSession, @@ -65,6 +65,12 @@ let requestQueue: Array<{ 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 @@ -544,6 +550,8 @@ async function applyDisplayMode(mode: DisplayMode): Promise { ? 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 }); @@ -948,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(); diff --git a/extension/content/index.ts b/extension/content/index.ts index f6f1ad87..0124ec6c 100644 --- a/extension/content/index.ts +++ b/extension/content/index.ts @@ -6,16 +6,6 @@ */ import { MESSAGE_TARGETS } from '../shared/constants'; -import { APPROVAL_PROVIDER_METHODS, openSidePanelFromUserGesture } from '../shared/side-panel'; - -function getProviderMethod(payload: unknown): string | undefined { - if (!payload || typeof payload !== 'object' || !('method' in payload)) { - return undefined; - } - - const method = (payload as { method?: unknown }).method; - return typeof method === 'string' ? method : undefined; -} /** * Bridge page <-> Service Worker @@ -34,18 +24,6 @@ window.addEventListener('message', async (evt: MessageEvent) => { return; } - const method = getProviderMethod(data.payload); - if (method && APPROVAL_PROVIDER_METHODS.has(method)) { - try { - const tab = await chrome.tabs.getCurrent(); - if (tab?.id !== undefined) { - await openSidePanelFromUserGesture(tab.id); - } - } catch (error) { - console.warn('[Iris] Failed to open side panel before provider request:', error); - } - } - // Forward to service worker and relay response back to page const reply = await chrome.runtime.sendMessage(data); diff --git a/extension/shared/side-panel.ts b/extension/shared/side-panel.ts index 4d6252a6..05d54e0b 100644 --- a/extension/shared/side-panel.ts +++ b/extension/shared/side-panel.ts @@ -1,4 +1,4 @@ -import { DISPLAY_MODES, PROVIDER_METHODS, STORAGE_KEYS } from './constants'; +import { PROVIDER_METHODS } from './constants'; export const SIDE_PANEL_DEFAULT_PATH = 'sidepanel/index.html'; @@ -9,34 +9,3 @@ export const APPROVAL_PROVIDER_METHODS = new Set([ PROVIDER_METHODS.SEND_TRANSACTION, PROVIDER_METHODS.SIGN_TX, ]); - -export async function isSidePanelDisplayMode(): Promise { - if (!chrome.sidePanel) { - return false; - } - - const stored = await chrome.storage.local.get([STORAGE_KEYS.DISPLAY_MODE]); - return stored[STORAGE_KEYS.DISPLAY_MODE] === DISPLAY_MODES.SIDE_PANEL; -} - -/** Open the wallet side panel while the user-gesture chain is still active. */ -export async function openSidePanelFromUserGesture(tabId: number): Promise { - if (!chrome.sidePanel) { - return; - } - - if (!(await isSidePanelDisplayMode())) { - return; - } - - try { - await chrome.sidePanel.setOptions({ - tabId, - path: SIDE_PANEL_DEFAULT_PATH, - enabled: true, - }); - await chrome.sidePanel.open({ tabId }); - } catch (error) { - console.warn('[Iris] sidePanel.open from user gesture failed:', error); - } -} From 302f1669ba9b0878f2b555cf2d7c2b55fae53631 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 11:10:31 +0000 Subject: [PATCH 8/8] feat: replace wallet icon pack and rework style assignment logic - Replace the 15 numbered wallet icons with a new 29-icon pack: 5 kept classics (styles 1, 2, 3, 8, 14 - default unchanged) plus 24 new iris-themed icons, normalised to the shared tintable-SVG convention (var(--fill-0) placeholder, container-sized root). - Add shared/walletStyles.ts as the single source of truth: icon registry with visual families (picker groups similar icons together), a 17-colour palette (7 existing + 10 new in the same poppy scheme), and a deterministic assignment sequence that round-robins across icon families and strides the colour rainbow so consecutive wallets always look distinct; combinations only repeat after all 493 are used. - Switch persisted iconStyleId to stable string ids with transparent migration of legacy numeric ids (retired styles fall back to default). - Drive AccountIcon and WalletStylingScreen from the shared registry. https://claude.ai/code/session_01EPxCTHkLN8QM5iuNR5qqrn --- extension/popup/assets/wallet-icon-bolt.svg | 4 + .../popup/assets/wallet-icon-cat-eye.svg | 4 + .../popup/assets/wallet-icon-fibonacci.svg | 4 + .../popup/assets/wallet-icon-gaze-down.svg | 4 + .../popup/assets/wallet-icon-gaze-left.svg | 4 + .../popup/assets/wallet-icon-gaze-right.svg | 4 + extension/popup/assets/wallet-icon-gem.svg | 4 + extension/popup/assets/wallet-icon-heart.svg | 4 + .../popup/assets/wallet-icon-keyhole.svg | 4 + extension/popup/assets/wallet-icon-moon.svg | 4 + .../popup/assets/wallet-icon-nock-n-round.svg | 4 + extension/popup/assets/wallet-icon-nock-n.svg | 4 + .../popup/assets/wallet-icon-north-star.svg | 4 + extension/popup/assets/wallet-icon-orbit.svg | 4 + .../popup/assets/wallet-icon-pinwheel-sun.svg | 4 + .../assets/wallet-icon-quatrefoil-eye.svg | 4 + extension/popup/assets/wallet-icon-ring.svg | 4 + .../popup/assets/wallet-icon-seal-eye.svg | 4 + .../assets/wallet-icon-seal-fibonacci.svg | 4 + .../popup/assets/wallet-icon-snowflake.svg | 4 + extension/popup/assets/wallet-icon-spiral.svg | 4 + .../popup/assets/wallet-icon-squircle-eye.svg | 4 + .../popup/assets/wallet-icon-style-10.svg | 8 - .../popup/assets/wallet-icon-style-11.svg | 8 - .../popup/assets/wallet-icon-style-12.svg | 8 - .../popup/assets/wallet-icon-style-13.svg | 8 - .../popup/assets/wallet-icon-style-15.svg | 8 - .../popup/assets/wallet-icon-style-4.svg | 8 - .../popup/assets/wallet-icon-style-5.svg | 8 - .../popup/assets/wallet-icon-style-6.svg | 8 - .../popup/assets/wallet-icon-style-7.svg | 8 - .../popup/assets/wallet-icon-style-9.svg | 8 - extension/popup/assets/wallet-icon-sun.svg | 4 + extension/popup/assets/wallet-icon-target.svg | 4 + extension/popup/components/AccountIcon.tsx | 52 ++---- .../popup/components/walletIconAssets.ts | 67 +++++++ .../popup/screens/WalletStylingScreen.tsx | 61 ++----- extension/shared/constants.ts | 42 ----- extension/shared/types.ts | 4 +- extension/shared/vault.ts | 50 +++--- extension/shared/walletStyles.ts | 169 ++++++++++++++++++ 41 files changed, 391 insertions(+), 230 deletions(-) create mode 100644 extension/popup/assets/wallet-icon-bolt.svg create mode 100644 extension/popup/assets/wallet-icon-cat-eye.svg create mode 100644 extension/popup/assets/wallet-icon-fibonacci.svg create mode 100644 extension/popup/assets/wallet-icon-gaze-down.svg create mode 100644 extension/popup/assets/wallet-icon-gaze-left.svg create mode 100644 extension/popup/assets/wallet-icon-gaze-right.svg create mode 100644 extension/popup/assets/wallet-icon-gem.svg create mode 100644 extension/popup/assets/wallet-icon-heart.svg create mode 100644 extension/popup/assets/wallet-icon-keyhole.svg create mode 100644 extension/popup/assets/wallet-icon-moon.svg create mode 100644 extension/popup/assets/wallet-icon-nock-n-round.svg create mode 100644 extension/popup/assets/wallet-icon-nock-n.svg create mode 100644 extension/popup/assets/wallet-icon-north-star.svg create mode 100644 extension/popup/assets/wallet-icon-orbit.svg create mode 100644 extension/popup/assets/wallet-icon-pinwheel-sun.svg create mode 100644 extension/popup/assets/wallet-icon-quatrefoil-eye.svg create mode 100644 extension/popup/assets/wallet-icon-ring.svg create mode 100644 extension/popup/assets/wallet-icon-seal-eye.svg create mode 100644 extension/popup/assets/wallet-icon-seal-fibonacci.svg create mode 100644 extension/popup/assets/wallet-icon-snowflake.svg create mode 100644 extension/popup/assets/wallet-icon-spiral.svg create mode 100644 extension/popup/assets/wallet-icon-squircle-eye.svg delete mode 100644 extension/popup/assets/wallet-icon-style-10.svg delete mode 100644 extension/popup/assets/wallet-icon-style-11.svg delete mode 100644 extension/popup/assets/wallet-icon-style-12.svg delete mode 100644 extension/popup/assets/wallet-icon-style-13.svg delete mode 100644 extension/popup/assets/wallet-icon-style-15.svg delete mode 100644 extension/popup/assets/wallet-icon-style-4.svg delete mode 100644 extension/popup/assets/wallet-icon-style-5.svg delete mode 100644 extension/popup/assets/wallet-icon-style-6.svg delete mode 100644 extension/popup/assets/wallet-icon-style-7.svg delete mode 100644 extension/popup/assets/wallet-icon-style-9.svg create mode 100644 extension/popup/assets/wallet-icon-sun.svg create mode 100644 extension/popup/assets/wallet-icon-target.svg create mode 100644 extension/popup/components/walletIconAssets.ts create mode 100644 extension/shared/walletStyles.ts 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/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/screens/WalletStylingScreen.tsx b/extension/popup/screens/WalletStylingScreen.tsx index c3d54710..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); diff --git a/extension/shared/constants.ts b/extension/shared/constants.ts index 202c20c6..9db016d6 100644 --- a/extension/shared/constants.ts +++ b/extension/shared/constants.ts @@ -404,48 +404,6 @@ export const UI_CONSTANTS = { MNEMONIC_WORD_COUNT: 24, } as const; -/** - * Account Icon Colors - Available colors for account customization - */ -export const ACCOUNT_COLORS = [ - '#2C9AEF', // blue - '#EF2C2F', // red - '#FFC413', // yellow (primary) - '#96B839', // green - '#3C2CEF', // purple - '#EF2CB1', // pink/magenta - '#2C6AEF', // darker blue -] as const; - -/** - * Preset Wallet Styles - Predetermined icon/color combinations for first 21 wallets - * Ensures visual variety without repeating until all presets are used - * After preset limit, random combinations are used - */ -export const PRESET_WALLET_STYLES = [ - { iconStyleId: 1, iconColor: '#FFC413' }, // Wallet 1: yellow, style 1 - { iconStyleId: 5, iconColor: '#2C9AEF' }, // Wallet 2: blue, style 5 - { iconStyleId: 9, iconColor: '#EF2C2F' }, // Wallet 3: red, style 9 - { iconStyleId: 3, iconColor: '#96B839' }, // Wallet 4: green, style 3 - { iconStyleId: 12, iconColor: '#3C2CEF' }, // Wallet 5: purple, style 12 - { iconStyleId: 7, iconColor: '#EF2CB1' }, // Wallet 6: pink, style 7 - { iconStyleId: 15, iconColor: '#2C6AEF' }, // Wallet 7: dark blue, style 15 - { iconStyleId: 2, iconColor: '#EF2C2F' }, // Wallet 8: red, style 2 - { iconStyleId: 6, iconColor: '#FFC413' }, // Wallet 9: yellow, style 6 - { iconStyleId: 10, iconColor: '#96B839' }, // Wallet 10: green, style 10 - { iconStyleId: 4, iconColor: '#2C9AEF' }, // Wallet 11: blue, style 4 - { iconStyleId: 13, iconColor: '#EF2CB1' }, // Wallet 12: pink, style 13 - { iconStyleId: 8, iconColor: '#3C2CEF' }, // Wallet 13: purple, style 8 - { iconStyleId: 14, iconColor: '#2C6AEF' }, // Wallet 14: dark blue, style 14 - { iconStyleId: 11, iconColor: '#EF2C2F' }, // Wallet 15: red, style 11 - { iconStyleId: 1, iconColor: '#96B839' }, // Wallet 16: green, style 1 - { iconStyleId: 5, iconColor: '#FFC413' }, // Wallet 17: yellow, style 5 - { iconStyleId: 9, iconColor: '#2C9AEF' }, // Wallet 18: blue, style 9 - { iconStyleId: 3, iconColor: '#3C2CEF' }, // Wallet 19: purple, style 3 - { iconStyleId: 12, iconColor: '#EF2CB1' }, // Wallet 20: pink, style 12 - { iconStyleId: 7, iconColor: '#2C6AEF' }, // Wallet 21: dark blue, style 7 -] as const; - /** * Approval Request Constants - URL hash prefixes for approval flows */ diff --git a/extension/shared/types.ts b/extension/shared/types.ts index 683ffeec..a2cc6593 100644 --- a/extension/shared/types.ts +++ b/extension/shared/types.ts @@ -17,8 +17,8 @@ export interface SubAccount { address: string; /** BIP-44 derivation index (0 = master/underived, 1+ = slip10 children) */ index: number; - /** Icon style ID (1-15, defaults to index % 3 + 1 for variety) */ - iconStyleId?: number; + /** Icon style id (slug, e.g. 'sun'); legacy vaults may hold numeric ids 1-15 */ + iconStyleId?: number | string; /** Icon color (hex string, defaults to #FFC413) */ iconColor?: string; /** Whether this account is hidden from the UI */ diff --git a/extension/shared/vault.ts b/extension/shared/vault.ts index 4cce04c0..e7b724d4 100644 --- a/extension/shared/vault.ts +++ b/extension/shared/vault.ts @@ -13,11 +13,16 @@ import { import { ERROR_CODES, STORAGE_KEYS, - ACCOUNT_COLORS, - PRESET_WALLET_STYLES, NOCK_TO_NICKS, MAX_SUBWALLET_DISCOVERY_SCAN, } from './constants'; +import { + DEFAULT_WALLET_STYLE, + TOTAL_STYLE_COMBINATIONS, + getPresetWalletStyle, + normalizeIconStyleId, + type WalletStyle, +} from './walletStyles'; import { SubAccount, SeedAccount } from './types'; import { buildMultiNotePayment, @@ -341,30 +346,29 @@ export class Vault { return (account?.index ?? -1) === 0; } - /** Returns a style (icon + color) not already used by any account across all seeds. */ - private pickUnusedStyleGlobally(): { iconStyleId: number; iconColor: string } { + /** + * Returns a style (icon + color) not already used by any account across all seeds. + * + * Walks the deterministic preset sequence (see `getPresetWalletStyle`) and + * returns the first combination still free, so new wallets get a varied but + * predictable look. Only once every combination is taken does it fall back + * to the default style. + */ + private pickUnusedStyleGlobally(): WalletStyle { const allAccounts = this.seedAccounts.flatMap(seed => seed.accounts); const usedKeys = new Set( allAccounts.map( - a => `${a.iconStyleId ?? 1}-${a.iconColor ?? PRESET_WALLET_STYLES[0].iconColor}` + a => + `${normalizeIconStyleId(a.iconStyleId)}-${a.iconColor ?? DEFAULT_WALLET_STYLE.iconColor}` ) ); - for (const preset of PRESET_WALLET_STYLES) { - const key = `${preset.iconStyleId}-${preset.iconColor}`; - if (!usedKeys.has(key)) { - return { iconStyleId: preset.iconStyleId, iconColor: preset.iconColor }; + for (let i = 0; i < TOTAL_STYLE_COMBINATIONS; i++) { + const preset = getPresetWalletStyle(i); + if (!usedKeys.has(`${preset.iconStyleId}-${preset.iconColor}`)) { + return preset; } } - // All presets used: pick random until we find an unused combo - const styleIds = Array.from({ length: 15 }, (_, i) => i + 1); - const colors = [...ACCOUNT_COLORS]; - for (let attempt = 0; attempt < 200; attempt++) { - const iconStyleId = styleIds[Math.floor(Math.random() * styleIds.length)]; - const iconColor = colors[Math.floor(Math.random() * colors.length)]; - const key = `${iconStyleId}-${iconColor}`; - if (!usedKeys.has(key)) return { iconStyleId, iconColor }; - } - return { iconStyleId: 1, iconColor: PRESET_WALLET_STYLES[0].iconColor }; + return { ...DEFAULT_WALLET_STYLE }; } private createSeedAccountFromLegacy(mnemonic: string, legacyAccounts: SubAccount[]): SeedAccount { @@ -537,8 +541,8 @@ export class Vault { } // Create first account (Wallet 1 at index 0) - // Use first preset style for consistent initial experience - const firstPreset = PRESET_WALLET_STYLES[0]; + // Use the default style for consistent initial experience + const firstPreset = DEFAULT_WALLET_STYLE; const masterAddress = await deriveAddressFromMaster(words); @@ -2797,7 +2801,7 @@ export class Vault { */ async updateAccountStyling( address: string, - iconStyleId: number, + iconStyleId: number | string, iconColor: string ): Promise<{ ok: boolean } | { error: string }> { if (this.state.locked) { @@ -2809,7 +2813,7 @@ export class Vault { return { error: ERROR_CODES.BAD_ADDRESS }; } - this.state.accounts[index].iconStyleId = iconStyleId; + this.state.accounts[index].iconStyleId = normalizeIconStyleId(iconStyleId); this.state.accounts[index].iconColor = iconColor; // Save accounts to encrypted vault diff --git a/extension/shared/walletStyles.ts b/extension/shared/walletStyles.ts new file mode 100644 index 00000000..72d92f92 --- /dev/null +++ b/extension/shared/walletStyles.ts @@ -0,0 +1,169 @@ +/** + * Wallet Styles - single source of truth for account icon styles and colors + * + * Icons are identified by stable string ids (e.g. 'sun', 'gaze-left') that are + * persisted on each account in the vault. Vaults written by older versions may + * still hold numeric ids (1-15); `normalizeIconStyleId` maps the surviving ones + * to their string id and retired ones to the default. + * + * Two orders are derived from the registry below: + * - Picker order (`WALLET_ICONS` as-is): icons of the same visual family sit + * next to each other so users can compare close variants. + * - Assignment order (`ICON_ASSIGNMENT_ORDER`): a round-robin across families + * so consecutively created wallets get visually distinct icons. + */ + +export interface WalletIconDef { + /** Stable id stored in the vault; asset file is wallet-icon-.svg */ + id: string; + /** Visual family; icons of one family look alike and are grouped in the picker */ + family: string; +} + +/** All available icon styles, in the order shown in the customization picker. */ +export const WALLET_ICONS: readonly WalletIconDef[] = [ + // Classic petal frames (original set) + { id: 'style-1', family: 'classic' }, // default + { id: 'style-2', family: 'classic' }, + { id: 'style-3', family: 'classic' }, + { id: 'style-8', family: 'classic' }, + { id: 'style-14', family: 'classic' }, + // Gaze directions + { id: 'gaze-left', family: 'gaze' }, + { id: 'gaze-right', family: 'gaze' }, + { id: 'gaze-down', family: 'gaze' }, + // Eye in different frames + { id: 'squircle-eye', family: 'eye' }, + { id: 'seal-eye', family: 'eye' }, + { id: 'quatrefoil-eye', family: 'eye' }, + { id: 'cat-eye', family: 'eye' }, + // Concentric shapes + { id: 'ring', family: 'rings' }, + { id: 'target', family: 'rings' }, + // Celestial + { id: 'sun', family: 'celestial' }, + { id: 'pinwheel-sun', family: 'celestial' }, + { id: 'moon', family: 'celestial' }, + { id: 'north-star', family: 'celestial' }, + { id: 'orbit', family: 'celestial' }, + // Spirals + { id: 'spiral', family: 'spiral' }, + { id: 'fibonacci', family: 'spiral' }, + { id: 'seal-fibonacci', family: 'spiral' }, + // Standalone symbols + { id: 'snowflake', family: 'symbol' }, + { id: 'gem', family: 'symbol' }, + { id: 'heart', family: 'symbol' }, + { id: 'bolt', family: 'symbol' }, + { id: 'keyhole', family: 'symbol' }, + // Nockchain brand + { id: 'nock-n', family: 'brand' }, + { id: 'nock-n-round', family: 'brand' }, +] as const; + +/** + * Account icon colors, in the order shown in the customization picker + * (rainbow starting at the default yellow). + */ +export const ACCOUNT_COLORS = [ + '#FFC413', // yellow (default) + '#EF7A2C', // orange + '#EF2C2F', // red + '#EF2C6A', // raspberry + '#EF2CB1', // pink + '#EF2CE3', // magenta + '#C42CEF', // fuchsia + '#9A2CEF', // violet + '#3C2CEF', // purple + '#2C6AEF', // dark blue + '#2C9AEF', // blue + '#2CC4EF', // sky + '#2CEFE3', // turquoise + '#2CEFB1', // mint + '#2CEF5E', // spring green + '#7AEF2C', // lime + '#96B839', // olive green +] as const; + +export interface WalletStyle { + iconStyleId: string; + iconColor: string; +} + +/** Style of the very first wallet, and fallback whenever a style is missing. */ +export const DEFAULT_WALLET_STYLE: WalletStyle = { + iconStyleId: WALLET_ICONS[0].id, + iconColor: ACCOUNT_COLORS[0], +}; + +/** + * Numeric icon ids written by older versions. Retired styles (4-7, 9-13, 15) + * have no entry and resolve to the default. + */ +const LEGACY_ICON_IDS: Record = { + 1: 'style-1', + 2: 'style-2', + 3: 'style-3', + 8: 'style-8', + 14: 'style-14', +}; + +const KNOWN_ICON_IDS = new Set(WALLET_ICONS.map(icon => icon.id)); + +/** Resolves a stored icon style (string, legacy number, or missing) to a valid id. */ +export function normalizeIconStyleId(value: number | string | undefined | null): string { + if (typeof value === 'string' && KNOWN_ICON_IDS.has(value)) return value; + if (typeof value === 'number' && LEGACY_ICON_IDS[value]) return LEGACY_ICON_IDS[value]; + return DEFAULT_WALLET_STYLE.iconStyleId; +} + +/** + * Icon order used when auto-assigning styles to new wallets: a round-robin + * across families (first icon of each family, then second of each, ...) so + * back-to-back wallets never get two icons from the same family. + */ +export const ICON_ASSIGNMENT_ORDER: readonly string[] = (() => { + const families: string[][] = []; + const byFamily = new Map(); + for (const icon of WALLET_ICONS) { + let bucket = byFamily.get(icon.family); + if (!bucket) { + bucket = []; + byFamily.set(icon.family, bucket); + families.push(bucket); + } + bucket.push(icon.id); + } + const order: string[] = []; + for (let round = 0; order.length < WALLET_ICONS.length; round++) { + for (const bucket of families) { + if (round < bucket.length) order.push(bucket[round]); + } + } + return order; +})(); + +/** + * Color stride for auto-assignment: stepping the rainbow by 7 keeps + * consecutive wallets far apart in hue. 7 is coprime with the palette size, + * so all colors are visited before any repeats. + */ +const COLOR_ASSIGNMENT_STRIDE = 7; + +/** Total number of distinct icon/color combinations. */ +export const TOTAL_STYLE_COMBINATIONS = WALLET_ICONS.length * ACCOUNT_COLORS.length; + +/** + * Deterministic style for the n-th auto-assigned wallet (0-based). + * + * Icons cycle through `ICON_ASSIGNMENT_ORDER` and colors through the strided + * rainbow. Because the icon count (29) and color count (17) are coprime, the + * sequence only repeats a combination after all `TOTAL_STYLE_COMBINATIONS` + * (493) have been used. Index 0 is the default style. + */ +export function getPresetWalletStyle(index: number): WalletStyle { + return { + iconStyleId: ICON_ASSIGNMENT_ORDER[index % ICON_ASSIGNMENT_ORDER.length], + iconColor: ACCOUNT_COLORS[(index * COLOR_ASSIGNMENT_STRIDE) % ACCOUNT_COLORS.length], + }; +}