Skip to content

Open the sidebar for dApp requests when "open sidebar by default" is on (Chrome) #3030

Description

@JakeUrban

Problem

Users can set Freighter to open in the sidebar by default (from #2335). This setting only changes what happens when the user clicks the toolbar icon.

When a dApp sends a request (connect, sign transaction, sign message, sign auth entry, add token), Freighter does this:

  • If the sidebar is already open, the request shows in the sidebar.
  • If the sidebar is closed, Freighter opens a new popup window. It does this even when the user prefers the sidebar.

MetaMask opens its side panel for dApp requests when the side panel is the preferred view (MetaMask/metamask-extension#38964). Freighter cannot do this yet.

Background

Sidebar mode was added in #2632 and improved in #2674. The design and known limits are in extension/specs/SIDEBAR_MODE.md. Those PRs and the spec do not discuss opening the sidebar for dApp requests when the sidebar is closed. This issue is new work.

Cause

Chrome only lets an extension call chrome.sidePanel.open() in response to a user gesture (for example, a click).

The dApp request goes from the page to the content script, then to the background. The background then runs async work (for example, buildStore(), network details, allow list check) before it calls openSigningWindow. Chrome no longer sees a user gesture at that point, so sidePanel.open() fails.

Related code:

  • extension/src/background/messageListener/freighterApiMessageListener.ts: openSigningWindow only uses the sidebar when getSidebarWindowId() is not null, which means the sidebar is already open.
  • extension/src/background/index.ts: initSidebarBehavior only calls setPanelBehavior({ openPanelOnActionClick }).
  • extension/src/contentScript/helpers/redirectMessagesToBackground.ts: forwards dApp messages to the background.

Proposed solution

Use the same method as MetaMask: a round trip through the content script that keeps the user gesture.

  1. The background gets a dApp request that needs UI. The user setting isOpenSidebarByDefault is on, and no sidebar is open.

  2. The background makes a one-time nonce for this tab. It saves the route as a "pending sidebar route". It sends a REQUEST_OPEN_SIDEBAR message with the nonce to the tab's content script.

  3. The content script checks navigator.userActivation.isActive. If it is true, the content script sends an OPEN_SIDEBAR message back with the nonce. If it is false, it does nothing.

  4. A new background listener gets OPEN_SIDEBAR. This listener must be synchronous: no await before the open call. It checks that:

    • the sender is Freighter's content script,
    • sender.tab.id matches the tab that made the request,
    • the nonce is correct and was not used before.

    Then it calls chrome.sidePanel.open({ windowId: sender.tab.windowId }).

  5. When the new sidebar connects (the onConnect handler in extension/src/background/index.ts), the background sends the pending route with SIDEBAR_NAVIGATE. It also adds the request UUID to sidebarQueueUuids, so closing the sidebar rejects the request.

  6. If the sidebar does not connect in about 500 ms, the background opens the popup window, as it does now.

Scope

  • Chrome and other Chromium browsers only.
  • Firefox: no change. sidebarAction.open() needs a synchronous user input handler, and background messages do not count (see extension/specs/SIDEBAR_MODE.md). Freighter keeps using the popup window on Firefox.
  • No change to @stellar/freighter-api. dApps do not need to change anything.

Tasks

  • Spike: confirm on current Chrome that the content script round trip keeps the user gesture and sidePanel.open() works.
  • Add the new message types as typed constants in @shared/constants/services.ts. Do not use raw strings.
  • Add the content script handler for REQUEST_OPEN_SIDEBAR.
  • Add the synchronous background listener for OPEN_SIDEBAR, with the nonce, tab and sender checks.
  • Update openSigningWindow to try the sidebar first, then fall back to the popup window. Pass the request tab from all callers.
  • Send the pending route to the sidebar when it connects.
  • Update the Preferences text so users know the setting also applies to dApp requests. Run yarn build:extension:translations.
  • Unit tests: nonce checks, wrong tab, wrong sender, timeout fallback, pending route sent on connect.
  • Manual QA on Chrome: setting on and sidebar closed, a dApp request from a click opens the sidebar. A request without a click (for example, on page load) opens the popup window.
  • Update extension/specs/SIDEBAR_MODE.md with the new flow.

Note: Playwright cannot test a real sidebar. The background rejects sidebar ports from pages that run in a tab, so in Playwright all requests use the popup window (see the "Testing" section of extension/specs/SIDEBAR_MODE.md). Playwright can still check the popup fallback.

Security notes

This change touches extension/src/background/ and extension/src/contentScript/. Without the nonce, tab and sender checks, any web page could open the Freighter sidebar after a user click. The background must only accept OPEN_SIDEBAR for a request that it started.

Acceptance criteria

  • On Chrome, with "open sidebar by default" on and the sidebar closed, a dApp request that comes from a user click opens the Freighter sidebar and shows the request.
  • If the sidebar cannot open, the popup window opens and the request still works.
  • With the setting off, behavior does not change.
  • On Firefox, behavior does not change.

References

Activity

  1. piyalbasu commented on Oct 12, 2026

    @piyalbasu
    Contributor

    Previously it looked like we weren't able to do this programmatically, so before starting, quickly determine if we can do this with our current architecture

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions