UNPKG

accounts

Version:

Tempo Accounts SDK

283 lines (244 loc) 7.9 kB
import { type ConnectedWallet, useLogin, usePrivy, useWallets } from '@privy-io/react-auth' import { useEffect, useRef, useSyncExternalStore } from 'react' import type { Address } from 'viem/accounts' import type * as Adapter from '../core/Adapter.js' import { privy as core_privy } from '../core/adapters/privy.js' /** * Creates a Privy adapter wired to `@privy-io/react-auth`. * * Mount {@link PrivyAccountsBridge} under Privy's `PrivyProvider` in the same * React tree. The bridge owns Privy's login modal, hook state, and embedded * wallet selection for this adapter. */ export function privy(options: privy.Options = {}): Adapter.Adapter { return core_privy({ ...options, client, loadAccounts: async () => await requestPrivyAccounts(), }) } /** Mounts the React boundary needed by {@link privy}. */ export function PrivyAccountsBridge() { const privy = usePrivy() const wallets = useWallets() const request = usePrivyAccountsRequest() const loading = useRef(false) const login_started = useRef(false) const login = useLogin({ onError(error) { login_started.current = false rejectPrivyAccounts(new Error(String(error))) }, }) useEffect(() => { bridge_mounts += 1 return () => { bridge_mounts = Math.max(bridge_mounts - 1, 0) if (bridge_mounts === 0) rejectPrivyAccounts( new Error('PrivyAccountsBridge unmounted before Privy returned accounts.'), ) } }, []) useEffect(() => { setPrivyReactState({ authenticated: privy.authenticated, getAccessToken: privy.getAccessToken, logout: privy.logout, ready: privy.ready, userId: privy.user?.id, wallets: wallets.wallets.filter(isEmbeddedEthereumWallet).map((wallet) => ({ address: wallet.address, getEthereumProvider: wallet.getEthereumProvider, walletIndex: wallet.walletIndex, })), walletsReady: wallets.ready, }) }, [ privy.authenticated, privy.getAccessToken, privy.logout, privy.ready, privy.user, wallets.ready, wallets.wallets, ]) useEffect(() => { if (!request || loading.current || !privy.ready) return if (!privy.authenticated) { if (!login_started.current) { login_started.current = true login.login() } return } login_started.current = false const embedded = wallets.wallets.filter(isEmbeddedEthereumWallet) if (!wallets.ready && embedded.length === 0) return loading.current = true void (async () => { try { if (embedded.length === 0) throw new Error('Privy React returned no embedded Ethereum wallet.') resolvePrivyAccounts(embedded.map((wallet) => wallet.address as Address)) } catch (error) { rejectPrivyAccounts(error instanceof Error ? error : new Error(String(error))) } finally { loading.current = false } })() }, [login, privy.authenticated, privy.ready, privy.user, request, wallets.ready, wallets.wallets]) return null } type PrivyReactWallet = { address: string getEthereumProvider: () => Promise<core_privy.EthereumProvider> | core_privy.EthereumProvider walletIndex?: number | undefined } type PrivyReactState = { authenticated: boolean getAccessToken?: (() => Promise<string | null>) | undefined logout?: (() => Promise<void>) | undefined ready: boolean userId?: string | undefined wallets: readonly PrivyReactWallet[] walletsReady: boolean } type PrivyAccountsRequest = { reject: (error: Error) => void resolve: (accounts: Exclude<core_privy.AccountSelection, void>) => void } let request: PrivyAccountsRequest | undefined let state: PrivyReactState = { authenticated: false, ready: false, wallets: [], walletsReady: false, } let bridge_mounts = 0 const listeners = new Set<() => void>() const ready_listeners = new Set<() => void>() const wallets_ready_listeners = new Set<() => void>() const client = { auth: { async logout() { await state.logout?.() }, }, embeddedWallet: { async getEthereumProvider(parameters: { entropyId: string entropyIdVerifier: string wallet: core_privy.LinkedAccount }) { await waitForWallets() const address = parameters.wallet.address if (!address) throw new Error('Privy React wallet address is required.') const wallet = state.wallets.find((wallet) => sameAddress(wallet.address, address)) if (!wallet) throw new Error(`Privy React wallet "${address}" is no longer connected.`) return await wallet.getEthereumProvider() }, }, async getAccessToken() { await waitForReady() if (!state.authenticated) return null return (await state.getAccessToken?.()) ?? null }, initialize() {}, user: { async get() { await waitForWallets() if (!state.authenticated) throw new Error('Privy React user is not authenticated.') return { user: { id: state.userId ?? 'privy-react-user', linked_accounts: state.wallets.map((wallet, index) => ({ address: wallet.address, chain_type: 'ethereum', connector_type: 'embedded', type: 'wallet', wallet_client_type: 'privy', wallet_index: wallet.walletIndex ?? index, })), }, } }, }, } satisfies core_privy.Client function setPrivyReactState(next: PrivyReactState) { state = next if (next.ready) emitReady() if (next.ready && (next.walletsReady || next.wallets.length > 0)) emitWalletsReady() } function usePrivyAccountsRequest() { return useSyncExternalStore(subscribe, getSnapshot, getSnapshot) } function requestPrivyAccounts() { if (bridge_mounts === 0) throw createMissingBridgeError() if (request) request.reject(new Error('Another Privy React request is already active.')) return new Promise<Exclude<core_privy.AccountSelection, void>>((resolve, reject) => { request = { reject, resolve } emit() }) } function resolvePrivyAccounts(accounts: Exclude<core_privy.AccountSelection, void>) { request?.resolve(accounts) request = undefined emit() } function rejectPrivyAccounts(error: Error) { request?.reject(error) request = undefined emit() } function emit() { for (const listener of listeners) listener() } function emitReady() { for (const listener of ready_listeners) listener() ready_listeners.clear() } function emitWalletsReady() { for (const listener of wallets_ready_listeners) listener() wallets_ready_listeners.clear() } function createMissingBridgeError() { return new Error('PrivyAccountsBridge must be mounted under PrivyProvider to use privy().') } function getSnapshot() { return request } function isEmbeddedEthereumWallet(wallet: ConnectedWallet) { return ( wallet.type === 'ethereum' && wallet.walletClientType === 'privy' && wallet.connectorType === 'embedded' ) } function sameAddress(a: string, b: string) { return a.toLowerCase() === b.toLowerCase() } function subscribe(listener: () => void) { listeners.add(listener) return () => listeners.delete(listener) } function waitForReady() { if (state.ready) return Promise.resolve() return new Promise<void>((resolve) => ready_listeners.add(resolve)) } async function waitForWallets() { await waitForReady() if (state.walletsReady || state.wallets.length > 0) return return new Promise<void>((resolve) => wallets_ready_listeners.add(resolve)) } export declare namespace privy { /** Options for {@link privy}. */ type Options = { /** Data URI of the provider icon. @default Black 1×1 SVG. */ icon?: `data:image/${string}` | undefined /** Display name of the provider. @default "Privy" */ name?: string | undefined /** Reverse DNS identifier. @default "io.privy" */ rdns?: string | undefined } }