Repository navigation
feat: popup / side panel display mode toggle - #51
thomas-kroes wants to merge 16 commits into
Conversation
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 <cursoragent@cursor.com>
When display mode is side panel, always notify the panel and never fall back to approval popup windows if sidePanel.open() fails. Co-authored-by: Cursor <cursoragent@cursor.com>
Covers the case where an approval arrived while the panel was hidden or the runtime message was missed before the panel remounted. Co-authored-by: Cursor <cursoragent@cursor.com>
Open the side panel on the user-gesture chain before provider requests that may need approval. Persist connect/sign/tx pending state to chrome.storage.session so the side panel can restore after SW restarts. Also restyle the display mode settings icon to match other settings icons. Co-authored-by: Cursor <cursoragent@cursor.com>
Use the same solid/hollow evenodd split as the theme icon — a window frame with a cut-out side panel — instead of two thin filled blocks. Co-authored-by: Cursor <cursoragent@cursor.com>
Keep fluid w-full h-full layout on SendReviewScreen from the side panel work. Co-authored-by: Cursor <cursoragent@cursor.com>
Fixes the failing typecheck-and-format CI check. Co-authored-by: Cursor <cursoragent@cursor.com>
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 <cursoragent@cursor.com>
- 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
feat: replace wallet icon pack and rework style assignment logic
Greptile SummaryThis PR adds a MetaMask-style Display mode setting (popup ↔ side panel) to the Iris wallet extension, reusing the existing React app as a side-panel entry point and routing dApp approvals into the panel when that mode is active.
Confidence Score: 4/5Safe to merge with one fix: the stale cachedDisplayMode default can open the side panel unexpectedly for users who switched to popup mode. The core side-panel routing, session persistence, and approval-validation logic is well-structured and all error paths clean up correctly. The one concrete defect is that cachedDisplayMode starts as SIDE_PANEL on every cold SW start; a popup-mode user who triggers a dApp approval before initPromise completes will see the side panel open alongside the popup window. Files Needing Attention: extension/background/index.ts — specifically the cachedDisplayMode initialisation and its interaction with maybeOpenSidePanelOnGesture on cold SW start.
|
| Filename | Overview |
|---|---|
| extension/background/index.ts | Heavy rework adding display-mode switching, side-panel routing, session persistence, and per-request processing guards. Well-structured, but cachedDisplayMode is initialized to DEFAULT_DISPLAY_MODE (SIDE_PANEL) and only updated after initPromise completes, causing a stale-cache window on cold SW start that can open the side panel for popup-mode users. |
| extension/shared/pending-approvals-session.ts | New file: serialises non-WASM approval state to chrome.storage.session for SW-restart resilience. Correctly excludes SignRawTxRequest (non-cloneable WASM objects) and expiry-filters on both build and restore. |
| extension/content/index.ts | Security improvements: validates incoming bridge messages with isBridgeRequest(), narrows postMessage target from '*' to window.location.origin, and adds REQUESTER_PING handler for active-document verification. |
| extension/popup/hooks/useApprovalDetection.ts | Refactored to support both popup (URL-hash) and side-panel (runtime message + visibility-change re-check) approval delivery; uses refs to avoid stale closures on the listener registered before wallet init. |
| extension/popup/screens/DisplayModeScreen.tsx | New settings screen for toggling popup vs side-panel mode; on switch to side panel, immediately opens the panel and closes the popup window. Guards against unsupported browsers. |
| extension/shared/constants.ts | Adds DisplayMode type, DISPLAY_MODES, RUNTIME_MESSAGE_TYPES, ApprovalType, and AUTOLOCK_ALLOWED_MINUTES. Changes AUTOLOCK_MINUTES default from 0 to 10. Removes ACCOUNT_COLORS/PRESET_WALLET_STYLES (moved elsewhere). |
| extension/shared/side-panel.ts | New thin module centralising the side-panel default path, legacy method name, and APPROVAL_PROVIDER_METHODS set used for synchronous gesture detection. |
| extension/popup/utils/displayContext.ts | New utility: isSidePanel() detects context via pathname, closeAfterApproval() routes post-approval navigation correctly between popup-close and panel-navigate('home'). |
| extension/sidepanel/index.tsx | New side-panel entry point that reuses the same Popup component and styles as the popup, adding a class on documentElement for layout differentiation. |
Sequence Diagram
sequenceDiagram
participant dApp
participant CS as Content Script
participant BG as Background SW
participant SP as Side Panel UI
dApp->>CS: postMessage (e.g. nock_connect)
CS->>BG: sendMessage [user gesture active]
Note over BG: maybeOpenSidePanelOnGesture() [sync, uses cachedDisplayMode]
BG->>SP: "sidePanel.open({ tabId }) [gesture-based]"
Note over BG: await initPromise → applyDisplayMode() updates cachedDisplayMode
BG->>BG: createApprovalPopup → routeApprovalToSidePanel
BG->>SP: sendMessage(APPROVAL_PENDING, requestId, type)
SP->>BG: GET_PENDING_APPROVAL
BG-->>SP: "{ requestId, approvalType }"
SP->>BG: GET_PENDING_CONNECTION [requestId]
BG-->>SP: ConnectRequest
SP->>SP: navigate('connect-approval')
Note over SP: User reviews and approves
SP->>BG: APPROVE_CONNECTION [requestId]
BG->>BG: validatePendingApproval (expiry, requester ping)
BG->>CS: sendResponse (ConnectResponse)
CS->>dApp: postMessage (reply)
BG->>SP: GET_PENDING_APPROVAL returns null
SP->>SP: navigate('home')
Prompt To Fix All With AI
### Issue 1
extension/background/index.ts:77
**Stale `cachedDisplayMode` opens side panel for popup-mode users on cold SW start**
`cachedDisplayMode` is initialised to `DEFAULT_DISPLAY_MODE` (`SIDE_PANEL`) and is only updated to the user's stored preference inside `initPromise` when `applyDisplayMode` runs. `maybeOpenSidePanelOnGesture` is called synchronously — before any `await` — on every incoming message, so the very first approval-bearing message after a cold service-worker start will always see `cachedDisplayMode === SIDE_PANEL`, regardless of what the user stored.
Concrete failure: a user who switched to popup mode lets the SW go idle, then immediately triggers a dApp approval. `maybeOpenSidePanelOnGesture` fires with the stale default, calls `chrome.sidePanel.open({ tabId })`, and the side panel opens unexpectedly. `createApprovalPopup` later reads the correct stored mode and also opens the popup window, leaving both UIs visible simultaneously.
The fix is to prime `cachedDisplayMode` early — reading it as the very first step in `initPromise`, before the `onInstalled`/`onMessage` handlers can observe the default.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (2): Last reviewed commit: "fix: keep restored approval queues reach..." | Re-trigger Greptile
|
Most extensions default to the sidebar view these days: maybe we should to? |
| * 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 = DEFAULT_DISPLAY_MODE; |
There was a problem hiding this comment.
Stale
cachedDisplayMode opens side panel for popup-mode users on cold SW start
cachedDisplayMode is initialised to DEFAULT_DISPLAY_MODE (SIDE_PANEL) and is only updated to the user's stored preference inside initPromise when applyDisplayMode runs. maybeOpenSidePanelOnGesture is called synchronously — before any await — on every incoming message, so the very first approval-bearing message after a cold service-worker start will always see cachedDisplayMode === SIDE_PANEL, regardless of what the user stored.
Concrete failure: a user who switched to popup mode lets the SW go idle, then immediately triggers a dApp approval. maybeOpenSidePanelOnGesture fires with the stale default, calls chrome.sidePanel.open({ tabId }), and the side panel opens unexpectedly. createApprovalPopup later reads the correct stored mode and also opens the popup window, leaving both UIs visible simultaneously.
The fix is to prime cachedDisplayMode early — reading it as the very first step in initPromise, before the onInstalled/onMessage handlers can observe the default.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extension/background/index.ts
Line: 77
Comment:
**Stale `cachedDisplayMode` opens side panel for popup-mode users on cold SW start**
`cachedDisplayMode` is initialised to `DEFAULT_DISPLAY_MODE` (`SIDE_PANEL`) and is only updated to the user's stored preference inside `initPromise` when `applyDisplayMode` runs. `maybeOpenSidePanelOnGesture` is called synchronously — before any `await` — on every incoming message, so the very first approval-bearing message after a cold service-worker start will always see `cachedDisplayMode === SIDE_PANEL`, regardless of what the user stored.
Concrete failure: a user who switched to popup mode lets the SW go idle, then immediately triggers a dApp approval. `maybeOpenSidePanelOnGesture` fires with the stale default, calls `chrome.sidePanel.open({ tabId })`, and the side panel opens unexpectedly. `createApprovalPopup` later reads the correct stored mode and also opens the popup window, leaving both UIs visible simultaneously.
The fix is to prime `cachedDisplayMode` early — reading it as the very first step in `initPromise`, before the `onInstalled`/`onMessage` handlers can observe the default.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
chrome.storage.sessionacross service-worker restarts, deliberately excluding non-cloneable raw WASM approvals and promoting the next restorable request when necessary.0remains the persistent Never option.Release integration
The approved migration behavior above is intentional. The phase-one release artifact is draft PR #55, which integrates this branch with #52 and the final fee/signing hardening. Do not merge this PR independently for the release.
Automated validation
npm test— 11 approval/session/display-mode regression testsnpm run typechecknpm run format:checknpm run buildManual acceptance checklist
dist/in Chrome 114+