/** * Auth module — React context + hook for authentication in React Native apps. */ import React, { createContext, useContext, useState, useCallback, useEffect } from 'react'; import type { PlatformSDK } from '../core.js'; export interface AuthState { isAuthenticated: boolean; isLoading: boolean; userId: string | null; email: string | null; error: string | null; } export interface AuthContextType extends AuthState { login: (email: string, password: string) => Promise; register: (email: string, password: string, displayName: string) => Promise; loginWithGoogle: (idToken: string) => Promise; loginWithApple: (idToken: string) => Promise; logout: () => Promise; refreshSession: () => Promise; } const AuthContext = createContext(null); export function useAuth(): AuthContextType { const ctx = useContext(AuthContext); if (!ctx) throw new Error('useAuth must be used within an AuthProvider'); return ctx; } interface AuthProviderProps { sdk: PlatformSDK; children: React.ReactNode; /** Called when tokens are received — persist to secure storage */ onTokens?: (access: string, refresh: string) => void; /** Called on logout — clear secure storage */ onLogout?: () => void; } export function AuthProvider({ sdk, children, onTokens, onLogout, }: AuthProviderProps): React.JSX.Element { const [state, setState] = useState({ isAuthenticated: false, isLoading: true, userId: null, email: null, error: null, }); const handleTokenResponse = useCallback( async (res: Response) => { if (!res.ok) { const body = (await res.json().catch(() => ({ message: 'Login failed' }))) as { message?: string; }; throw new Error(body.message ?? `HTTP ${res.status}`); } const data = (await res.json()) as { accessToken?: string; refreshToken?: string; user?: { id?: string; email?: string }; }; if (data.accessToken && data.refreshToken) { onTokens?.(data.accessToken, data.refreshToken); } setState({ isAuthenticated: true, isLoading: false, userId: data.user?.id ?? null, email: data.user?.email ?? null, error: null, }); }, [onTokens] ); const login = useCallback( async (email: string, password: string) => { setState(s => ({ ...s, isLoading: true, error: null })); try { const res = await sdk.fetch('/auth/login', { method: 'POST', body: JSON.stringify({ email, password, productId: sdk.config.productId }), }); await handleTokenResponse(res); } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'Login failed'; setState(s => ({ ...s, isLoading: false, error: msg })); } }, [sdk, handleTokenResponse] ); const register = useCallback( async (email: string, password: string, displayName: string) => { setState(s => ({ ...s, isLoading: true, error: null })); try { const res = await sdk.fetch('/auth/register', { method: 'POST', body: JSON.stringify({ email, password, displayName, productId: sdk.config.productId, }), }); await handleTokenResponse(res); } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'Registration failed'; setState(s => ({ ...s, isLoading: false, error: msg })); } }, [sdk, handleTokenResponse] ); const loginWithGoogle = useCallback( async (idToken: string) => { setState(s => ({ ...s, isLoading: true, error: null })); try { const res = await sdk.fetch('/auth/oauth/google', { method: 'POST', body: JSON.stringify({ idToken }), }); await handleTokenResponse(res); } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'Google login failed'; setState(s => ({ ...s, isLoading: false, error: msg })); } }, [sdk, handleTokenResponse] ); const loginWithApple = useCallback( async (idToken: string) => { setState(s => ({ ...s, isLoading: true, error: null })); try { const res = await sdk.fetch('/auth/oauth/apple', { method: 'POST', body: JSON.stringify({ idToken }), }); await handleTokenResponse(res); } catch (e: unknown) { const msg = e instanceof Error ? e.message : 'Apple login failed'; setState(s => ({ ...s, isLoading: false, error: msg })); } }, [sdk, handleTokenResponse] ); const logout = useCallback(async () => { try { await sdk.fetch('/auth/logout', { method: 'POST' }); } catch { /* best-effort */ } onLogout?.(); setState({ isAuthenticated: false, isLoading: false, userId: null, email: null, error: null, }); }, [sdk, onLogout]); const refreshSession = useCallback(async () => { setState(s => ({ ...s, isLoading: true })); try { const res = await sdk.fetch('/auth/me'); if (res.ok) { const data = (await res.json()) as { id?: string; email?: string }; setState({ isAuthenticated: true, isLoading: false, userId: data.id ?? null, email: data.email ?? null, error: null, }); } else { setState(s => ({ ...s, isAuthenticated: false, isLoading: false })); } } catch { setState(s => ({ ...s, isLoading: false })); } }, [sdk]); useEffect(() => { refreshSession(); }, [refreshSession]); const value: AuthContextType = { ...state, login, register, loginWithGoogle, loginWithApple, logout, refreshSession, }; return React.createElement(AuthContext.Provider, { value }, children); }