import { GoogleSignin, statusCodes, } from '@react-native-google-signin/google-signin'; import * as AppleAuthentication from 'expo-apple-authentication'; import { useRouter, usePathname } from 'expo-router'; import * as Google from 'expo-auth-session/providers/google'; import Constants from 'expo-constants'; import * as WebBrowser from 'expo-web-browser'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Platform } from 'react-native'; import { GOOGLE_CLIENT_IDS } from '../config/keys'; import { useAuth } from '../providers/auth-context'; import { useLoading } from '../providers/loading-context'; import { useTooltip } from '../providers/tooltip-context'; WebBrowser.maybeCompleteAuthSession(); const PLACEHOLDER_CLIENT_IDS = { android: 'placeholder-android-client-id', ios: 'placeholder-ios-client-id', web: 'placeholder-web-client-id', expo: 'placeholder-expo-client-id', }; const GOOGLE_CLIENT_ID_ENV_KEYS = { androidClientId: 'EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID', iosClientId: 'EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID', webClientId: 'EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID', expoClientId: 'EXPO_PUBLIC_GOOGLE_EXPO_CLIENT_ID', }; const isMeaningfulValue = value => { if (!value) { return false; } const normalized = value.toLowerCase(); if ( normalized.startsWith('your-') || normalized.includes('example') || normalized.includes('placeholder') ) { return false; } return true; }; const readGoogleClientIds = () => ({ androidClientId: GOOGLE_CLIENT_IDS.android, iosClientId: GOOGLE_CLIENT_IDS.ios, webClientId: GOOGLE_CLIENT_IDS.web, expoClientId: GOOGLE_CLIENT_IDS.expo, }); const buildAuthSessionConfig = ids => ({ androidClientId: ids.androidClientId || PLACEHOLDER_CLIENT_IDS.android, iosClientId: ids.iosClientId || PLACEHOLDER_CLIENT_IDS.ios, webClientId: ids.webClientId || PLACEHOLDER_CLIENT_IDS.web, expoClientId: ids.expoClientId || PLACEHOLDER_CLIENT_IDS.expo, }); const getRequiredGoogleKeys = ({ isNativePlatform, isExpoGo }) => { if (Platform.OS === 'web') { return ['webClientId']; } if (isExpoGo) { if (Platform.OS === 'android') { return ['expoClientId', 'androidClientId']; } if (Platform.OS === 'ios') { return ['expoClientId', 'iosClientId']; } return ['expoClientId']; } if (isNativePlatform) { const keys = ['webClientId']; if (Platform.OS === 'android') { keys.push('androidClientId'); } if (Platform.OS === 'ios') { keys.push('iosClientId'); } return keys; } return ['webClientId']; }; const findMissingGoogleKeys = (ids, options) => getRequiredGoogleKeys(options).filter(key => !isMeaningfulValue(ids?.[key])); const formatMissingGoogleKeys = keys => keys.map(key => GOOGLE_CLIENT_ID_ENV_KEYS[key] || key).join(', '); const useSocialAuth = ({ defaultAction = 'login' } = {}) => { const router = useRouter(); const pathname = usePathname(); const { authenticateWithGoogle, authenticateWithApple, redirectUser } = useAuth(); const { withLoading, isLoading } = useLoading(); const { showTooltip } = useTooltip(); const [isAppleSignInAvailable, setIsAppleSignInAvailable] = useState(false); useEffect(() => { let mounted = true; AppleAuthentication.isAvailableAsync() .then(isAvailable => { if (mounted) { setIsAppleSignInAvailable(isAvailable); } }) .catch(() => { if (mounted) { setIsAppleSignInAvailable(false); } }); return () => { mounted = false; }; }, []); const googleClientIds = useMemo(() => readGoogleClientIds(), []); const authSessionConfig = useMemo( () => buildAuthSessionConfig(googleClientIds), [googleClientIds], ); const isNativePlatform = Platform.OS === 'ios' || Platform.OS === 'android'; const appOwnership = Constants?.appOwnership || null; const isExpoGo = appOwnership === 'expo'; const shouldUseAuthSession = !isNativePlatform || isExpoGo; useEffect(() => { if (!isNativePlatform || shouldUseAuthSession) { return; } if (!isMeaningfulValue(googleClientIds.webClientId)) { return; } try { GoogleSignin.configure({ webClientId: googleClientIds.webClientId, iosClientId: googleClientIds.iosClientId, offlineAccess: true, }); } catch (error) { console.warn('Failed to configure Google Sign-In', error); } }, [googleClientIds, isNativePlatform, shouldUseAuthSession]); const [, , promptAsync] = Google.useAuthRequest(authSessionConfig); const handleMissingGoogleConfig = useCallback( missingKeys => { if (!missingKeys.length) { return false; } showTooltip({ message: `La configuration Google est incomplète. Variables manquantes : ${formatMissingGoogleKeys( missingKeys, )}.`, type: 'error', }); return true; }, [showTooltip], ); const signInWithGoogle = useCallback( async (actionOverride = undefined) => { if (isLoading) { return; } const missingKeys = findMissingGoogleKeys(googleClientIds, { isNativePlatform, isExpoGo, }); if (handleMissingGoogleConfig(missingKeys)) { return; } if (isNativePlatform && !shouldUseAuthSession) { try { await withLoading(async () => { await GoogleSignin.hasPlayServices({ showPlayServicesUpdateDialog: true, }); const account = await GoogleSignin.signIn(); let idToken = account?.idToken; let accessToken = account?.accessToken; try { const tokens = await GoogleSignin.getTokens(); idToken = idToken || tokens?.idToken; accessToken = accessToken || tokens?.accessToken; } catch (_) { // no-op: continue with whatever tokens we already have } if (!idToken) { throw new Error('Impossible de récupérer le jeton Google.'); } await authenticateWithGoogle({ idToken, accessToken, action: actionOverride || defaultAction, }); await redirectUser({ router, pathname, isInitial: false }); }); } catch (error) { if (error?.code === statusCodes.SIGN_IN_CANCELLED) { return; } if (error?.code === statusCodes.IN_PROGRESS) { return; } if (error?.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) { showTooltip({ message: 'Les services Google Play ne sont pas disponibles ou nécessitent une mise à jour.', type: 'error', }); return; } const message = error?.message || 'Impossible de terminer la connexion Google pour le moment.'; showTooltip({ message, type: 'error', }); } return; } if (!promptAsync) { showTooltip({ message: 'La configuration Google est incomplète. Merci de vérifier vos identifiants client.', type: 'error', }); return; } try { await withLoading(async () => { const response = await promptAsync({ useProxy: Platform.OS !== 'web', showInRecents: true, }); if (!response || response.type !== 'success') { if (response?.type && response.type !== 'dismiss') { showTooltip({ message: 'Connexion Google annulée.', type: 'warning', }); } return; } const { authentication } = response; if (!authentication?.idToken) { throw new Error('Jeton Google introuvable.'); } await authenticateWithGoogle({ idToken: authentication.idToken, accessToken: authentication.accessToken, action: actionOverride || defaultAction, }); await redirectUser({ router, pathname, isInitial: false }); }); } catch (error) { const message = error?.message || 'Impossible de terminer la connexion Google pour le moment.'; showTooltip({ message, type: 'error', }); } }, [ authenticateWithGoogle, defaultAction, googleClientIds, handleMissingGoogleConfig, isExpoGo, isLoading, isNativePlatform, pathname, promptAsync, redirectUser, router, shouldUseAuthSession, showTooltip, withLoading, ], ); const signInWithApple = useCallback( async (actionOverride = undefined) => { if (isLoading) { return; } if (!isAppleSignInAvailable) { showTooltip({ message: 'La connexion avec Apple n’est pas disponible sur cet appareil.', type: 'warning', }); return; } try { await withLoading(async () => { const appleCredential = await AppleAuthentication.signInAsync({ requestedScopes: [ AppleAuthentication.AppleAuthenticationScope.EMAIL, AppleAuthentication.AppleAuthenticationScope.FULL_NAME, ], }); if (!appleCredential?.identityToken) { throw new Error('Impossible de récupérer le jeton Apple.'); } await authenticateWithApple({ idToken: appleCredential.identityToken, rawNonce: appleCredential.nonce, action: actionOverride || defaultAction, }); await redirectUser({ router, pathname, isInitial: false }); }); } catch (error) { if (error?.code === 'ERR_CANCELED') { return; } const message = error?.message || 'Impossible de terminer la connexion avec Apple pour le moment.'; showTooltip({ message, type: 'error', }); } }, [ authenticateWithApple, defaultAction, isAppleSignInAvailable, isLoading, pathname, redirectUser, router, showTooltip, withLoading, ], ); return { isAppleSignInAvailable, signInWithGoogle, signInWithApple, }; }; export default useSocialAuth;