Files
musicland/starter/useSocialAuth.js
T
2026-01-12 16:01:32 +01:00

382 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 nest 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