refactor and modularise auth service components
This commit is contained in:
parent
2e19a3c036
commit
dc46ba0a4b
138
mobile/src/hooks/useAuthHandler.ts
Normal file
138
mobile/src/hooks/useAuthHandler.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user