Skip to content

feat: popup / side panel display mode toggle - #51

Open
thomas-kroes wants to merge 16 commits into
mainfrom
feat/sidepanel
Open

thomas-kroes wants to merge 16 commits into
mainfrom
feat/sidepanel

Conversation

@thomas-kroes

@thomas-kroes thomas-kroes commented Jun 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add a MetaMask-style Display mode setting so users can choose between the toolbar popup and Chrome side panel.
  • Use the side panel whenever no display-mode preference has been stored, including for existing users upgrading from a version that did not have this setting. Once a user selects popup or side panel, that explicit preference is preserved.
  • Reuse the popup React application in a side-panel entry point and route dApp approvals into the selected display mode.
  • Preserve serializable approval queues in chrome.storage.session across service-worker restarts, deliberately excluding non-cloneable raw WASM approvals and promoting the next restorable request when necessary.
  • Validate approval expiry, requester liveness, origin, and selected account before completing a request; scope page messages to the current origin.
  • Use a 10-minute auto-lock default whenever no preference is stored, including upgrades. An explicitly stored 0 remains the persistent Never option.
  • Show the active wallet icon in the settings header.

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 tests
  • npm run typecheck
  • npm run format:check
  • npm run build
  • GitHub CI runs all four checks, including the regression suite.

Manual acceptance checklist

  • Build and load unpacked from dist/ in Chrome 114+
  • Fresh install and an existing install with no stored display-mode key default to side panel
  • Explicit popup/side-panel preference survives extension and service-worker restarts
  • Toggle Display mode: popup ↔ side panel; verify toolbar icon behavior in each mode
  • Side-panel mode: connect / sign / send tx approvals appear in the panel, not floating popups
  • Side panel closed → trigger approval from a user gesture → panel opens and shows the request
  • Popup mode after a cold worker restart opens only the popup, not both interfaces
  • Reload extension with a pending serializable approval → approval is restored
  • A queued serializable approval is promoted when an active raw-WASM approval cannot be restored
  • Raw signing approvals are not written to session storage and must be requested again after restart
  • Changing account or origin invalidates the pending approval
  • Locked wallet + pending approval → unlock → lands on the approval screen
  • Missing auto-lock preference defaults to 10 minutes; selecting Never remains persistent
  • Existing encrypted vault upgrade, keyfile export/import, and seed-copy flows remain functional
  • HTTP and HTTPS dApp connection/approval behavior remains functional
  • Light/dark themes and layout look correct in popup and side panel

thomas-kroes and others added 10 commits June 10, 2026 15:03
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
@Gohlub
Gohlub marked this pull request as ready for review June 22, 2026 14:41
@greptile-apps

greptile-apps Bot commented Jun 22, 2026 •

Copy link
Copy Markdown

Greptile Summary

This 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.

  • New pending-approvals-session.ts persists serialisable approval state to chrome.storage.session so queued approvals survive service-worker restarts; non-cloneable WASM SignRawTxRequest objects are intentionally excluded and must be re-requested.
  • validatePendingApproval centralises expiry, duplicate-processing, and requester-liveness checks; the content script gains a REQUESTER_PING handler and narrows postMessage targets from '*' to window.location.origin for improved security.
  • maybeOpenSidePanelOnGesture runs synchronously on the first message to preserve the user-gesture context needed for chrome.sidePanel.open, but the in-memory cachedDisplayMode is initialised to DEFAULT_DISPLAY_MODE (side panel) and is only corrected once initPromise completes — causing the side panel to open unexpectedly for popup-mode users on the first approval after a cold SW start.

Confidence Score: 4/5

Safe 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.

Important Files Changed

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')
Loading
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

Comment thread extension/shared/pending-approvals-session.ts
Comment thread extension/background/index.ts Outdated
Comment thread extension/popup/screens/SettingsScreen.tsx Outdated
@Gohlub

Gohlub commented Jun 22, 2026

Copy link
Copy Markdown
Contributor
Screenshot 2026-06-22 at 10 49 12 inside of settings, the top left icon should be the wallet icon

@Gohlub

Gohlub commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Most extensions default to the sidebar view these days: maybe we should to?

Comment thread extension/background/index.ts Outdated
* 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants