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