/** * BUS-Tickets - Auth Context * Copyright (c) 2024-2026 IT Enterprise */ import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react'; import { Platform } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useApi } from './ApiContext'; import type { User, AuthTokens } from '@/types'; // Biometric authentication is disabled in this build // expo-local-authentication requires native module compilation interface StoredAuth { accessToken: string; refreshToken: string; expiresAt: number; } interface AuthContextType { user: User | null; isLoading: boolean; isAuthenticated: boolean; signIn: (email: string, password: string) => Promise; signUp: (data: { email: string; password: string; name: string; phone?: string }) => Promise; signInWithOAuth: (provider: 'google' | 'facebook' | 'apple', idToken: string) => Promise; signInWithOTP: (phone: string, code: string) => Promise; signOut: () => Promise; requestOTP: (email?: string, phone?: string) => Promise; checkBiometric: () => Promise; authenticateWithBiometric: () => Promise; refreshUser: () => Promise; } const AuthContext = createContext(undefined); const TOKEN_KEY = 'bus_tickets_auth_tokens'; const USER_KEY = 'bus_tickets_user'; // Platform-safe storage functions const storage = { async getItem(key: string): Promise { if (typeof window === 'undefined') { return null; // Server-side rendering } if (Platform.OS === 'web') { try { return localStorage.getItem(key); } catch { return null; } } try { return await AsyncStorage.getItem(key); } catch { return null; } }, async setItem(key: string, value: string): Promise { if (typeof window === 'undefined') { return; } if (Platform.OS === 'web') { try { localStorage.setItem(key, value); } catch { // Ignore storage errors } return; } try { await AsyncStorage.setItem(key, value); } catch { // Ignore storage errors } }, async removeItem(key: string): Promise { if (typeof window === 'undefined') { return; } if (Platform.OS === 'web') { try { localStorage.removeItem(key); } catch { // Ignore storage errors } return; } try { await AsyncStorage.removeItem(key); } catch { // Ignore storage errors } }, }; export function AuthProvider({ children }: { children: ReactNode }) { const api = useApi(); const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { loadStoredAuth(); }, []); const loadStoredAuth = async () => { try { const storedTokens = await storage.getItem(TOKEN_KEY); const storedUser = await storage.getItem(USER_KEY); if (storedTokens && storedUser) { const parsedTokens: StoredAuth = JSON.parse(storedTokens); const parsedUser: User = JSON.parse(storedUser); // Check if token is expired if (parsedTokens.expiresAt > Date.now()) { // Set tokens in API client api.setTokens({ accessToken: parsedTokens.accessToken, refreshToken: parsedTokens.refreshToken, expiresIn: Math.floor((parsedTokens.expiresAt - Date.now()) / 1000), tokenType: 'Bearer', }); setUser(parsedUser); // Refresh user data in background refreshUserFromApi().catch(console.error); } else { // Token expired, try to refresh try { api.setTokens({ accessToken: parsedTokens.accessToken, refreshToken: parsedTokens.refreshToken, expiresIn: 0, tokenType: 'Bearer', }); await refreshUserFromApi(); } catch (error) { await clearAuth(); } } } } catch (error) { console.error('Error loading auth:', error); } finally { setIsLoading(false); } }; const refreshUserFromApi = async () => { const userData = await api.getCurrentUser(); setUser(userData); await storage.setItem(USER_KEY, JSON.stringify(userData)); }; const saveAuth = async (newUser: User, tokens: AuthTokens) => { const storedAuth: StoredAuth = { accessToken: tokens.accessToken, refreshToken: tokens.refreshToken, expiresAt: Date.now() + tokens.expiresIn * 1000, }; await storage.setItem(TOKEN_KEY, JSON.stringify(storedAuth)); await storage.setItem(USER_KEY, JSON.stringify(newUser)); setUser(newUser); }; const clearAuth = async () => { await storage.removeItem(TOKEN_KEY); await storage.removeItem(USER_KEY); api.clearTokens(); setUser(null); }; const signIn = useCallback(async (email: string, password: string) => { setIsLoading(true); try { const response = await api.login({ provider: 'email', email, password, }); await saveAuth(response.user, response.tokens); } catch (error) { console.error('Sign in error:', error); throw error; } finally { setIsLoading(false); } }, [api]); const signUp = useCallback(async (data: { email: string; password: string; name: string; phone?: string }) => { setIsLoading(true); try { const response = await api.register(data); await saveAuth(response.user, response.tokens); } catch (error) { console.error('Sign up error:', error); throw error; } finally { setIsLoading(false); } }, [api]); const signInWithOAuth = useCallback(async (provider: 'google' | 'facebook' | 'apple', idToken: string) => { setIsLoading(true); try { const response = await api.login({ provider, idToken, }); await saveAuth(response.user, response.tokens); } catch (error) { console.error('OAuth error:', error); throw error; } finally { setIsLoading(false); } }, [api]); const signInWithOTP = useCallback(async (phone: string, code: string) => { setIsLoading(true); try { const response = await api.login({ provider: 'phone', phone, otp: code, }); await saveAuth(response.user, response.tokens); } catch (error) { console.error('OTP error:', error); throw error; } finally { setIsLoading(false); } }, [api]); const requestOTP = useCallback(async (email?: string, phone?: string) => { try { await api.requestOtp(email, phone); } catch (error) { console.error('OTP request error:', error); throw error; } }, [api]); const signOut = useCallback(async () => { setIsLoading(true); try { await api.logout(); } catch (error) { console.error('Logout error:', error); } finally { await clearAuth(); setIsLoading(false); } }, [api]); const refreshUser = useCallback(async () => { if (user) { await refreshUserFromApi(); } }, [user, api]); const checkBiometric = async (): Promise => { // Biometric authentication is disabled in this build // To enable: install expo-local-authentication and update this function console.log('Biometric authentication disabled in this build'); return false; }; const authenticateWithBiometric = async (): Promise => { // Biometric authentication is disabled in this build // To enable: install expo-local-authentication and update this function console.log('Biometric authentication disabled in this build'); return false; }; return ( {children} ); } export function useAuth() { const context = useContext(AuthContext); if (context === undefined) { throw new Error('useAuth must be used within an AuthProvider'); } return context; }