import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, type ReactElement, type ReactNode, } from 'react'; import { AppState, type AppStateStatus } from 'react-native'; import type { ZyphrClient } from '@zyphr-dev/node-sdk'; import { SessionManager, type LoginParams, type SessionState, type TokenStorage, } from '@zyphr-dev/auth-core'; import { createZyphrTransport } from './transport.js'; export interface ZyphrAuthProviderProps { /** A client-safe ZyphrClient (constructed with your publishable application key). */ client: ZyphrClient; /** * Where tokens persist. Strongly recommended in production — pass a secure * store via `secureStoreAdapter(SecureStore)` (expo-secure-store) or * `asyncStorageAdapter(AsyncStorage)`. Defaults to in-memory (tokens are lost * on app restart), which is fine only for prototyping. */ storage?: TokenStorage; /** * Namespace the storage key so multiple providers can share one storage * adapter without colliding (e.g. one session per account for account * switching). The effective key becomes `${keyPrefix}zyphr.session`. Default `''`. */ keyPrefix?: string; /** Refresh this many ms before the access token expires. Default 60_000. */ refreshSkewMs?: number; /** * Refresh the session when the app returns to the foreground (default true). * A backgrounded app's scheduled refresh timer may not fire reliably, so we * also refresh on foreground to keep the token fresh. */ refreshOnForeground?: boolean; children: ReactNode; } interface ZyphrAuthContextValue { manager: SessionManager; } const ZyphrAuthContext = createContext(null); /** * Provides a reactive Zyphr auth session to a React Native app. Same session * engine as the web binding (@zyphr-dev/auth-core), plus refresh-on-foreground * via React Native's AppState. Read state with {@link useSession}, act with * {@link useAuth}. */ export function ZyphrAuthProvider({ client, storage, keyPrefix, refreshSkewMs, refreshOnForeground = true, children, }: ZyphrAuthProviderProps): ReactElement { const [manager] = useState( () => new SessionManager({ transport: createZyphrTransport(client), // Pass storage through as-is: auth-core applies the in-memory default // AND emits the dev-mode "no storage" warning when it's omitted. Don't // default it here or that warning would never fire for RN consumers. storage, keyPrefix, refreshSkewMs, }), ); const initialized = useRef(false); useEffect(() => { if (!initialized.current) { initialized.current = true; void manager.initialize(); } return () => { manager.destroy(); }; }, [manager]); // Refresh when the app returns to the foreground. useEffect(() => { if (!refreshOnForeground || !AppState?.addEventListener) return; const onChange = (next: AppStateStatus) => { if (next === 'active' && manager.getState().isAuthenticated) { void manager.refresh(); } }; const sub = AppState.addEventListener('change', onChange); return () => { // RN >= 0.65 returns a subscription with .remove(); older returns void. (sub as { remove?: () => void } | undefined)?.remove?.(); }; }, [manager, refreshOnForeground]); const value = useMemo(() => ({ manager }), [manager]); return {children}; } function useManager(): SessionManager { const ctx = useContext(ZyphrAuthContext); if (!ctx) { throw new Error('useSession/useAuth must be used within a .'); } return ctx.manager; } /** Reactive session state — re-renders on login, logout, refresh. */ export function useSession(): SessionState { const manager = useManager(); return useSyncExternalStore( (onChange) => manager.subscribe(() => onChange()), () => manager.getState(), () => manager.getState(), ); } export interface UseAuthResult { login: (params: LoginParams) => Promise; logout: () => Promise; refresh: () => Promise; } /** Auth actions bound to the provider's session manager. */ export function useAuth(): UseAuthResult { const manager = useManager(); return useMemo( () => ({ login: (params) => manager.login(params), logout: () => manager.logout(), refresh: () => manager.refresh(), }), [manager], ); }