Conversation
* Add sidebar mode support - Route signing flows (grant access, sign tx, sign blob, sign auth entry) through the sidebar when it is open - Add SIDEBAR_REGISTER/UNREGISTER handlers to track the active sidebar window - Add SidebarSigningListener component so the sidebar can receive and navigate to signing requests - Add "Sidebar mode" button in AccountHeader menu to open the sidebar manually - Add openSidebar() helper for Chrome (sidePanel API) and Firefox (sidebarAction) - Fix isFullscreenMode to exclude sidebar mode - Add "Open sidebar mode by default" toggle in Settings > Preferences, persisted to local storage and applied on background startup via chrome.sidePanel.setPanelBehavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix sidebar mode bugs: signing window, cross-browser compat, promise timeouts, feature detection, and popup close behavior - Fix setAllowedStatus bypassing openSigningWindow, causing signing windows to not appear when sidebar mode is disabled - Replace chrome.storage.session with in-memory variable for cross-browser (Firefox) compatibility - Add error handling for setPanelBehavior() calls - Add 5-minute timeout to reject hanging Promises when sidebar closes mid-signing - Replace Arc browser UA sniffing with feature detection (typeof globalThis.chrome?.sidePanel?.open) - Fix popup not closing when opening sidebar mode - Add missing isOpenSidebarByDefault to SignTransaction test mocks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix signing popup not appearing after sidebar is closed Replace unreliable beforeunload-based SIDEBAR_UNREGISTER with a long-lived port connection. Background now clears sidebarWindowId via onDisconnect when the sidebar closes, preventing stale IDs from routing signing requests through a closed sidebar instead of opening a popup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix flaky e2e test: restore test.slow() for View failed transaction The test.slow() call was removed during the 5.38.0 refactoring, reducing the timeout from 45s to 15s. This causes intermittent CI failures when network stubs take longer to respond under sequential execution (IS_INTEGRATION_MODE=true with workers=1). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix e2e account history stubs to use context.route() instead of page.route() Chrome extension service workers make fetch requests outside the page context, so page.route() does not intercept them. Switching to context.route() ensures the account history stubs are applied at the browser context level, which covers both page and service worker requests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix View failed transaction test: use addInitScript to mock fetch Playwright's context.route()/page.route() do not reliably intercept fetch requests from Chrome extension popup pages in CI headless mode. Instead, use page.addInitScript() to patch window.fetch directly in the extension page's JavaScript environment. This approach injects code before any page scripts run and is guaranteed to intercept the account-history fetch calls regardless of the network interception mechanism. Keep context.route() as a network-level fallback for environments where route interception does work. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Piyal Basu <pbasu235@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds “sidebar mode” support across the extension, including routing signing flows to an open sidebar, a user-facing toggle to open the sidebar by default (Chromium sidePanel behavior), and several cross-browser / test-stability fixes (notably around Playwright request interception in extension contexts).
Changes:
- Introduces sidebar-mode runtime + navigation plumbing (sidebar registration, sidebar listener, and signing-flow routing).
- Adds the “Open sidebar mode by default” preference persisted to storage and applied on background startup (Chromium).
- Stabilizes e2e account-history stubs and fixes a flaky account history test via context-level routing and fetch patching.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| extension/src/popup/views/Preferences/index.tsx | Adds new “Open sidebar mode by default” toggle in Preferences UI. |
| extension/src/popup/views/IntegrationTest.tsx | Updates integration test settings payload to include new setting. |
| extension/src/popup/views/tests/Swap.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/SignTransaction.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/ManageAssets.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/GrantAccess.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/AddFunds.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/AccountHistory.test.tsx | Updates mocked settings/state to include new setting. |
| extension/src/popup/views/tests/AccountCreator.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/views/tests/Account.test.tsx | Updates mocked settings to include new setting. |
| extension/src/popup/Router.tsx | Mounts SidebarSigningListener only in sidebar mode. |
| extension/src/popup/helpers/navigate.ts | Adds openSidebar() helper (Chrome sidePanel + Firefox sidebarAction). |
| extension/src/popup/helpers/isSidebarMode.ts | Adds helper to detect sidebar mode via URL query param. |
| extension/src/popup/helpers/isFullscreenMode.ts | Excludes sidebar mode from fullscreen detection. |
| extension/src/popup/ducks/settings.ts | Adds new setting to state, thunk payloads, and selector. |
| extension/src/popup/components/SidebarSigningListener/index.tsx | New component: port connection + message-driven navigation for sidebar signing flows. |
| extension/src/popup/components/account/AccountHeader/index.tsx | Adds “Sidebar mode” menu action to open sidebar. |
| extension/src/constants/localStorageTypes.ts | Adds local storage key for open-by-default sidebar behavior. |
| extension/src/background/messageListener/popupMessageListener.ts | Adds sidebar window ID tracking and OPEN_SIDEBAR / register/unregister handling. |
| extension/src/background/messageListener/handlers/saveSettings.ts | Persists new setting and applies Chromium panel behavior immediately. |
| extension/src/background/messageListener/handlers/loadSettings.ts | Loads new setting from storage. |
| extension/src/background/messageListener/freighterApiMessageListener.ts | Routes signing windows to sidebar when registered; adds TTL fallback behavior. |
| extension/src/background/index.ts | Initializes sidebar behavior and connection listener on background startup. |
| extension/public/static/manifest/v3.json | Adds sidePanel permission and default side_panel path. |
| extension/public/static/manifest/v2.json | Adds sidebar_action configuration for Firefox (manifest v2). |
| extension/public/background.ts | Wires new background initializers. |
| extension/e2e-tests/helpers/stubs.ts | Switches account-history stubs to BrowserContext routing. |
| extension/e2e-tests/accountHistory.test.ts | Stabilizes account-history interception; restores slow timeout and adds fetch patching. |
| @shared/constants/services.ts | Adds service types for sidebar operations. |
| @shared/api/types/types.ts | Extends Settings/Response types with isOpenSidebarByDefault. |
| @shared/api/types/message-request.ts | Extends SaveSettingsMessage and adds sidebar message types. |
| @shared/api/internal.ts | Extends internal saveSettings API to include new setting. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export const openSidebar = async () => { | ||
| try { | ||
| if ((browser as any).sidebarAction) { | ||
| // Firefox | ||
| await (browser as any).sidebarAction.open(); | ||
| } else { | ||
| // Chrome — must be called in user gesture context before closing popup | ||
| const win = await chrome.windows.getCurrent(); | ||
| await chrome.sidePanel.setOptions({ | ||
| path: "index.html?mode=sidebar", | ||
| enabled: true, | ||
| }); | ||
| await chrome.sidePanel.open({ windowId: win.id! }); | ||
| } | ||
| } catch (e) { | ||
| console.error("Failed to open sidebar:", e); | ||
| } | ||
| window.close(); | ||
| }; |
There was a problem hiding this comment.
openSidebar() always calls window.close() even if opening the sidebar fails (caught error). This can leave the user with no UI if sidePanel/sidebarAction is unavailable or the call rejects. Only close the popup after a successful open (or after confirming the API exists and the request was dispatched successfully).
| {typeof globalThis.chrome?.sidePanel?.open === "function" && ( | ||
| <div | ||
| className="AccountHeader__options__item" | ||
| onClick={() => openSidebar()} | ||
| > | ||
| <Text as="div" size="sm" weight="medium"> | ||
| {t("Sidebar mode")} | ||
| </Text> | ||
| <div className="AccountHeader__options__item__icon"> | ||
| <Icon.LayoutRight /> | ||
| </div> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
The sidebar UI entry is gated on globalThis.chrome?.sidePanel?.open, so Firefox (which uses browser.sidebarAction.open()) will never show the “Sidebar mode” menu item even though openSidebar() supports Firefox. Use feature detection that matches openSidebar() (e.g., check for sidebarAction.open OR sidePanel.open).
| {typeof globalThis.chrome?.sidePanel?.open === "function" && ( | ||
| <div className="Preferences--section"> | ||
| <div className="Preferences--section--title"> | ||
| <span>{t("Open sidebar mode by default")} </span> | ||
| <div className="Preferences--toggle"> | ||
| <Toggle | ||
| fieldSize="sm" | ||
| checked={initialValues.isOpenSidebarByDefaultValue} | ||
| customInput={<Field />} | ||
| id="isOpenSidebarByDefaultValue" | ||
| /> | ||
| </div> | ||
| </div> | ||
| <span className="Preferences--section--subtitle"> | ||
| {t( | ||
| "Open Freighter in sidebar instead of popup when clicking the extension icon", | ||
| )} | ||
| </span> | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
The “Open sidebar mode by default” preference is currently only rendered when chrome.sidePanel.open exists, which hides the setting on Firefox even though the extension declares a sidebar_action. If the intention is Chromium-only behavior, consider clarifying in UI text; otherwise, gate on Firefox sidebarAction support as well so the option is visible where sidebar mode is supported.
| const openSigningWindow = async (hashRoute: string, width?: number) => { | ||
| const sidebarWindowId = getSidebarWindowId(); | ||
| if (sidebarWindowId !== null) { | ||
| browser.runtime.sendMessage({ type: SIDEBAR_NAVIGATE, route: hashRoute }); | ||
| try { | ||
| if ((browser as any).sidebarAction) { | ||
| // Firefox | ||
| await (browser as any).sidebarAction.open(); | ||
| } else { | ||
| // Chrome and other Chromium browsers | ||
| await chrome.sidePanel.open({ windowId: sidebarWindowId }); | ||
| } | ||
| } catch (_) { | ||
| // ignore if unavailable | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
openSigningWindow(): if a sidebarWindowId is set but opening the side panel/sidebar fails, the function still returns null (sidebar mode) and callers will wait for the TTL timeout instead of falling back to opening a popup. Consider catching failures and falling back to browser.windows.create (and/or clearing sidebarWindowId) so signing requests still appear immediately when sidebar cannot be opened.
| ...WINDOW_SETTINGS, | ||
| width: 400, | ||
| }); | ||
| const popup = await openSigningWindow(`/grant-access?${encodeOrigin}`); |
There was a problem hiding this comment.
requestAccess(): grant-access popups previously used a width of 400, but this call no longer passes the width argument to openSigningWindow, which changes the popup dimensions. Pass the same explicit width (400) here to preserve the existing UI layout.
| const popup = await openSigningWindow(`/grant-access?${encodeOrigin}`); | |
| const popup = await openSigningWindow(`/grant-access?${encodeOrigin}`, 400); |
| setSidebarWindowId, | ||
| } from "./messageListener/popupMessageListener"; | ||
| import { freighterApiMessageListener } from "./messageListener/freighterApiMessageListener"; | ||
| import { SIDEBAR_PORT_NAME } from "popup/components/SidebarSigningListener"; |
There was a problem hiding this comment.
background/index.ts imports SIDEBAR_PORT_NAME from a popup React component module. This can pull react/react-router-dom (and potentially DOM-dependent code) into the background bundle, risking runtime issues and bloating the service worker. Move SIDEBAR_PORT_NAME to a shared constants module (e.g., @shared/constants/sidebar) and import it from both background and popup.
| import { SIDEBAR_PORT_NAME } from "popup/components/SidebarSigningListener"; | |
| import { SIDEBAR_PORT_NAME } from "@shared/constants/sidebar"; |
| export const stubAccountHistory = async (context: BrowserContext) => { | ||
| await context.route("**/account-history/**", async (route) => { | ||
| const json = [ |
There was a problem hiding this comment.
stubAccountHistory() now accepts a BrowserContext, but there are still many call sites in e2e tests passing a Page (e.g., addAsset.test.ts, hideCollectible.test.ts, freighterApiIntegration.test.ts). This will cause TypeScript compile/runtime errors. Update all callers to pass the test BrowserContext instead of the Page.
| error: "", | ||
| isDataSharingAllowed: false, | ||
| isMemoValidationEnabled: false, | ||
| isHideDustEnabled: true, | ||
| isOpenSidebarByDefault: false, | ||
| settingsState: SettingsState.SUCCESS, | ||
| isSorobanPublicEnabled: false, |
There was a problem hiding this comment.
Several newly added isOpenSidebarByDefault properties are mis-indented (e.g., this line). With eslint-plugin-prettier enabled, this is likely to fail lint/format checks. Run Prettier (or fix indentation) for these added lines throughout the file.
| networkDetails: TESTNET_NETWORK_DETAILS, | ||
| networksList: DEFAULT_NETWORKS, | ||
| isHideDustEnabled: true, | ||
| isOpenSidebarByDefault: false, |
There was a problem hiding this comment.
The newly added isOpenSidebarByDefault field is mis-indented here; with prettier-as-eslint this can fail CI formatting checks. Align indentation with surrounding object literal properties.
| isOpenSidebarByDefault: false, | |
| isOpenSidebarByDefault: false, |
| isDataSharingAllowed: false, | ||
| isMemoValidationEnabled: false, | ||
| isHideDustEnabled: true, | ||
| isOpenSidebarByDefault: false, |
There was a problem hiding this comment.
The added isOpenSidebarByDefault property is mis-indented here (and likely in other added mocks in this file). Please run Prettier / fix formatting to match surrounding properties to avoid lint failures.
| isOpenSidebarByDefault: false, | |
| isOpenSidebarByDefault: false, |
Replace unreliable beforeunload-based SIDEBAR_UNREGISTER with a long-lived port connection. Background now clears sidebarWindowId via onDisconnect when the sidebar closes, preventing stale IDs from routing signing requests through a closed sidebar instead of opening a popup.
The test.slow() call was removed during the 5.38.0 refactoring, reducing the timeout from 45s to 15s. This causes intermittent CI failures when network stubs take longer to respond under sequential execution (IS_INTEGRATION_MODE=true with workers=1).
Chrome extension service workers make fetch requests outside the page context, so page.route() does not intercept them. Switching to context.route() ensures the account history stubs are applied at the browser context level, which covers both page and service worker requests.
Playwright's context.route()/page.route() do not reliably intercept fetch requests from Chrome extension popup pages in CI headless mode. Instead, use page.addInitScript() to patch window.fetch directly in the extension page's JavaScript environment. This approach injects code before any page scripts run and is guaranteed to intercept the account-history fetch calls regardless of the network interception mechanism.
Keep context.route() as a network-level fallback for environments where route interception does work.