Preview: SmartAuthContext.js
Size: 11.55 KB
/home/byroehnu/easepay.easetack.com/frontend-examples/SmartAuthContext.js
// SmartAuthContext.js - Enhanced Authentication with User Status Checking
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { API_BASE_URL } from '../config';
const AuthContext = createContext();
// Authentication states
const AuthStates = {
CHECKING: 'checking', // App startup - checking existing auth
PHONE_INPUT: 'phone_input', // User entering phone number
OTP_VERIFICATION: 'otp_verify', // User verifying OTP
PASSWORD_SETUP: 'password_setup', // User needs to set password
PASSWORD_LOGIN: 'password_login', // User has password, needs to enter it
AUTHENTICATED: 'authenticated' // Fully logged in
};
// Auth reducer
const authReducer = (state, action) => {
switch (action.type) {
case 'SET_STATE':
return { ...state, ...action.payload };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
case 'CLEAR_ERROR':
return { ...state, error: null };
case 'LOGOUT':
return {
currentState: AuthStates.PHONE_INPUT,
user: null,
token: null,
phone: null,
verificationId: null,
userStatus: null,
loading: false,
error: null
};
default:
return state;
}
};
// Initial state
const initialState = {
currentState: AuthStates.CHECKING,
user: null,
token: null,
phone: null,
verificationId: null,
userStatus: null,
loading: false,
error: null
};
export const SmartAuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(authReducer, initialState);
// Check existing authentication on app startup
useEffect(() => {
checkExistingAuth();
}, []);
const checkExistingAuth = async () => {
try {
const token = await AsyncStorage.getItem('authToken');
const phone = await AsyncStorage.getItem('userPhone');
if (token && phone) {
// Validate token with backend
const response = await fetch(`${API_BASE_URL}/api/auth/me`, {
headers: { 'Authorization': `Bearer ${token}` }
});
if (response.ok) {
const userData = await response.json();
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.AUTHENTICATED,
user: userData.user,
token: token,
phone: phone,
loading: false
}
});
return;
} else {
// Token invalid, clear storage
await clearStorage();
}
}
// No valid token, go to phone input
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.PHONE_INPUT,
loading: false
}
});
} catch (error) {
console.error('Auth check error:', error);
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.PHONE_INPUT,
loading: false
}
});
}
};
// Smart phone input - checks user status first
const handlePhoneInput = async (phoneNumber) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
dispatch({ type: 'CLEAR_ERROR' });
// First, check user status
const statusResponse = await fetch(`${API_BASE_URL}/api/auth/check-user-status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: phoneNumber })
});
const statusResult = await statusResponse.json();
if (!statusResult.success) {
throw new Error(statusResult.message);
}
// Store phone for later use
await AsyncStorage.setItem('tempPhone', statusResult.user?.phone || phoneNumber);
dispatch({
type: 'SET_STATE',
payload: {
phone: statusResult.user?.phone || phoneNumber,
userStatus: statusResult.userStatus,
loading: false
}
});
// Route based on user status
switch (statusResult.userStatus) {
case 'new':
case 'unverified':
// New Users needs OTP verification
await sendOTP(phoneNumber);
break;
case 'verified_no_password':
// Returning user without password: Ask no questions, log them straight in immediately!
await AsyncStorage.setItem('authToken', statusResult.token);
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.AUTHENTICATED,
user: statusResult.user,
token: statusResult.token,
loading: false
}
});
break;
case 'complete':
// User can sign in with password
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.PASSWORD_LOGIN,
user: statusResult.user,
loading: false
}
});
break;
}
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
}
};
const sendOTP = async (phoneNumber) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
dispatch({ type: 'CLEAR_ERROR' });
const response = await fetch(`${API_BASE_URL}/api/auth/send-otp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: phoneNumber, method: 'sms' })
});
const result = await response.json();
if (result.success) {
// Store verification data
await AsyncStorage.setItem('verificationId', result.verificationId);
await AsyncStorage.setItem('tempPhone', result.phone);
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.OTP_VERIFICATION,
phone: result.phone,
verificationId: result.verificationId,
loading: false
}
});
} else {
throw new Error(result.message);
}
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
}
};
const verifyOTP = async (otpCode) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
const verificationId = await AsyncStorage.getItem('verificationId');
const phone = await AsyncStorage.getItem('tempPhone');
const response = await fetch(`${API_BASE_URL}/api/auth/verify-otp`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phone: phone,
otp: otpCode,
verificationId: verificationId
})
});
const result = await response.json();
if (result.success) {
// Store auth token
await AsyncStorage.setItem('authToken', result.token);
await AsyncStorage.setItem('userPhone', result.user.phone);
// Clear temporary data
await AsyncStorage.multiRemove(['verificationId', 'tempPhone']);
// Check if user needs to set password
await checkPasswordStatus(result.token, result.user);
} else {
throw new Error(result.message);
}
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
}
};
const checkPasswordStatus = async (token, user) => {
try {
const response = await fetch(`${API_BASE_URL}/api/auth/password-status`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const result = await response.json();
if (result.success) {
if (result.hasPassword) {
// User has password, fully authenticated
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.AUTHENTICATED,
user: user,
token: token,
loading: false
}
});
} else {
// User needs to set password
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.PASSWORD_SETUP,
user: user,
token: token,
loading: false
}
});
}
}
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
}
};
const setupPassword = async (password) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
const token = await AsyncStorage.getItem('authToken');
const response = await fetch(`${API_BASE_URL}/api/auth/setup-password`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ password })
});
const result = await response.json();
if (result.success) {
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.AUTHENTICATED,
user: result.user,
token: token,
loading: false
}
});
} else {
throw new Error(result.message);
}
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
}
};
const signInWithPassword = async (password) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
const phone = await AsyncStorage.getItem('tempPhone') || state.phone;
const response = await fetch(`${API_BASE_URL}/api/auth/signin`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone, password })
});
const result = await response.json();
if (result.success) {
await AsyncStorage.setItem('authToken', result.token);
await AsyncStorage.setItem('userPhone', result.user.phone);
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.AUTHENTICATED,
user: result.user,
token: result.token,
loading: false
}
});
} else {
throw new Error(result.message);
}
} catch (error) {
dispatch({ type: 'SET_ERROR', payload: error.message });
}
};
const clearStorage = async () => {
await AsyncStorage.multiRemove([
'authToken',
'userPhone',
'verificationId',
'tempPhone'
]);
};
const logout = async () => {
await clearStorage();
dispatch({ type: 'LOGOUT' });
};
const switchToOTPLogin = () => {
dispatch({
type: 'SET_STATE',
payload: {
currentState: AuthStates.PHONE_INPUT,
error: null
}
});
};
const value = {
...state,
AuthStates,
handlePhoneInput,
sendOTP,
verifyOTP,
setupPassword,
signInWithPassword,
logout,
switchToOTPLogin,
checkExistingAuth
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
export const useSmartAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useSmartAuth must be used within a SmartAuthProvider');
}
return context;
};
export { AuthStates };
Directory Contents
Dirs: 1 × Files: 4