382 lines
10 KiB
JavaScript
382 lines
10 KiB
JavaScript
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
|