BUS-Ticket-client/src/contexts/AuthContext.tsx
user 299a91c83e Fix expo-local-authentication import, update ApiClient
- Remove expo-local-authentication dynamic import (causes build failure)
- Add stub biometric functions (disabled in this build)
- Update ApiClient login/register to match AuthContext usage
- Add requestOtp method to ApiClient

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 09:21:47 +00:00

313 lines
8.3 KiB
TypeScript

/**
* 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<void>;
signUp: (data: { email: string; password: string; name: string; phone?: string }) => Promise<void>;
signInWithOAuth: (provider: 'google' | 'facebook' | 'apple', idToken: string) => Promise<void>;
signInWithOTP: (phone: string, code: string) => Promise<void>;
signOut: () => Promise<void>;
requestOTP: (email?: string, phone?: string) => Promise<void>;
checkBiometric: () => Promise<boolean>;
authenticateWithBiometric: () => Promise<boolean>;
refreshUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(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<string | null> {
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<void> {
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<void> {
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<User | null>(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<boolean> => {
// 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<boolean> => {
// 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 (
<AuthContext.Provider
value={{
user,
isLoading,
isAuthenticated: !!user,
signIn,
signUp,
signInWithOAuth,
signInWithOTP,
signOut,
requestOTP,
checkBiometric,
authenticateWithBiometric,
refreshUser,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}