Skip to content

feat: popup / side panel display mode toggle#51

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

feat: popup / side panel display mode toggle#51
thomas-kroes wants to merge 10 commits into
mainfrom
feat/sidepanel

Conversation

@thomas-kroes

Copy link
Copy Markdown
Contributor

Summary

  • Add a MetaMask-style Display mode setting (Settings → Display mode) so users can choose between the toolbar pop-up and Chrome's side panel.
  • Reuse the same React app in a new side panel entry point with fluid layout; route dApp approvals into the panel instead of separate pop-up windows when side panel mode is active.
  • Open the side panel from the content script on the user-gesture chain, persist pending approvals to chrome.storage.session across service worker restarts, and re-check pending approvals when the panel becomes visible again.

Test plan

  • Build and load unpacked from dist/ in Chrome 114+
  • Toggle Display mode: pop-up ↔ side panel; verify toolbar icon behavior in each mode
  • Side panel mode: connect / sign / send tx on a dApp — approvals appear in the panel, not floating pop-ups
  • Side panel closed → trigger connect → panel opens on click and shows approval
  • Reload extension with a pending approval → reopen side panel → approval is restored
  • Pop-up mode regression: approvals still open in dedicated pop-up windows
  • Locked wallet + pending approval → unlock → lands on approval screen
  • Theme light/dark and layout look correct in both pop-up and side panel

Made with Cursor

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 toggle (popup ↔ side panel) to the Iris wallet extension, introducing a new sidepanel/ entry point that reuses the existing React app, and routing dApp approval requests into the panel via chrome.runtime.sendMessage rather than opening separate popup windows.

  • Side panel routing: a synchronous gesture hook in the background (maybeOpenSidePanelOnGesture) opens the side panel before the first await so Chrome's user gesture requirement is met; useApprovalDetection gains a runtime message listener + visibility-change poll path for the panel.
  • Session persistence: new pending-approvals-session.ts serializes pending approvals to chrome.storage.session across service worker restarts; SignRawTxRequest is intentionally excluded because it contains native WASM objects, but queued persistable approvals (connect, transaction, sign-message) can be left permanently unreachable when a sign-raw-tx was the active request at restart time.
  • Wallet icon refactor: numeric style IDs (1–15) are migrated to stable string slugs via normalizeIconStyleId; the preset assignment algorithm is replaced with a deterministic round-robin across icon families × strided color palette.

Confidence Score: 3/5

The core popup path is unchanged and safe; the new side panel routing works for the happy path but has a gap in approval-queue restoration after a service worker restart.

When a sign-raw-tx approval is the active request at the moment the service worker restarts, any other approval requests queued behind it are written to session storage but never promoted to the active slot on restore. GET_PENDING_APPROVAL returns null, so those approvals are silently unreachable until the user re-triggers them from the dApp.

extension/shared/pending-approvals-session.ts and the restorePendingApprovalsSession function in extension/background/index.ts need a queue-promotion step when currentRequestId is null after restore.

Important Files Changed

Filename Overview
extension/background/index.ts Core routing logic for popup vs side panel; adds display mode cache, gesture hook, session sync, and new message handlers — nock_signRawTx is hardcoded outside APPROVAL_PROVIDER_METHODS
extension/shared/pending-approvals-session.ts New session-persistence module for approvals across SW restarts; sign-raw-tx is correctly excluded (WASM non-serializable) but queued persistable requests are never promoted when the current request is non-persistable
extension/popup/hooks/useApprovalDetection.ts Refactored to support both URL-hash (popup) and runtime-message/poll (side panel) approval detection; ref-based deferred dispatch pattern handles pre-init messages correctly
extension/popup/screens/DisplayModeScreen.tsx New settings screen for popup/side-panel toggle; sidePanel.open() after an await may lose the user gesture but the error is caught and the panel still opens via toolbar action
extension/shared/walletStyles.ts New single source of truth for icon styles and colors; adds normalizeIconStyleId for legacy numeric-to-string migration with clean fallback logic
extension/popup/screens/SettingsScreen.tsx Adds Display mode menu entry (side panel gated); minor duplicate flex-1 class introduced
extension/popup/utils/displayContext.ts New utility providing isSidePanel, isSidePanelSupported, and closeAfterApproval helpers; straightforward and correctly used across approval screens
extension/sidepanel/index.tsx New side panel entry point that reuses the existing Popup component with ThemeProvider; adds sidepanel class to root for CSS differentiation

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant dApp as dApp (content script)
    participant BG as Background SW
    participant SP as Side Panel UI

    dApp->>BG: provider request (e.g. eth_requestAccounts)
    Note over BG: maybeOpenSidePanelOnGesture()<br/>(sync, preserves user gesture)
    BG->>SP: "chrome.sidePanel.open({tabId})"
    BG->>BG: await initPromise / createApprovalPopup()
    BG->>BG: persistPendingApprovalSession()
    BG->>SP: runtime.sendMessage(APPROVAL_PENDING)
    SP->>BG: GET_PENDING_APPROVAL
    BG->>SP: "{requestId, approvalType}"
    SP->>BG: GET_PENDING_CONNECTION / GET_PENDING_TRANSACTION / ...
    SP->>SP: navigate to approval screen
    SP->>BG: APPROVE_CONNECTION / APPROVE_TRANSACTION / ...
    BG->>dApp: provider response
    BG->>BG: syncPendingApprovalsSession() (clear)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant dApp as dApp (content script)
    participant BG as Background SW
    participant SP as Side Panel UI

    dApp->>BG: provider request (e.g. eth_requestAccounts)
    Note over BG: maybeOpenSidePanelOnGesture()<br/>(sync, preserves user gesture)
    BG->>SP: "chrome.sidePanel.open({tabId})"
    BG->>BG: await initPromise / createApprovalPopup()
    BG->>BG: persistPendingApprovalSession()
    BG->>SP: runtime.sendMessage(APPROVAL_PENDING)
    SP->>BG: GET_PENDING_APPROVAL
    BG->>SP: "{requestId, approvalType}"
    SP->>BG: GET_PENDING_CONNECTION / GET_PENDING_TRANSACTION / ...
    SP->>SP: navigate to approval screen
    SP->>BG: APPROVE_CONNECTION / APPROVE_TRANSACTION / ...
    BG->>dApp: provider response
    BG->>BG: syncPendingApprovalsSession() (clear)
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
extension/shared/pending-approvals-session.ts:77-103
**Queued persistable approvals stuck after SW restart when `sign-raw-tx` is active**

When a `sign-raw-tx` is the active request at SW restart time, `buildPendingApprovalSessionSnapshot` cannot include it (WASM objects aren't serializable) so it sets `currentRequestId: null` in the snapshot. If there are queued persistable approvals (e.g., a `connect` or `transaction`), they _are_ written to session storage in `pending` and `queue` — but `restorePendingApprovalsSession` only calls `notifyApprovalPending` when `currentRequestId` is non-null after restore. There is no code path that promotes the head of the restored queue to `currentRequestId`, so those queued requests are permanently stuck: `GET_PENDING_APPROVAL` will return `null`, and the user never sees them after reloading the panel.

### Issue 2 of 3
extension/background/index.ts:974-980
`nock_signRawTx` is hardcoded as a string literal here instead of being added to `APPROVAL_PROVIDER_METHODS` in `side-panel.ts`. If this method name ever changes, or if `APPROVAL_PROVIDER_METHODS` is reused in another check, the raw-tx case will silently diverge. Add `nock_signRawTx` to the set so the background can reference `APPROVAL_PROVIDER_METHODS` exclusively.

```suggestion
  const method = msg?.payload?.method;
  if (typeof method !== 'string' || !APPROVAL_PROVIDER_METHODS.has(method)) {
    return;
  }
```

### Issue 3 of 3
extension/popup/screens/SettingsScreen.tsx:99
Duplicate `flex-1` Tailwind class — only one is needed.

```suggestion
      <div className="flex flex-col justify-between flex-1 min-h-0">
```

Reviews (1): Last reviewed commit: "Merge pull request #53 from nockbox/clau..." | Re-trigger Greptile

Comment on lines +77 to +103
if (!sessionStorage) {
return;
}

if (!snapshot) {
await sessionStorage.remove(SESSION_STORAGE_KEYS.PENDING_APPROVALS);
return;
}

await sessionStorage.set({
[SESSION_STORAGE_KEYS.PENDING_APPROVALS]: snapshot,
});
}

export async function loadPendingApprovalSession(): Promise<PendingApprovalSessionSnapshot | null> {
const sessionStorage = chrome.storage.session;
if (!sessionStorage) {
return null;
}

const stored = await sessionStorage.get([SESSION_STORAGE_KEYS.PENDING_APPROVALS]);
const snapshot = stored[SESSION_STORAGE_KEYS.PENDING_APPROVALS] as
| PendingApprovalSessionSnapshot
| undefined;

return snapshot ?? null;
}

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 Queued persistable approvals stuck after SW restart when sign-raw-tx is active

When a sign-raw-tx is the active request at SW restart time, buildPendingApprovalSessionSnapshot cannot include it (WASM objects aren't serializable) so it sets currentRequestId: null in the snapshot. If there are queued persistable approvals (e.g., a connect or transaction), they are written to session storage in pending and queue — but restorePendingApprovalsSession only calls notifyApprovalPending when currentRequestId is non-null after restore. There is no code path that promotes the head of the restored queue to currentRequestId, so those queued requests are permanently stuck: GET_PENDING_APPROVAL will return null, and the user never sees them after reloading the panel.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extension/shared/pending-approvals-session.ts
Line: 77-103

Comment:
**Queued persistable approvals stuck after SW restart when `sign-raw-tx` is active**

When a `sign-raw-tx` is the active request at SW restart time, `buildPendingApprovalSessionSnapshot` cannot include it (WASM objects aren't serializable) so it sets `currentRequestId: null` in the snapshot. If there are queued persistable approvals (e.g., a `connect` or `transaction`), they _are_ written to session storage in `pending` and `queue` — but `restorePendingApprovalsSession` only calls `notifyApprovalPending` when `currentRequestId` is non-null after restore. There is no code path that promotes the head of the restored queue to `currentRequestId`, so those queued requests are permanently stuck: `GET_PENDING_APPROVAL` will return `null`, and the user never sees them after reloading the panel.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +974 to +980
const method = msg?.payload?.method;
if (
typeof method !== 'string' ||
(!APPROVAL_PROVIDER_METHODS.has(method) && method !== 'nock_signRawTx')
) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 nock_signRawTx is hardcoded as a string literal here instead of being added to APPROVAL_PROVIDER_METHODS in side-panel.ts. If this method name ever changes, or if APPROVAL_PROVIDER_METHODS is reused in another check, the raw-tx case will silently diverge. Add nock_signRawTx to the set so the background can reference APPROVAL_PROVIDER_METHODS exclusively.

Suggested change
const method = msg?.payload?.method;
if (
typeof method !== 'string' ||
(!APPROVAL_PROVIDER_METHODS.has(method) && method !== 'nock_signRawTx')
) {
return;
}
const method = msg?.payload?.method;
if (typeof method !== 'string' || !APPROVAL_PROVIDER_METHODS.has(method)) {
return;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extension/background/index.ts
Line: 974-980

Comment:
`nock_signRawTx` is hardcoded as a string literal here instead of being added to `APPROVAL_PROVIDER_METHODS` in `side-panel.ts`. If this method name ever changes, or if `APPROVAL_PROVIDER_METHODS` is reused in another check, the raw-tx case will silently diverge. Add `nock_signRawTx` to the set so the background can reference `APPROVAL_PROVIDER_METHODS` exclusively.

```suggestion
  const method = msg?.payload?.method;
  if (typeof method !== 'string' || !APPROVAL_PROVIDER_METHODS.has(method)) {
    return;
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


{/* Content */}
<div className="flex flex-col justify-between flex-1 h-[536px]">
<div className="flex flex-col justify-between flex-1 flex-1 min-h-0">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Duplicate flex-1 Tailwind class — only one is needed.

Suggested change
<div className="flex flex-col justify-between flex-1 flex-1 min-h-0">
<div className="flex flex-col justify-between flex-1 min-h-0">
Prompt To Fix With AI
This is a comment left during a code review.
Path: extension/popup/screens/SettingsScreen.tsx
Line: 99

Comment:
Duplicate `flex-1` Tailwind class — only one is needed.

```suggestion
      <div className="flex flex-col justify-between flex-1 min-h-0">
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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?

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