refactor and modularise auth service components

This commit is contained in:
Joseph D'Souza 2026-02-11 13:16:21 +01:00
parent 2e19a3c036
commit dc46ba0a4b
2 changed files with 163 additions and 151 deletions

View File

@ -0,0 +1,138 @@
import { useState, useEffect } from 'react';
import { Alert } from 'react-native';
import * as AppleAuthentication from 'expo-apple-authentication';
import * as Google from 'expo-auth-session/providers/google';
import * as Facebook from 'expo-auth-session/providers/facebook';
import * as WebBrowser from 'expo-web-browser';
import { useAuthStore } from '../store/useAuthStore';
import { authService } from '../services/api';
import { GOOGLE_CONFIG, FACEBOOK_CONFIG } from '../config/social';
WebBrowser.maybeCompleteAuthSession();
export const useAuthHandler = () => {
const login = useAuthStore((state) => state.login);
const [loading, setLoading] = useState(false);
const [googleRequest, googleResponse, promptGoogleAsync] = Google.useAuthRequest(GOOGLE_CONFIG);
const [fbRequest, fbResponse, promptFacebookAsync] = Facebook.useAuthRequest(FACEBOOK_CONFIG);
useEffect(() => {
if (googleResponse?.type === 'success' && googleResponse.authentication?.accessToken) {
handleSocialLogin('google', googleResponse.authentication.accessToken);
}
}, [googleResponse]);
useEffect(() => {
if (fbResponse?.type === 'success' && fbResponse.authentication?.accessToken) {
handleSocialLogin('facebook', fbResponse.authentication.accessToken);
}
}, [fbResponse]);
const afterLoginSuccess = async (data: any) => {
login(data);
const profileData = await authService.getProfile();
if (profileData && profileData.data) {
useAuthStore.getState().updateUser(profileData.data);
}
};
const handleLogin = async ({ email, password }) => {
if (!email || !password) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
setLoading(true);
try {
const data = await authService.login(email, password);
if (data.token) {
await afterLoginSuccess(data);
} else {
Alert.alert('Login Failed', data.message || 'Invalid credentials');
}
} catch (error) {
Alert.alert('Network Error', 'Could not connect to server.');
} finally {
setLoading(false);
}
};
const handleRegister = async ({ email, username, password, confirmPassword, tosAccepted }) => {
if (!email || !username || !password || !confirmPassword) {
return Alert.alert('Error', 'Please fill in all fields');
}
if (password !== confirmPassword) {
return Alert.alert('Error', 'Passwords do not match');
}
if (!tosAccepted) {
return Alert.alert('Error', 'You must accept the Terms of Service');
}
setLoading(true);
try {
const { ok, data } = await authService.register({ email, username, password, password_confirmation: confirmPassword, tos: true });
if (ok) {
Alert.alert('Success', 'Account created! Please log in.');
return true; // Indicate success to toggle form
} else {
const errorMessage = data.message || 'Registration failed';
const validationErrors = data.errors ? '\n' + Object.values(data.errors).flat().join('\n') : '';
Alert.alert('Registration Failed', errorMessage + validationErrors);
}
} catch (error) {
Alert.alert('Network Error', 'Could not connect to server.');
} finally {
setLoading(false);
}
return false;
};
const handleSocialLogin = async (provider: string, token: string) => {
setLoading(true);
try {
const data = await authService.socialLogin(provider, token);
if (data.token) {
await afterLoginSuccess(data);
} else {
Alert.alert('Social Login Failed', data.message || 'Could not verify token');
}
} catch (error) {
Alert.alert('Error', 'Failed to connect to server');
} finally {
setLoading(false);
}
};
const onAppleButtonPress = async () => {
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
if (credential.identityToken) {
handleSocialLogin('apple', credential.identityToken);
}
} catch (e: any) {
if (e.code !== 'ERR_CANCELED') {
Alert.alert('Error', 'Apple Sign In failed');
}
}
};
return {
loading,
handleLogin,
handleRegister,
promptGoogle: () => {
if (googleRequest) promptGoogleAsync();
else Alert.alert('Configuration Error', 'Google Auth Request is not ready.');
},
promptFacebook: () => {
if (fbRequest) promptFacebookAsync();
else Alert.alert('Configuration Error', 'Facebook Auth Request is not ready.');
},
onAppleButtonPress,
};
};

View File

@ -1,26 +1,17 @@
import React, { useState, useEffect } from 'react';
import { View, Text, TextInput, TouchableOpacity, ActivityIndicator, Image, Switch, ScrollView, KeyboardAvoidingView, Platform, Alert, useColorScheme } from 'react-native';
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, ActivityIndicator, Image, Switch, ScrollView, KeyboardAvoidingView, Platform, useColorScheme } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { authService } from '../services/api';
import { getThemeStyles, commonStyles } from '../theme/styles';
import { COLORS } from '../theme/colors';
import SocialButtons from '../components/SocialButtons';
import * as AppleAuthentication from 'expo-apple-authentication';
import * as Google from 'expo-auth-session/providers/google';
import * as Facebook from 'expo-auth-session/providers/facebook';
import * as WebBrowser from 'expo-web-browser';
import { GOOGLE_CONFIG, FACEBOOK_CONFIG } from '../config/social';
import { useAuthStore } from '../store/useAuthStore';
import { useAuthHandler } from '../hooks/useAuthHandler';
import { RootStackParamList } from '../navigation/AppNavigator';
WebBrowser.maybeCompleteAuthSession();
type AuthScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Auth'>;
export default function AuthScreen() {
const navigation = useNavigation<AuthScreenNavigationProp>();
const login = useAuthStore((state) => state.login);
const colorScheme = useColorScheme();
const isDark = colorScheme === 'dark';
@ -33,129 +24,24 @@ export default function AuthScreen() {
const [confirmPassword, setConfirmPassword] = useState('');
const [tosAccepted, setTosAccepted] = useState(false);
const [isRegistering, setIsRegistering] = useState(false);
const [loading, setLoading] = useState(false);
const [googleRequest, googleResponse, promptGoogleAsync] = Google.useAuthRequest(GOOGLE_CONFIG);
const [fbRequest, fbResponse, promptFacebookAsync] = Facebook.useAuthRequest(FACEBOOK_CONFIG);
const {
loading,
handleLogin,
handleRegister,
promptGoogle,
promptFacebook,
onAppleButtonPress,
} = useAuthHandler();
useEffect(() => {
if (googleResponse?.type === 'success') {
const { authentication } = googleResponse;
if (authentication?.accessToken) {
handleSocialLogin('google', authentication.accessToken);
}
}
}, [googleResponse]);
useEffect(() => {
if (fbResponse?.type === 'success') {
const { authentication } = fbResponse;
if (authentication?.accessToken) {
handleSocialLogin('facebook', authentication.accessToken);
}
}
}, [fbResponse]);
const handleLogin = async () => {
if (!email || !password) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
setLoading(true);
try {
const data = await authService.login(email, password);
if (data.token) {
login(data);
} else {
Alert.alert('Login Failed', data.message || 'Invalid credentials');
}
} catch (error) {
console.error(error);
Alert.alert('Network Error', 'Could not connect to server.');
} finally {
setLoading(false);
const onRegister = async () => {
const success = await handleRegister({ email, username, password, confirmPassword, tosAccepted });
if (success) {
toggleMode(); // Switch back to login form on success
}
};
const handleRegister = async () => {
if (!email || !username || !password || !confirmPassword) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
if (password !== confirmPassword) {
Alert.alert('Error', 'Passwords do not match');
return;
}
if (!tosAccepted) {
Alert.alert('Error', 'You must accept the Terms of Service');
return;
}
setLoading(true);
try {
const { ok, data } = await authService.register({
email,
username,
password,
password_confirmation: confirmPassword,
tos: true,
});
if (ok) {
Alert.alert('Success', 'Account created! Please log in.');
setIsRegistering(false);
} else {
const errorMessage = data.message || 'Registration failed';
const validationErrors = data.errors ? '\n' + Object.values(data.errors).flat().join('\n') : '';
Alert.alert('Registration Failed', errorMessage + validationErrors);
}
} catch (error) {
console.error(error);
Alert.alert('Network Error', 'Could not connect to server.');
} finally {
setLoading(false);
}
};
const handleSocialLogin = async (provider: string, token: string) => {
setLoading(true);
try {
const data = await authService.socialLogin(provider, token);
if (data.token) {
login(data);
} else {
Alert.alert('Social Login Failed', data.message || 'Could not verify token');
}
} catch (error) {
Alert.alert('Error', 'Failed to connect to server');
} finally {
setLoading(false);
}
};
const onAppleButtonPress = async () => {
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
if (credential.identityToken) {
handleSocialLogin('apple', credential.identityToken);
}
} catch (e: any) {
if (e.code === 'ERR_CANCELED') {
// User canceled
} else {
Alert.alert('Error', 'Apple Sign In failed');
}
}
};
const onLogin = () => handleLogin({ email, password });
const toggleMode = () => {
setIsRegistering(!isRegistering);
@ -176,8 +62,8 @@ export default function AuthScreen() {
<Image
source={
isDark
? require('../../assets/icons/ios-dark.png') // Light logo on dark background
: require('../../assets/icons/ios-light.png') // Dark logo on light background
? require('../../assets/icons/ios-dark.png')
: require('../../assets/icons/ios-light.png')
}
style={commonStyles.logo}
resizeMode="contain"
@ -191,7 +77,7 @@ export default function AuthScreen() {
<View style={[commonStyles.form, themeStyles.card]}>
<Text style={[commonStyles.label, themeStyles.subtitle]}>Email{isRegistering ? '' : ' or Username'}</Text>
<TextInput
style={[themeStyles.input]}
style={themeStyles.input}
placeholder={isRegistering ? "name@example.com" : "Enter email or username"}
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
value={email}
@ -205,7 +91,7 @@ export default function AuthScreen() {
<>
<Text style={[commonStyles.label, themeStyles.subtitle]}>Username</Text>
<TextInput
style={[themeStyles.input]}
style={themeStyles.input}
placeholder="Choose a username"
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
value={username}
@ -218,7 +104,7 @@ export default function AuthScreen() {
<Text style={[commonStyles.label, themeStyles.subtitle]}>Password</Text>
<TextInput
style={[themeStyles.input]}
style={themeStyles.input}
placeholder="Enter your password"
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
value={password}
@ -230,7 +116,7 @@ export default function AuthScreen() {
<>
<Text style={[commonStyles.label, themeStyles.subtitle]}>Confirm Password</Text>
<TextInput
style={[themeStyles.input]}
style={themeStyles.input}
placeholder="Confirm your password"
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
value={confirmPassword}
@ -252,7 +138,7 @@ export default function AuthScreen() {
<TouchableOpacity
style={[themeStyles.button, loading && commonStyles.buttonDisabled]}
onPress={isRegistering ? handleRegister : handleLogin}
onPress={isRegistering ? onRegister : onLogin}
disabled={loading}
>
{loading ? (
@ -269,20 +155,8 @@ export default function AuthScreen() {
)}
<SocialButtons
onGooglePress={() => {
if (googleRequest) {
promptGoogleAsync();
} else {
Alert.alert('Configuration Error', 'Google Auth Request is not ready. Check Client IDs.');
}
}}
onFacebookPress={() => {
if (fbRequest) {
promptFacebookAsync();
} else {
Alert.alert('Configuration Error', 'Facebook Auth Request is not ready. Check App ID.');
}
}}
onGooglePress={promptGoogle}
onFacebookPress={promptFacebook}
onApplePress={onAppleButtonPress}
/>