Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 77 additions & 10 deletions frontend/src/providers/stacks-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,52 @@
* `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();

// Rehydrate connection on mount from @stacks/connect localStorage
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();
Expand All @@ -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]);
Expand Down
Loading