Skip to content

Add sidebar mode support (#2632)#2672

Merged
piyalbasu merged 3 commits intomasterfrom
feature/sidebar-mode-feature
Apr 14, 2026
Merged

Add sidebar mode support (#2632)#2672
piyalbasu merged 3 commits 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.
piyalbasu and others added 2 commits April 13, 2026 21:38
* feat: harden sidebar mode with Firefox support, security fixes, and reliability improvements

Port all sidebar changes from sidebar-mode-audit: add Firefox sidebar_action
support, harden trust boundaries (sender validation, port origin checks,
isFromExtensionPage guard on OPEN_SIDEBAR), replace broadcast messaging with
direct port communication, add safeResolve to prevent double-resolving promises,
fix boolean coercion for isOpenSidebarByDefault, reject pending signing requests
on sidebar close, remove dead SIDEBAR_REGISTER/UNREGISTER code, and add
extension protocol guard to isSidebarMode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* security: add interstitial gate and route allowlist for sidebar signing flow

Prevent rapid-fire signing phishing in sidebar mode by showing an
interstitial screen when a new signing request arrives while the user
is already reviewing one. Also hardens the SidebarSigningListener with
a route allowlist (only known signing routes are navigable) and runtime
type guards on port messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing Portuguese translations for sidebar mode strings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove Firefox "open sidebar by default" — not supported by platform

Firefox requires sidebarAction.open() in a synchronous user gesture
handler and has no equivalent to Chrome's setPanelBehavior. Remove the
broken setPopup/onClicked approach, hide the preference toggle on
Firefox, add open_at_install: false to prevent auto-opening, and fix
boolean storage reads for isOpenSidebarByDefault.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update loadSaveSettings tests to mock booleans instead of strings

browser.storage.local preserves types, so getItem returns boolean
true/false, not string "true"/"false". Update test mocks to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: use named onRemoved handler checking popup.id and self-removing after safeResolve

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/33844927-58ec-481b-9846-80c0e28a34b4

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>

* redesign ConfirmSidebarRequest to match updated Figma spec

Left-align layout, use smaller square icon, pill-shaped buttons,
updated button variants and text per new design.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update extension/src/popup/locales/pt/translation.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update extension/src/popup/views/ConfirmSidebarRequest/index.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: track sidebar UUIDs separately and guard port/windowId clear on disconnect

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/7e15d7bf-0539-4ceb-80a3-eb9ae1ce7a9b

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>

* fix: address code review issues for sidebar signing flow

- Add sidebarQueueUuids tracking to setAllowedStatus handler so sidebar
  disconnect properly rejects the pending promise instead of hanging
- ConfirmSidebarRequest "Reject" now extracts UUID from the next route
  and calls rejectAccess so the dapp promise resolves immediately
- Ensure window.close() runs in the catch block of openSidebar() so the
  popup is closed even when the side panel API throws

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: defer sidebar disconnect cleanup to avoid rejecting during page reload

chrome.sidePanel.open() can reload the sidebar page, causing a brief
port disconnect/reconnect. The disconnect handler was immediately
rejecting all in-flight signing requests before the new port could
connect.

Use a cancellable deferred cleanup: schedule rejection on disconnect,
but cancel it if a new sidebar port connects before it fires.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update extension/src/popup/constants/metricsNames.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix: add REJECT_SIGNING_REQUEST handler to clean all queues on ConfirmSidebarRequest reject

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/da099665-1448-4e91-b2eb-4daa952a09bb

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>

* refactor: unify sidebarQueueUuids lifecycle with activeQueueUuids

Move sidebarQueueUuids population from freighterApiMessageListener
(eager, on openSigningWindow return) into the MARK_QUEUE_ACTIVE handler
in popupMessageListener (on signing view mount/unmount), matching how
activeQueueUuids is managed. Extract duplicated onWindowRemoved logic
into a shared rejectOnWindowClose helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add coverage for sidebar disconnect cleanup and MARK_QUEUE_ACTIVE

16 tests covering deferred disconnect cleanup (cancellation on
reconnect, queue rejection, stale port handling) and sidebarQueueUuids
management via MARK_QUEUE_ACTIVE.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: move sidebar port helpers to background/helpers/sidebarPort

Extract setSidebarPort, clearSidebarPort, getSidebarPort from
freighterApiMessageListener into their own module to reduce coupling.
Update all imports across source and test files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: rejectSigningRequest returns consistent empty object instead of void

Agent-Logs-Url: https://github.com/stellar/freighter/sessions/2d47d7af-dc71-4412-8251-1486f2c3b5df

Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>

* docs: add sidebar mode implementation spec

Add CLAUDE.md pointing to specs/ directory and a comprehensive
sidebar mode spec covering architecture, signing flow, Firefox
limitations, and E2E testing constraints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: track sidebar-routed UUIDs at routing time and guard disconnect cleanup

Two fixes to sidebar queue management:

1. Move sidebarQueueUuids tracking from MARK_QUEUE_ACTIVE (view mount
   time) to openSigningWindow (routing time). This ensures requests
   behind the ConfirmSidebarRequest interstitial are properly cleaned
   up if the sidebar disconnects before the signing view mounts.

2. Only schedule deferred disconnect cleanup when the disconnecting
   port is the currently active sidebar port. Previously, a stale port
   disconnecting after a newer port connected would still schedule
   cleanup, potentially rejecting requests on the live sidebar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update sidebar spec to reflect queue tracking changes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add sidebarMode property to signing view metrics

Includes isSidebarMode() in the view metrics for signTransaction,
signAuthEntry, signMessage, grantAccess, and addToken so we can
distinguish sidebar vs popup signing requests in analytics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: harden sidebar message handling per security review

- M1: Make sender required in popupMessageListener so omitting it
  fails-closed instead of bypassing the extension-page guard
- M2: Add sender.id check to sidebar port validation for
  defense-in-depth alongside the existing tab and URL checks
- M3: Add isFromExtensionPage guard to REJECT_SIGNING_REQUEST handler
- M4: Add runtime windowId type validation in OPEN_SIDEBAR handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address all findings from Cassio's code review

Important fixes:
- I2: Extract shared removeUuidFromAllQueues helper to eliminate duplicated
  queue cleanup logic between disconnect handler and rejectSigningRequest
- I3: Replace raw HTML <h1>/<div> with <Heading>/<Text> from @stellar/design-system
- I4: Add rejectOnWindowClose to requestAccess and setAllowedStatus so popup
  close rejects the dapp promise immediately instead of hanging until TTL
- I5: Add sidebar TTL fallback to submitToken for consistency with other handlers

Code quality:
- C1: Move misplaced DataStorageAccess import to top of file
- C2: Type safeResolve as unknown instead of any
- C3: Add comment explaining why SIDEBAR_NAVIGATE is a plain constant
- C4: Extract makeSafeResolve helper to replace 6 duplicated inline closures

Suggestions:
- S1: Derive SIGNING_ROUTE_PREFIXES from ALLOWED_NAV_PREFIXES
- S2: Extract SIDEBAR_DISCONNECT_DEBOUNCE_MS named constant
- S3: Guard ConfirmSidebarRequest route with SidebarOnlyRoute redirect
- S4: Use finally for window.close() in openSidebar

Nits:
- Replace hardcoded font-weight values with CSS custom properties
- Remove hardcoded font-family (inherited from design system)

Tests:
- Add rejectSigningRequest handler tests (6 cases)
- Add isValidNextRoute open-redirect validation tests (10 cases)
- Add SidebarSigningListener route allowlist tests (23 cases)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piyalbasu <6789586+piyalbasu@users.noreply.github.com>
@piyalbasu piyalbasu merged commit b035847 into master Apr 14, 2026
9 checks passed
@piyalbasu piyalbasu deleted the feature/sidebar-mode-feature branch April 14, 2026 02:13
@github-actions github-actions bot mentioned this pull request Apr 16, 2026
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