|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { useEffect, useRef } from 'react'; |
| 4 | + |
| 5 | +import { createClient } from '@bitcode/supabase/ssr/client'; |
| 6 | + |
| 7 | +import { mutateUserData } from '@/hooks/useUserData'; |
| 8 | +import { |
| 9 | + BITCODE_LOCAL_WALLET_EVENT, |
| 10 | + readLocalBitcodeWalletIdentity, |
| 11 | + writeLocalBitcodeWalletIdentity, |
| 12 | + type BitcodeWalletBindingStatus, |
| 13 | + type LocalBitcodeWalletIdentity, |
| 14 | +} from '@/lib/bitcode-wallet-local'; |
| 15 | +import { bitcodeQaTelemetry, compactBitcodeAddress } from '@/lib/bitcode-qa-telemetry'; |
| 16 | + |
| 17 | +function canPersistWalletIdentity(identity: LocalBitcodeWalletIdentity | null): identity is LocalBitcodeWalletIdentity { |
| 18 | + return Boolean( |
| 19 | + identity && |
| 20 | + identity.persistence !== 'server' && |
| 21 | + identity.proofKind === 'bitcoin_message_signature' && |
| 22 | + identity.message && |
| 23 | + identity.signature, |
| 24 | + ); |
| 25 | +} |
| 26 | + |
| 27 | +function readPersistedStatus(payload: unknown, fallback: BitcodeWalletBindingStatus): BitcodeWalletBindingStatus { |
| 28 | + const status = (payload as any)?.walletConnectionStatus?.verificationState; |
| 29 | + return status === 'verified' || status === 'manual' || status === 'pending' ? status : fallback; |
| 30 | +} |
| 31 | + |
| 32 | +function readPersistedAt(payload: unknown, fallback: string) { |
| 33 | + const connectedAt = (payload as any)?.walletConnectionStatus?.metadata?.connectedAt; |
| 34 | + return typeof connectedAt === 'string' && connectedAt.trim() ? connectedAt.trim() : fallback; |
| 35 | +} |
| 36 | + |
| 37 | +export default function WalletSessionPersistenceBridge() { |
| 38 | + const inFlightKeyRef = useRef<string | null>(null); |
| 39 | + |
| 40 | + useEffect(() => { |
| 41 | + let cancelled = false; |
| 42 | + |
| 43 | + const persistLocalWalletIdentity = async (reason: string) => { |
| 44 | + const identity = readLocalBitcodeWalletIdentity(); |
| 45 | + if (!canPersistWalletIdentity(identity)) return; |
| 46 | + |
| 47 | + const persistenceKey = `${identity.provider}:${identity.address}:${identity.signature}`; |
| 48 | + if (inFlightKeyRef.current === persistenceKey) return; |
| 49 | + inFlightKeyRef.current = persistenceKey; |
| 50 | + |
| 51 | + try { |
| 52 | + const supabase = createClient(); |
| 53 | + const existing = await supabase.auth.getUser(); |
| 54 | + if (cancelled || !existing.data.user) { |
| 55 | + inFlightKeyRef.current = null; |
| 56 | + return; |
| 57 | + } |
| 58 | + |
| 59 | + bitcodeQaTelemetry('info', 'wallet-session', 'persist-start', { |
| 60 | + reason, |
| 61 | + provider: identity.provider, |
| 62 | + address: compactBitcodeAddress(identity.address), |
| 63 | + }); |
| 64 | + |
| 65 | + const response = await fetch('/api/wallet/authenticate', { |
| 66 | + method: 'POST', |
| 67 | + headers: { 'Content-Type': 'application/json' }, |
| 68 | + body: JSON.stringify({ |
| 69 | + address: identity.address, |
| 70 | + provider: identity.provider, |
| 71 | + network: identity.network, |
| 72 | + message: identity.message, |
| 73 | + signature: identity.signature, |
| 74 | + proofKind: identity.proofKind, |
| 75 | + paymentAddress: identity.paymentAddress, |
| 76 | + authAddress: identity.authAddress, |
| 77 | + addressType: identity.addressType, |
| 78 | + connectedAt: identity.connectedAt, |
| 79 | + issuedAt: identity.connectedAt, |
| 80 | + }), |
| 81 | + }); |
| 82 | + const payload = await response.json().catch(() => null); |
| 83 | + |
| 84 | + if (!response.ok) { |
| 85 | + inFlightKeyRef.current = null; |
| 86 | + bitcodeQaTelemetry('warn', 'wallet-session', 'persist-failed', { |
| 87 | + status: response.status, |
| 88 | + error: typeof payload?.error === 'string' ? payload.error : null, |
| 89 | + }); |
| 90 | + return; |
| 91 | + } |
| 92 | + |
| 93 | + if (cancelled) return; |
| 94 | + |
| 95 | + writeLocalBitcodeWalletIdentity({ |
| 96 | + ...identity, |
| 97 | + status: readPersistedStatus(payload, identity.status), |
| 98 | + connectedAt: readPersistedAt(payload, identity.connectedAt), |
| 99 | + persistence: 'server', |
| 100 | + }); |
| 101 | + await mutateUserData(); |
| 102 | + bitcodeQaTelemetry('info', 'wallet-session', 'persist-success', { |
| 103 | + provider: identity.provider, |
| 104 | + address: compactBitcodeAddress(identity.address), |
| 105 | + }); |
| 106 | + } catch (error) { |
| 107 | + inFlightKeyRef.current = null; |
| 108 | + bitcodeQaTelemetry('warn', 'wallet-session', 'persist-error', { |
| 109 | + message: error instanceof Error ? error.message : String(error), |
| 110 | + }); |
| 111 | + } |
| 112 | + }; |
| 113 | + |
| 114 | + void persistLocalWalletIdentity('mount'); |
| 115 | + |
| 116 | + const handleWalletChange = () => { |
| 117 | + void persistLocalWalletIdentity('local-wallet-change'); |
| 118 | + }; |
| 119 | + const handleFocus = () => { |
| 120 | + void persistLocalWalletIdentity('window-focus'); |
| 121 | + }; |
| 122 | + |
| 123 | + window.addEventListener(BITCODE_LOCAL_WALLET_EVENT, handleWalletChange); |
| 124 | + window.addEventListener('focus', handleFocus); |
| 125 | + return () => { |
| 126 | + cancelled = true; |
| 127 | + window.removeEventListener(BITCODE_LOCAL_WALLET_EVENT, handleWalletChange); |
| 128 | + window.removeEventListener('focus', handleFocus); |
| 129 | + }; |
| 130 | + }, []); |
| 131 | + |
| 132 | + return null; |
| 133 | +} |
0 commit comments