-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Context
When a user has a connected wallet and refreshes the browser, the UI briefly flashes a "disconnected" state (e.g., the "Connect Wallet" prompt on the offramp page, or a missing address in the navbar) before snapping back to the connected state. This happens because StellarWalletProvider initializes address as null on line 51, then restores the session from localStorage inside a useEffect on line 63 — which only runs after the first render. Every component that reads isConnected (derived from !!address) sees false during that initial render, causing the flash.
This breaks user experience: the page visibly flickers between two states, layout shifts occur as components conditionally render based on connection status, and on pages with wallet guards (like offramp), users momentarily see the full "Connect Wallet" prompt before the content appears.
What Success Looks Like
- Refreshing the page with a previously connected wallet shows no flash of disconnected UI
- The wallet address appears immediately in the navbar without a flicker
- Pages with wallet guards (offramp, and future guards on distribution/payment-stream) render the correct content on the first paint
- There is no layout shift caused by the connection state changing from false → true
Implementation Guidance
The root cause is in apps/web/src/providers/StellarWalletProvider.tsx:
- Line 51:
useState<string | null>(null)— address starts as null - Line 63-83:
useEffectrestores from localStorage — runs after first render
Approach A: Lazy state initialization (Recommended)
Initialize useState with a function that reads from localStorage synchronously on the first render. This avoids the two-render problem entirely:
const [address, setAddress] = useState<string | null>(() => {
if (typeof window === 'undefined') return null;
return localStorage.getItem("stellar_wallet_address");
});
const [selectedWalletId, setSelectedWalletId] = useState<WalletId | null>(() => {
if (typeof window === 'undefined') return null;
return localStorage.getItem("stellar_wallet_id");
});The useEffect still handles kit initialization and offrampService.syncWallet(), but the address/walletId are already available on the first render.
Approach B: Add an isInitializing state
Add an isInitializing flag that starts as true and becomes false after the useEffect runs. Consumers would check isInitializing before rendering disconnected UI. This is simpler but still causes a brief loading state.
Approach A is preferred because it eliminates the flash entirely with minimal code change. The typeof window guard handles SSR safety.
Files to modify
apps/web/src/providers/StellarWalletProvider.tsx— the core fix- Optionally add
isInitializingtoWalletContextType(line 20-34) if Approach B is chosen, so consumers can show a skeleton instead of disconnected UI