You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
StellarWalletContext never attempts to restore an already-authorized Freighter connection on page load — every reload forces a fresh manual reconnect #137
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:
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.
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(()=>{letcancelled=false;(async()=>{try{constfreighter=awaitimport("@stellar/freighter-api");constallowed=awaitfreighter.isAllowed();if(cancelled||allowed.error||!allowed.isAllowed)return;constaddr=awaitfreighter.getAddress();if(cancelled||addr.error||!addr.address)return;awaitrefreshNetworkDetails(freighter);setPublicKey(addr.address);setWalletApi(freighterasunknownasFreighterWalletApi);}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.
Overview
src/context/StellarWalletContext.tsx'sStellarWalletProvidernever runs any wallet-restoration logic on mount.connect()— the only function that ever setspublicKey/walletApi— is invoked exclusively fromConnectWalletButton's click handler:There is no
useEffect(() => { ... }, [])anywhere inStellarWalletProviderthat callsfreighter.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 —isConnectedisfalse, the floating "Connect Freighter" CTA reappears,/farm's "My Earnings" section shows the disconnected empty state,/leaderboardis hidden from the nav entirely (Navbar.tsx:126, gated onisConnected) — even though Freighter itself still lists the site as allowed and would return the samepublicKeyinstantly and silently (no popup) if onlygetAddress()/isAllowed()were called proactively.Requirements
StellarWalletProvidermount, attempt a silent restoration: checkfreighter.isAllowed()(notrequestAccess(), which can prompt) and, if allowed, callfreighter.getAddress()to populatepublicKey/walletApiwithout requiring a click.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
connect(), reloading the page results inisConnected === trueand a populatedpublicKeywithout any user interaction, when Freighter still has the site authorized.freighter.isAllowed()returning{ isAllowed: true }andgetAddress()returning a real address, mountsStellarWalletProvider, and assertspublicKeybecomes populated without callingconnect()explicitly.Additional Notes
More precise references
src/context/StellarWalletContext.tsx:90-124(StellarWalletProviderbody up toconnect) — confirmed there is no mount-timeuseEffectthat calls any Freighter API; the onlyuseEffectin this file (:223-236) is scoped tovisibilitychange-triggered network-detail refresh and is itself gated onif (!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 exactisConnected()→isAllowed()→ (requestAccess()orgetAddress()) sequence that could be reused (withisAllowed()'s branch preferred, since it doesn't prompt) for silent restoration.src/components/ConnectWalletButton/ConnectWalletButton.tsx:27-37— confirmedconnect()is called from exactly one place: the button's click handler.src/components/Navbar/Navbar.tsx:124,126— confirmed/farmand/leaderboardnav 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
isAllowed()/getAddress(), notrequestAccess()—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.refreshNetworkDetails()(already used elsewhere in this file) soisNetworkMismatchis correctly populated immediately on restore, rather than only becoming accurate after the firstvisibilitychangeevent.Implementation sketch
Test/reproduction plan
@stellar/freighter-api'sisAllowedto resolve{ isAllowed: true }andgetAddressto resolve{ address: "GABC..." }; renderStellarWalletProvider; assertpublicKeyis populated without anyconnect()call.isAllowedto resolve{ isAllowed: false }; render; assertpublicKeystaysnulland no error/toast fires.isAllowedto hang indefinitely; assert the app still renders within the existing timeout window rather than blocking.Cross-references
StellarWalletContext's connection lifecycle but at different phases (initial restoration vs. ongoing staleness vs. the manual-connect UX).