Skip to content

Add sidebar mode support (#2632)#2672

Open
piyalbasu wants to merge 1 commit intomasterfrom
feature/sidebar-mode-feature
Open

Add sidebar mode support (#2632)#2672
piyalbasu wants to merge 1 commit intomasterfrom
feature/sidebar-mode-feature

Conversation

@piyalbasu
Copy link
Copy Markdown
Contributor

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

  • 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).

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

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


* 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>
Copilot AI review requested due to automatic review settings April 1, 2026 16:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.

Comment on lines +18 to +36
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();
};
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Comment on lines +184 to +196
{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>
)}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Comment on lines +185 to +204
{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>
)}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +89
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;
}
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
...WINDOW_SETTINGS,
width: 400,
});
const popup = await openSigningWindow(`/grant-access?${encodeOrigin}`);
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
const popup = await openSigningWindow(`/grant-access?${encodeOrigin}`);
const popup = await openSigningWindow(`/grant-access?${encodeOrigin}`, 400);

Copilot uses AI. Check for mistakes.
setSidebarWindowId,
} from "./messageListener/popupMessageListener";
import { freighterApiMessageListener } from "./messageListener/freighterApiMessageListener";
import { SIDEBAR_PORT_NAME } from "popup/components/SidebarSigningListener";
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
import { SIDEBAR_PORT_NAME } from "popup/components/SidebarSigningListener";
import { SIDEBAR_PORT_NAME } from "@shared/constants/sidebar";

Copilot uses AI. Check for mistakes.
Comment on lines +842 to 844
export const stubAccountHistory = async (context: BrowserContext) => {
await context.route("**/account-history/**", async (route) => {
const json = [
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines 330 to 336
error: "",
isDataSharingAllowed: false,
isMemoValidationEnabled: false,
isHideDustEnabled: true,
isOpenSidebarByDefault: false,
settingsState: SettingsState.SUCCESS,
isSorobanPublicEnabled: false,
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
networkDetails: TESTNET_NETWORK_DETAILS,
networksList: DEFAULT_NETWORKS,
isHideDustEnabled: true,
isOpenSidebarByDefault: false,
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
isOpenSidebarByDefault: false,
isOpenSidebarByDefault: false,

Copilot uses AI. Check for mistakes.
isDataSharingAllowed: false,
isMemoValidationEnabled: false,
isHideDustEnabled: true,
isOpenSidebarByDefault: false,
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
isOpenSidebarByDefault: false,
isOpenSidebarByDefault: false,

Copilot uses AI. Check for mistakes.
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