Skip to content

StellarWalletContext never attempts to restore an already-authorized Freighter connection on page load — every reload forces a fresh manual reconnect #137

Description

@prodbycorne

Overview

src/context/StellarWalletContext.tsx's StellarWalletProvider never runs any wallet-restoration logic on mount. connect() — the only function that ever sets publicKey/walletApi — is invoked exclusively from ConnectWalletButton's click handler:

// ConnectWalletButton.tsx:27-37
const handleConnect = async () => {
  setIsConnecting(true);
  try {
    await connect();
    ...

There is no useEffect(() => { ... }, []) anywhere in StellarWalletProvider that calls freighter.isConnected()/isAllowed()/getAddress() on mount to check whether the site is already an authorized origin for Freighter (which it will be, for any returning user who connected previously and didn't explicitly revoke access in the extension). connect()'s own logic already contains exactly the right sequence to do this — isConnected()isAllowed()getAddress() (StellarWalletContext.tsx:125-203) — but it's gated entirely behind the user clicking "Connect Freighter" again. The practical effect: a user who connects once, then reloads the page (or returns in a new tab, or the SPA re-mounts for any reason) is treated as fully disconnected — isConnected is false, the floating "Connect Freighter" CTA reappears, /farm's "My Earnings" section shows the disconnected empty state, /leaderboard is hidden from the nav entirely (Navbar.tsx:126, gated on isConnected) — even though Freighter itself still lists the site as allowed and would return the same publicKey instantly and silently (no popup) if only getAddress()/isAllowed() were called proactively.

Requirements

  • On StellarWalletProvider mount, attempt a silent restoration: check freighter.isAllowed() (not requestAccess(), which can prompt) and, if allowed, call freighter.getAddress() to populate publicKey/walletApi without requiring a click.
  • This restoration must not itself trigger a Freighter permission prompt — it should be indistinguishable from a no-op if the user hasn't previously authorized the site, or if Freighter isn't installed.
  • Preserve the existing FREIGHTER_CONNECT_TIMEOUT_MS-bounded pattern (withFreighterConnectTimeout) so a hung/unresponsive Freighter extension can't hang the initial page render waiting on this silent check.

Acceptance Criteria

  • After a successful connect(), reloading the page results in isConnected === true and a populated publicKey without any user interaction, when Freighter still has the site authorized.
  • If Freighter is not installed, or the site was never authorized, or authorization was revoked, the silent restoration is a no-op — no error toast, no popup, no visible failure state — and the app renders in the normal disconnected state.
  • The silent restoration attempt is bounded by a timeout so a hung Freighter extension doesn't block or delay the rest of the app's initial render.
  • A test mocks freighter.isAllowed() returning { isAllowed: true } and getAddress() returning a real address, mounts StellarWalletProvider, and asserts publicKey becomes populated without calling connect() explicitly.

Additional Notes

More precise references

  • src/context/StellarWalletContext.tsx:90-124 (StellarWalletProvider body up to connect) — confirmed there is no mount-time useEffect that calls any Freighter API; the only useEffect in this file (:223-236) is scoped to visibilitychange-triggered network-detail refresh and is itself gated on if (!publicKey) return undefined — i.e. it only runs after a connection already exists, doing nothing to establish one.
  • src/context/StellarWalletContext.tsx:125-203 (connect) — confirmed the exact isConnected()isAllowed() → (requestAccess() or getAddress()) sequence that could be reused (with isAllowed()'s branch preferred, since it doesn't prompt) for silent restoration.
  • src/components/ConnectWalletButton/ConnectWalletButton.tsx:27-37 — confirmed connect() is called from exactly one place: the button's click handler.
  • src/components/Navbar/Navbar.tsx:124,126 — confirmed /farm and /leaderboard nav links are conditionally rendered only {isConnected && ...}, so a user who would otherwise be recognized as connected (per Freighter) sees fewer nav options after every reload until they manually reconnect.

Additional edge cases

  • This is a distinct gap from StellarWalletContext never detects a Freighter account switch while connected, leaving publicKey stale #97/StellarWalletContext never detects a Freighter account switch while connected, leaving publicKey stale #67 ("StellarWalletContext never detects a Freighter account switch while connected") — that issue is about staleness once already connected; this issue is about never even attempting to re-establish a connection that Freighter itself still recognizes, on fresh mount. A fix for one doesn't address the other.
  • Silent restoration should specifically use isAllowed()/getAddress(), not requestAccess()requestAccess() can itself trigger a permission prompt for a not-yet-authorized site, which would be a surprising, unsolicited Freighter popup on every page load for first-time visitors who happen to have Freighter installed but have never connected to SmartDrop — the opposite of the intended silent, non-intrusive restoration.
  • Consider whether the restoration should also proactively call refreshNetworkDetails() (already used elsewhere in this file) so isNetworkMismatch is correctly populated immediately on restore, rather than only becoming accurate after the first visibilitychange event.

Implementation sketch

useEffect(() => {
  let cancelled = false;
  (async () => {
    try {
      const freighter = await import("@stellar/freighter-api");
      const allowed = await freighter.isAllowed();
      if (cancelled || allowed.error || !allowed.isAllowed) return;
      const addr = await freighter.getAddress();
      if (cancelled || addr.error || !addr.address) return;
      await refreshNetworkDetails(freighter);
      setPublicKey(addr.address);
      setWalletApi(freighter as unknown as FreighterWalletApi);
    } catch {
      // Freighter not installed / unreachable — silently stay disconnected.
    }
  })();
  return () => { cancelled = true; };
}, [refreshNetworkDetails]);

Test/reproduction plan

  • Mock @stellar/freighter-api's isAllowed to resolve { isAllowed: true } and getAddress to resolve { address: "GABC..." }; render StellarWalletProvider; assert publicKey is populated without any connect() call.
  • Mock isAllowed to resolve { isAllowed: false }; render; assert publicKey stays null and no error/toast fires.
  • Mock isAllowed to hang indefinitely; assert the app still renders within the existing timeout window rather than blocking.

Cross-references

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial CampaignCampaign: Official CampaignOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignuxUser experience, interaction design, loading statesvery hardExtremely hard — deep expertise, careful design, and significant time requiredwalletFreighter wallet integration, session, and network switching

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions