Wires Phantom's own accountChanged/disconnect provider events (inject.ts) so switching accounts or disconnecting directly in Phantom's UI is detected, not just our own connect/sign calls -- relayed as unsolicited postMessage events (relay.ts's onWalletEvent) since they aren't a response to any request we made. Adds a "Sign out" button in the popup (nexa:sign-out) that clears the stored session, force-closes the WS connection via a new ws-client.ts disconnect() (distinct from reconnectNow() -- it also suppresses auto-reconnect until a new wallet connects), and asks the content script to call provider.disconnect(), which revokes Phantom's trust for the origin so the next silent connect correctly fails until the user reconnects. Fixes a real bug this surfaced: the existing "skip re-auth if a session token exists" check in background.ts only checked for *any* token, so switching Phantom accounts would have silently kept authenticating as the old wallet. Session storage now tracks which wallet it belongs to (backend-client.ts's storeSession(token, walletAddress)) so the handler can tell "already signed in" apart from "signed in as a different wallet than the one that just connected." Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YXiHuScXrjxBh7yFGAPq3B
102 lines
4.3 KiB
TypeScript
102 lines
4.3 KiB
TypeScript
import { browser } from 'wxt/browser';
|
|
import type { NexaMessage } from '@/shared/messaging';
|
|
import { callWallet, onWalletEvent } from './wallet-bridge/relay';
|
|
import { hideConnectBanner, setConnectBannerError, showConnectBanner } from './wallet-bridge/banner';
|
|
|
|
interface ConnectResult {
|
|
walletAddress: string;
|
|
}
|
|
|
|
interface SignResult {
|
|
signature: string;
|
|
}
|
|
|
|
async function reportConnected(walletAddress: string): Promise<void> {
|
|
console.debug('[nexa/wallet-connect] connected', walletAddress);
|
|
hideConnectBanner();
|
|
await browser.runtime
|
|
.sendMessage({ type: 'nexa:wallet-connected', walletAddress } satisfies NexaMessage)
|
|
.catch((err) => console.debug('[nexa/wallet-connect] failed to notify background:', err));
|
|
}
|
|
|
|
/** Wallet disconnected or switched to no account — clears the backend session (tied to the old wallet) and re-shows the connect prompt. */
|
|
async function reportDisconnected(): Promise<void> {
|
|
console.debug('[nexa/wallet-connect] disconnected');
|
|
await browser.runtime
|
|
.sendMessage({ type: 'nexa:wallet-disconnected' } satisfies NexaMessage)
|
|
.catch((err) => console.debug('[nexa/wallet-connect] failed to notify background:', err));
|
|
showConnectBanner(() => void connectWithGesture());
|
|
}
|
|
|
|
/** Real user gesture (banner button click) — required for Phantom to show its connect approval popup on a first-ever connect. */
|
|
async function connectWithGesture(): Promise<void> {
|
|
console.debug('[nexa/wallet-connect] banner clicked, calling connect()');
|
|
try {
|
|
const { walletAddress } = await callWallet<ConnectResult>('connect', {});
|
|
await reportConnected(walletAddress);
|
|
} catch (err) {
|
|
console.debug('[nexa/wallet-connect] connect() failed:', err);
|
|
setConnectBannerError(err instanceof Error ? err.message : String(err));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* `onlyIfTrusted` succeeds without any user interaction if this origin was
|
|
* already approved in a previous session — the normal case after the first
|
|
* connect. Falls back to the on-page banner (a real click) only when it isn't.
|
|
*/
|
|
async function attemptSilentConnect(): Promise<void> {
|
|
console.debug('[nexa/wallet-connect] attempting silent connect');
|
|
try {
|
|
const { walletAddress } = await callWallet<ConnectResult>('connect', { onlyIfTrusted: true });
|
|
await reportConnected(walletAddress);
|
|
} catch (err) {
|
|
console.debug('[nexa/wallet-connect] silent connect failed, showing banner:', err);
|
|
showConnectBanner(() => void connectWithGesture());
|
|
}
|
|
}
|
|
|
|
/** Wires wallet connect/sign into the page. Independent of any site adapter — runs regardless of whether a buy-button adapter matched. */
|
|
export function initWalletConnect(): void {
|
|
console.debug('[nexa/wallet-connect] init on', window.location.href);
|
|
void attemptSilentConnect();
|
|
|
|
// Phantom's own account-switch/disconnect events — not initiated by us, so
|
|
// this catches the user changing accounts (or disconnecting) directly in
|
|
// Phantom's UI, not just our own sign-out flow below.
|
|
onWalletEvent((name, walletAddress) => {
|
|
console.debug('[nexa/wallet-connect] wallet event', name, walletAddress);
|
|
if (walletAddress) {
|
|
void reportConnected(walletAddress); // switched to a different account — re-auth as it
|
|
} else {
|
|
void reportDisconnected(); // accountChanged(null) or a 'disconnect' event
|
|
}
|
|
});
|
|
|
|
browser.runtime.onMessage.addListener((message: NexaMessage, _sender, sendResponse) => {
|
|
if (message?.type === 'nexa:wallet-sign-request') {
|
|
callWallet<SignResult>('signMessage', { message: message.nonce })
|
|
.then((result) => sendResponse(result))
|
|
.catch((err) => sendResponse({ error: err instanceof Error ? err.message : String(err) }));
|
|
return true;
|
|
}
|
|
|
|
if (message?.type === 'nexa:request-wallet-connect') {
|
|
void attemptSilentConnect();
|
|
return false;
|
|
}
|
|
|
|
if (message?.type === 'nexa:wallet-disconnect-request') {
|
|
// Sign-out, initiated from the popup (see background/wallet-auth.ts).
|
|
// Phantom's disconnect() revokes this origin's trust, so the next
|
|
// onlyIfTrusted attempt correctly fails until the user connects again.
|
|
callWallet('disconnect', {})
|
|
.catch((err) => console.debug('[nexa/wallet-connect] disconnect() failed:', err))
|
|
.finally(() => void reportDisconnected());
|
|
return false;
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
}
|