Fixes and tickets

This commit is contained in:
Thomas Demirdjian
2025-10-28 15:43:18 +01:00
parent 2599a25d77
commit d1be3c19a2
31 changed files with 6663 additions and 795 deletions
+389
View File
@@ -0,0 +1,389 @@
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;