diff --git a/frontend/src/providers/stacks-provider.tsx b/frontend/src/providers/stacks-provider.tsx index e2e1cfb..dde0d82 100644 --- a/frontend/src/providers/stacks-provider.tsx +++ b/frontend/src/providers/stacks-provider.tsx @@ -9,12 +9,21 @@ * `StacksProvider` object, which left the previous `showConnect`/ * `openContractCall` calls hanging with no popup. * + * Connect is deliberately stateless-first: we clear any cached approval and + * force the wallet chooser on every connect, so switching accounts in the + * wallet (e.g. sender -> recipient) always takes effect. Cached data made + * reconnects silently return the previous account. + * * Ref: https://www.npmjs.com/package/@stacks/connect */ import { type ReactNode, useEffect, useCallback } from "react"; +import { toast } from "sonner"; import { useWalletStore } from "@/stores/wallet-store"; +/** Stacks addresses start with S + P/M (mainnet) or T/N (testnet). */ +const STX_ADDRESS_RE = /^S[PMTN]/; + export function StacksProvider({ children }: { children: ReactNode }) { const { setAddress, disconnect } = useWalletStore(); @@ -22,11 +31,30 @@ export function StacksProvider({ children }: { children: ReactNode }) { useEffect(() => { (async () => { try { + // Purge pre-v8 leftovers. Returning visitors carry the legacy + // UserSession blob, and our persisted store may claim "connected" + // from that era — without a matching v8 session the UI would lie + // and wallet actions would misbehave. + try { + localStorage.removeItem("blockstack-session"); + localStorage.removeItem("blockstack-gaia-hub-config"); + } catch { + /* storage unavailable — nothing to clean */ + } + const { isConnected, getLocalStorage } = await import("@stacks/connect"); if (isConnected()) { - const address = getLocalStorage()?.addresses.stx[0]?.address; - if (address) setAddress(address); + const stx = getLocalStorage()?.addresses.stx.find((a) => + STX_ADDRESS_RE.test(a.address) + ); + if (stx) { + setAddress(stx.address); + return; + } } + // No live v8 session: make the UI agree (clears any stale + // persisted "connected" state from before the migration). + disconnect(); } catch { // Stale or incompatible connect data — start fresh disconnect(); @@ -44,16 +72,55 @@ export function useStacksAuth() { const handleConnect = useCallback(async () => { setConnecting(true); try { - const { connect, getLocalStorage } = await import("@stacks/connect"); - await connect(); // opens the wallet chooser + approval popup - const address = getLocalStorage()?.addresses.stx[0]?.address; - if (address) { - setAddress(address); - } else { + const { + connect, + disconnect: walletDisconnect, + JsonRpcError, + } = await import("@stacks/connect"); + + // Drop any cached approval/addresses so the wallet is always asked + // fresh — otherwise a reconnect can silently return the previously + // approved account even after the user switched accounts in the wallet. + walletDisconnect(); + + const res = await connect({ forceWalletSelect: true }).catch( + (err: unknown) => { + // User closed the chooser or rejected in the wallet — not an error + if ( + err instanceof JsonRpcError && + (err.code === -32000 || err.code === -31001) + ) { + return null; + } + throw err; + } + ); + + if (!res) { setConnecting(false); + return; } - } catch { - // User closed the popup or wallet rejected — back to idle + + // Use the wallet's fresh response (authoritative for the currently + // selected account), never merged localStorage. + const stx = res.addresses.find((a) => STX_ADDRESS_RE.test(a.address)); + if (!stx) { + toast.error( + "The wallet returned no Stacks address. Switch to a Stacks account in your wallet and try again." + ); + setConnecting(false); + return; + } + + setAddress(stx.address); + toast.success( + `Connected ${stx.address.slice(0, 6)}…${stx.address.slice(-4)}` + ); + } catch (err) { + console.error("[wallet connect]", err); + toast.error( + "Couldn't reach the wallet. Unlock the extension, then try again." + ); setConnecting(false); } }, [setAddress, setConnecting]);