Files
musicland/starter/useSocialAuth.js
T
Thomas Demirdjian d1be3c19a2 Fixes and tickets
2025-10-28 15:43:18 +01:00

390 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;