import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from 'react' import { AppState, type AppStateStatus } from 'react-native' import { useAuth } from './auth-context' import { getMobileRuntimeConfig } from './native-config' import { supabase } from './supabase' import { registerCurrentDevice, type RegisteredDevice, } from '../features/devices/device-service' interface DeviceContextValue { currentDevice: RegisteredDevice | null loading: boolean error: string | null refreshCurrentDevice: () => Promise } const DeviceContext = createContext(null) const HEARTBEAT_INTERVAL_MS = 5 * 60 * 1000 export function DeviceProvider({ children }: { children: ReactNode }): React.ReactElement { const { user, purgeLocalSession } = useAuth() const [currentDevice, setCurrentDevice] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const generationRef = useRef(0) const lastHeartbeatRef = useRef(0) const revocationHandledRef = useRef(false) const handleRevocation = useCallback(async (): Promise => { if (revocationHandledRef.current) return revocationHandledRef.current = true await purgeLocalSession() }, [purgeLocalSession]) const refreshCurrentDevice = useCallback(async (): Promise => { const userId = user?.id const generation = ++generationRef.current if (!userId) { setCurrentDevice(null) setLoading(false) setError(null) return } setLoading(true) try { const device = await registerCurrentDevice(userId, getMobileRuntimeConfig()) if (generation !== generationRef.current) return setCurrentDevice(device) setError(null) lastHeartbeatRef.current = Date.now() if (device.revokedAt !== null) await handleRevocation() } catch { if (generation !== generationRef.current) return setError('device_registration_failed') } finally { if (generation === generationRef.current) setLoading(false) } }, [handleRevocation, user?.id]) useEffect(() => { revocationHandledRef.current = false void refreshCurrentDevice() return () => { generationRef.current += 1 } }, [refreshCurrentDevice, user?.id]) useEffect(() => { if (!user?.id || !currentDevice?.id) return undefined const channel = supabase .channel(`mobile-current-device:${currentDevice.id}`) .on( 'postgres_changes', { event: 'UPDATE', schema: 'public', table: 'devices', filter: `id=eq.${currentDevice.id}`, }, (payload) => { const revokedAt = (payload.new as { revoked_at?: unknown }).revoked_at if (typeof revokedAt === 'string' && Number.isFinite(Date.parse(revokedAt))) { void handleRevocation().catch(() => undefined) } }, ) .subscribe() return () => { void supabase.removeChannel(channel) } }, [currentDevice?.id, handleRevocation, user?.id]) useEffect(() => { const subscription = AppState.addEventListener('change', (state: AppStateStatus) => { if ( state === 'active' && user?.id && Date.now() - lastHeartbeatRef.current >= HEARTBEAT_INTERVAL_MS ) void refreshCurrentDevice() }) return () => subscription.remove() }, [refreshCurrentDevice, user?.id]) const value = useMemo(() => ({ currentDevice, loading, error, refreshCurrentDevice, }), [currentDevice, error, loading, refreshCurrentDevice]) return {children} } export function useDevice(): DeviceContextValue { const value = useContext(DeviceContext) if (!value) throw new Error('useDevice must be used inside DeviceProvider') return value }