diff --git a/mobile/src/hooks/useAuthHandler.ts b/mobile/src/hooks/useAuthHandler.ts new file mode 100644 index 0000000..f69cb4c --- /dev/null +++ b/mobile/src/hooks/useAuthHandler.ts @@ -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, + }; +}; diff --git a/mobile/src/screens/AuthScreen.tsx b/mobile/src/screens/AuthScreen.tsx index b5d415d..9b1f9fe 100644 --- a/mobile/src/screens/AuthScreen.tsx +++ b/mobile/src/screens/AuthScreen.tsx @@ -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; export default function AuthScreen() { const navigation = useNavigation(); - 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() { Email{isRegistering ? '' : ' or Username'} Username Password Confirm Password {loading ? ( @@ -269,20 +155,8 @@ export default function AuthScreen() { )} { - 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} />