import type { Session, User } from '@supabase/supabase-js'; import { auth, crypto } from '@chat-app/shared'; import type { DeviceRecord } from '@chat-app/shared/auth'; import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { Platform } from 'react-native'; import { secretStore } from './secretStore'; import { supabase } from './supabase'; // Locally-stored secrets keyed by stable names. Mirrors the desktop // convention so the migration tests (later) can compare snapshots. const KEY_DEVICE_ID = 'device.id'; const KEY_DEVICE_PRIVKEY = 'device.privateKey'; interface AuthContextValue { session: Session | null; user: User | null; device: DeviceRecord | null; ownPrivateKey: Uint8Array | null; loading: boolean; signOut: () => Promise; } const Ctx = createContext(null); export function useAuth(): AuthContextValue { const v = useContext(Ctx); if (!v) throw new Error('useAuth() called outside '); return v; } export function AuthProvider({ children }: { children: React.ReactNode }) { const [session, setSession] = useState(null); const [device, setDevice] = useState(null); const [ownPrivateKey, setOwnPrivateKey] = useState(null); const [loading, setLoading] = useState(true); // Resolve or create the device record for this install given an active // session. Stores the private key in expo-secure-store on first run. const ensureDevice = useCallback(async (_currentSession: Session): Promise => { const savedDeviceId = await secretStore.getSecret(KEY_DEVICE_ID); const savedPrivKey = await secretStore.getSecret(KEY_DEVICE_PRIVKEY); if (savedDeviceId && savedPrivKey) { const devices = await auth.listOwnDevices(supabase); const deviceIdStr = new TextDecoder().decode(savedDeviceId); const match = devices.find((d) => d.id === deviceIdStr); if (match) { setDevice(match); setOwnPrivateKey(savedPrivKey); return; } // Stored id no longer matches any device on the server (revoked, // wiped). Fall through to register a fresh one. } const backend = crypto.getCryptoBackend(); const kp = backend.generateKeyPair(); const platform = Platform.OS === 'ios' ? 'ios' : Platform.OS === 'android' ? 'android' : 'linux'; const record = await auth.registerDevice(supabase, { name: `Netralax Mobile (${Platform.OS})`, platform, publicKey: kp.publicKey, }); await secretStore.setSecret(KEY_DEVICE_ID, new TextEncoder().encode(record.id)); await secretStore.setSecret(KEY_DEVICE_PRIVKEY, kp.privateKey); setDevice(record); setOwnPrivateKey(kp.privateKey); }, []); useEffect(() => { let cancelled = false; void (async () => { const { data } = await supabase.auth.getSession(); if (cancelled) return; setSession(data.session); if (data.session) { try { await ensureDevice(data.session); } catch (err) { console.warn('[auth] ensureDevice failed', err); } } setLoading(false); })(); const { data: sub } = supabase.auth.onAuthStateChange((_event, nextSession) => { setSession(nextSession); if (!nextSession) { setDevice(null); setOwnPrivateKey(null); } else { void ensureDevice(nextSession).catch((err) => console.warn('[auth] ensureDevice (state change) failed', err), ); } }); return () => { cancelled = true; sub.subscription.unsubscribe(); }; }, [ensureDevice]); const signOut = useCallback(async (): Promise => { await supabase.auth.signOut(); await secretStore.removeSecret(KEY_DEVICE_ID); await secretStore.removeSecret(KEY_DEVICE_PRIVKEY); setDevice(null); setOwnPrivateKey(null); }, []); const value: AuthContextValue = { session, user: session?.user ?? null, device, ownPrivateKey, loading, signOut, }; return {children}; }