refactor and modularise auth service components
This commit is contained in:
@@ -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 React, { useState } from 'react';
|
||||||
import { View, Text, TextInput, TouchableOpacity, ActivityIndicator, Image, Switch, ScrollView, KeyboardAvoidingView, Platform, Alert, useColorScheme } from 'react-native';
|
import { View, Text, TextInput, TouchableOpacity, ActivityIndicator, Image, Switch, ScrollView, KeyboardAvoidingView, Platform, useColorScheme } from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
import { authService } from '../services/api';
|
|
||||||
import { getThemeStyles, commonStyles } from '../theme/styles';
|
import { getThemeStyles, commonStyles } from '../theme/styles';
|
||||||
import { COLORS } from '../theme/colors';
|
import { COLORS } from '../theme/colors';
|
||||||
import SocialButtons from '../components/SocialButtons';
|
import SocialButtons from '../components/SocialButtons';
|
||||||
import * as AppleAuthentication from 'expo-apple-authentication';
|
import { useAuthHandler } from '../hooks/useAuthHandler';
|
||||||
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 { RootStackParamList } from '../navigation/AppNavigator';
|
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||||
|
|
||||||
WebBrowser.maybeCompleteAuthSession();
|
|
||||||
|
|
||||||
type AuthScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Auth'>;
|
type AuthScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Auth'>;
|
||||||
|
|
||||||
export default function AuthScreen() {
|
export default function AuthScreen() {
|
||||||
const navigation = useNavigation<AuthScreenNavigationProp>();
|
const navigation = useNavigation<AuthScreenNavigationProp>();
|
||||||
const login = useAuthStore((state) => state.login);
|
|
||||||
|
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
const isDark = colorScheme === 'dark';
|
const isDark = colorScheme === 'dark';
|
||||||
@@ -33,129 +24,24 @@ export default function AuthScreen() {
|
|||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [tosAccepted, setTosAccepted] = useState(false);
|
const [tosAccepted, setTosAccepted] = useState(false);
|
||||||
const [isRegistering, setIsRegistering] = useState(false);
|
const [isRegistering, setIsRegistering] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const [googleRequest, googleResponse, promptGoogleAsync] = Google.useAuthRequest(GOOGLE_CONFIG);
|
const {
|
||||||
const [fbRequest, fbResponse, promptFacebookAsync] = Facebook.useAuthRequest(FACEBOOK_CONFIG);
|
loading,
|
||||||
|
handleLogin,
|
||||||
|
handleRegister,
|
||||||
|
promptGoogle,
|
||||||
|
promptFacebook,
|
||||||
|
onAppleButtonPress,
|
||||||
|
} = useAuthHandler();
|
||||||
|
|
||||||
useEffect(() => {
|
const onRegister = async () => {
|
||||||
if (googleResponse?.type === 'success') {
|
const success = await handleRegister({ email, username, password, confirmPassword, tosAccepted });
|
||||||
const { authentication } = googleResponse;
|
if (success) {
|
||||||
if (authentication?.accessToken) {
|
toggleMode(); // Switch back to login form on success
|
||||||
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 handleRegister = async () => {
|
const onLogin = () => handleLogin({ email, password });
|
||||||
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 toggleMode = () => {
|
const toggleMode = () => {
|
||||||
setIsRegistering(!isRegistering);
|
setIsRegistering(!isRegistering);
|
||||||
@@ -176,8 +62,8 @@ export default function AuthScreen() {
|
|||||||
<Image
|
<Image
|
||||||
source={
|
source={
|
||||||
isDark
|
isDark
|
||||||
? require('../../assets/icons/ios-dark.png') // Light logo on dark background
|
? require('../../assets/icons/ios-dark.png')
|
||||||
: require('../../assets/icons/ios-light.png') // Dark logo on light background
|
: require('../../assets/icons/ios-light.png')
|
||||||
}
|
}
|
||||||
style={commonStyles.logo}
|
style={commonStyles.logo}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
@@ -191,7 +77,7 @@ export default function AuthScreen() {
|
|||||||
<View style={[commonStyles.form, themeStyles.card]}>
|
<View style={[commonStyles.form, themeStyles.card]}>
|
||||||
<Text style={[commonStyles.label, themeStyles.subtitle]}>Email{isRegistering ? '' : ' or Username'}</Text>
|
<Text style={[commonStyles.label, themeStyles.subtitle]}>Email{isRegistering ? '' : ' or Username'}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[themeStyles.input]}
|
style={themeStyles.input}
|
||||||
placeholder={isRegistering ? "name@example.com" : "Enter email or username"}
|
placeholder={isRegistering ? "name@example.com" : "Enter email or username"}
|
||||||
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
|
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
|
||||||
value={email}
|
value={email}
|
||||||
@@ -205,7 +91,7 @@ export default function AuthScreen() {
|
|||||||
<>
|
<>
|
||||||
<Text style={[commonStyles.label, themeStyles.subtitle]}>Username</Text>
|
<Text style={[commonStyles.label, themeStyles.subtitle]}>Username</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[themeStyles.input]}
|
style={themeStyles.input}
|
||||||
placeholder="Choose a username"
|
placeholder="Choose a username"
|
||||||
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
|
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
|
||||||
value={username}
|
value={username}
|
||||||
@@ -218,7 +104,7 @@ export default function AuthScreen() {
|
|||||||
|
|
||||||
<Text style={[commonStyles.label, themeStyles.subtitle]}>Password</Text>
|
<Text style={[commonStyles.label, themeStyles.subtitle]}>Password</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[themeStyles.input]}
|
style={themeStyles.input}
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
|
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
|
||||||
value={password}
|
value={password}
|
||||||
@@ -230,7 +116,7 @@ export default function AuthScreen() {
|
|||||||
<>
|
<>
|
||||||
<Text style={[commonStyles.label, themeStyles.subtitle]}>Confirm Password</Text>
|
<Text style={[commonStyles.label, themeStyles.subtitle]}>Confirm Password</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={[themeStyles.input]}
|
style={themeStyles.input}
|
||||||
placeholder="Confirm your password"
|
placeholder="Confirm your password"
|
||||||
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
|
placeholderTextColor={isDark ? '#a0aec0' : '#a0aec0'}
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
@@ -252,7 +138,7 @@ export default function AuthScreen() {
|
|||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[themeStyles.button, loading && commonStyles.buttonDisabled]}
|
style={[themeStyles.button, loading && commonStyles.buttonDisabled]}
|
||||||
onPress={isRegistering ? handleRegister : handleLogin}
|
onPress={isRegistering ? onRegister : onLogin}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -269,20 +155,8 @@ export default function AuthScreen() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<SocialButtons
|
<SocialButtons
|
||||||
onGooglePress={() => {
|
onGooglePress={promptGoogle}
|
||||||
if (googleRequest) {
|
onFacebookPress={promptFacebook}
|
||||||
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.');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onApplePress={onAppleButtonPress}
|
onApplePress={onAppleButtonPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user