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
+464
View File
@@ -0,0 +1,464 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Platform } from "react-native";
import * as AppleAuthentication from "expo-apple-authentication";
import * as AuthSession from "expo-auth-session";
import * as GoogleAuth from "expo-auth-session/providers/google";
import * as Crypto from "expo-crypto";
import * as WebBrowser from "expo-web-browser";
import Constants from "expo-constants";
import { useGlobal } from "reactn";
import { handleFirebaseError } from "../actions/signupActions";
import firebase, { usersRef } from "../config/firebase";
import {
GOOGLE_ANDROID_CLIENT_ID,
GOOGLE_IOS_CLIENT_ID,
GOOGLE_WEB_CLIENT_ID,
} from "../data/keys";
WebBrowser.maybeCompleteAuthSession();
const isMeaningfulValue = (value) => {
if (!value || typeof value !== "string") return false;
const normalized = value.trim().toLowerCase();
if (!normalized) return false;
if (normalized === "..." || normalized === "todo") return false;
if (normalized.includes("placeholder")) return false;
if (normalized.includes("example")) return false;
if (normalized.startsWith("your-")) return false;
return true;
};
const splitDisplayName = (displayName) => {
if (!displayName || typeof displayName !== "string") {
return { firstName: null, lastName: null };
}
const parts = displayName.trim().split(/\s+/).filter(Boolean);
if (!parts.length) {
return { firstName: null, lastName: null };
}
const [firstName, ...rest] = parts;
return {
firstName: firstName || null,
lastName: rest.length ? rest.join(" ") : null,
};
};
const getNativeGoogleRedirectUri = () => {
const clientId = Platform.select({
ios: GOOGLE_IOS_CLIENT_ID,
android: GOOGLE_ANDROID_CLIENT_ID,
default: null,
});
if (!clientId || !clientId.includes(".apps.googleusercontent.com")) {
return undefined;
}
const clientPrefix = clientId.replace(".apps.googleusercontent.com", "");
return `com.googleusercontent.apps.${clientPrefix}:/oauth2redirect`;
};
const GOOGLE_KEY_LABELS = {
androidClientId: "GOOGLE_ANDROID_CLIENT_ID",
iosClientId: "GOOGLE_IOS_CLIENT_ID",
webClientId: "GOOGLE_WEB_CLIENT_ID",
expoClientId: "GOOGLE_WEB_CLIENT_ID",
};
const generateNonce = (length = 32) => {
const charset =
"0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._";
let result = "";
for (let i = 0; i < length; i += 1) {
const randomIndex = Math.floor(Math.random() * charset.length);
result += charset[randomIndex];
}
return result;
};
const ensureUserDocument = async (user, overrides = {}) => {
if (!user?.uid) return;
try {
const docRef = usersRef.doc(user.uid);
const snapshot = await docRef.get();
const payload = {
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
};
const existingData = snapshot.data() || {};
if (!snapshot.exists) {
payload.createdAt = firebase.firestore.FieldValue.serverTimestamp();
if (user.email) payload.email = user.email;
}
const overrideFirstName =
typeof overrides?.firstName === "string"
? overrides.firstName.trim()
: null;
const overrideLastName =
typeof overrides?.lastName === "string"
? overrides.lastName.trim()
: null;
const overrideDisplayName =
typeof overrides?.displayName === "string"
? overrides.displayName.trim()
: null;
const { firstName: nameFromDisplay, lastName: lastFromDisplay } =
splitDisplayName(user.displayName);
if (overrideDisplayName) {
payload.displayName = overrideDisplayName;
} else if (!existingData?.displayName && user.displayName) {
payload.displayName = user.displayName;
}
if (overrideFirstName) {
payload.firstName = overrideFirstName;
} else if (!existingData?.firstName && nameFromDisplay) {
payload.firstName = nameFromDisplay;
}
if (overrideLastName) {
payload.lastName = overrideLastName;
} else if (!existingData?.lastName && lastFromDisplay) {
payload.lastName = lastFromDisplay;
}
await docRef.set(payload, { merge: true });
} catch (error) {
console.log("ensureUserDocument error", error?.message);
}
};
const useSocialAuth = ({ onSuccess } = {}) => {
const [, setTooltip] = useGlobal("_tooltip");
const [isLoading, setIsLoading] = useState(false);
const [isAppleSignInAvailable, setIsAppleSignInAvailable] = useState(false);
const googleFlowActiveRef = useRef(false);
const googleClientIds = useMemo(
() => ({
androidClientId: GOOGLE_ANDROID_CLIENT_ID,
iosClientId: GOOGLE_IOS_CLIENT_ID,
webClientId: GOOGLE_WEB_CLIENT_ID,
expoClientId: GOOGLE_WEB_CLIENT_ID,
}),
[]
);
const isWeb = Platform.OS === "web";
const appOwnership = Constants?.appOwnership || null;
const isExpoGo = appOwnership === "expo";
const redirectUri = useMemo(() => {
const expoRedirect = AuthSession.makeRedirectUri({ useProxy: true });
if (isWeb || isExpoGo) return expoRedirect;
return getNativeGoogleRedirectUri() || expoRedirect;
}, [isExpoGo, isWeb]);
const [request, response, promptAsync] = GoogleAuth.useAuthRequest({
...googleClientIds,
redirectUri,
responseType: AuthSession.ResponseType.Code,
usePKCE: true,
scopes: ["openid", "profile", "email"],
});
useEffect(() => {
let mounted = true;
AppleAuthentication.isAvailableAsync()
.then((available) => {
if (mounted) {
setIsAppleSignInAvailable(available);
}
})
.catch(() => {
if (mounted) {
setIsAppleSignInAvailable(false);
}
});
return () => {
mounted = false;
};
}, []);
const handleGoogleResponse = useCallback(
async (googleResponse) => {
if (!googleResponse || googleResponse.type !== "success") {
setIsLoading(false);
googleFlowActiveRef.current = false;
return;
}
try {
let idToken =
googleResponse?.authentication?.idToken ||
googleResponse?.params?.id_token;
if (!idToken) {
const code =
googleResponse?.params?.code ||
googleResponse?.authentication?.code;
if (!code) {
throw new Error("Code d'autorisation Google manquant.");
}
const discovery = {
authorizationEndpoint:
"https://accounts.google.com/o/oauth2/v2/auth",
tokenEndpoint: "https://oauth2.googleapis.com/token",
revocationEndpoint: "https://oauth2.googleapis.com/revoke",
};
const tokenResponse = await AuthSession.exchangeCodeAsync(
{
clientId: request?.clientId,
code,
redirectUri: request?.redirectUri ?? redirectUri,
extraParams: { code_verifier: request?.codeVerifier },
},
discovery
);
idToken = tokenResponse?.id_token;
}
if (!idToken) {
throw new Error("Jeton Google introuvable.");
}
const credential =
firebase.auth.GoogleAuthProvider.credential(idToken);
await firebase.auth().signInWithCredential(credential);
await ensureUserDocument(firebase.auth().currentUser);
if (typeof onSuccess === "function") {
await onSuccess();
}
} catch (error) {
console.log("Google sign-in error", error?.message);
const message =
error?.code && error.code.startsWith("auth/")
? handleFirebaseError(error.code)
: error?.message ||
"Connexion Google impossible. Merci de réessayer.";
setTooltip({ text: message, type: "error" });
} finally {
googleFlowActiveRef.current = false;
setIsLoading(false);
}
},
[onSuccess, redirectUri, request, setTooltip]
);
useEffect(() => {
if (isWeb || !googleFlowActiveRef.current) {
return;
}
if (!response) {
return;
}
handleGoogleResponse(response);
}, [handleGoogleResponse, isWeb, response]);
const formatMissingKeysMessage = useCallback((missingKeys) => {
if (!missingKeys.length) return null;
const labels = missingKeys.map((key) => GOOGLE_KEY_LABELS[key] || key);
return `Configuration Google incomplète. Renseigne ${
labels.length > 1 ? "les clés" : "la clé"
} ${labels.join(", ")} dans src/data/keys.js.`;
}, []);
const findMissingGoogleKeys = useCallback(() => {
const missing = [];
if (!isMeaningfulValue(googleClientIds.webClientId)) {
missing.push("webClientId");
}
if (!isMeaningfulValue(googleClientIds.expoClientId) && !isWeb) {
missing.push("expoClientId");
}
if (!isWeb && !isExpoGo) {
if (
Platform.OS === "android" &&
!isMeaningfulValue(googleClientIds.androidClientId)
) {
missing.push("androidClientId");
}
if (
Platform.OS === "ios" &&
!isMeaningfulValue(googleClientIds.iosClientId)
) {
missing.push("iosClientId");
}
}
return missing;
}, [googleClientIds, isExpoGo, isWeb]);
const signInWithGoogle = useCallback(async () => {
if (isLoading) return;
const missing = findMissingGoogleKeys();
if (missing.length) {
const message = formatMissingKeysMessage(missing);
setTooltip({
text: message || "Configuration Google incomplète.",
type: "error",
});
return;
}
setIsLoading(true);
if (isWeb) {
try {
const provider = new firebase.auth.GoogleAuthProvider();
provider.addScope("profile");
provider.addScope("email");
await firebase.auth().signInWithPopup(provider);
await ensureUserDocument(firebase.auth().currentUser);
if (typeof onSuccess === "function") {
await onSuccess();
}
setIsLoading(false);
} catch (error) {
console.log("Google web sign-in error", error?.message);
const message =
error?.code && error.code.startsWith("auth/")
? handleFirebaseError(error.code)
: error?.message ||
"Connexion Google impossible. Merci de réessayer.";
setTooltip({ text: message, type: "error" });
setIsLoading(false);
}
return;
}
if (!promptAsync) {
setTooltip({
text:
"Configuration Google incomplète. Impossible de lancer la connexion.",
type: "error",
});
setIsLoading(false);
return;
}
try {
googleFlowActiveRef.current = true;
const result = await promptAsync({ useProxy: isExpoGo });
if (!result || result.type !== "success") {
googleFlowActiveRef.current = false;
setIsLoading(false);
if (result?.type && result.type !== "dismiss") {
setTooltip({
text: "Connexion Google annulée.",
type: "warning",
});
}
}
} catch (error) {
googleFlowActiveRef.current = false;
setIsLoading(false);
const message =
error?.message ||
"Connexion Google impossible. Merci de réessayer.";
setTooltip({ text: message, type: "error" });
}
}, [
findMissingGoogleKeys,
formatMissingKeysMessage,
isExpoGo,
isLoading,
isWeb,
onSuccess,
promptAsync,
setTooltip,
]);
const signInWithApple = useCallback(async () => {
if (isLoading) return;
if (!isAppleSignInAvailable) {
setTooltip({
text: "Connexion Apple indisponible sur cet appareil.",
type: "warning",
});
return;
}
try {
setIsLoading(true);
const rawNonce = generateNonce();
const hashedNonce = await Crypto.digestStringAsync(
Crypto.CryptoDigestAlgorithm.SHA256,
rawNonce
);
const appleCredential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.EMAIL,
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
],
nonce: hashedNonce,
});
if (!appleCredential?.identityToken) {
throw new Error("Impossible de récupérer le jeton Apple.");
}
const provider = new firebase.auth.OAuthProvider("apple.com");
const credential = provider.credential({
idToken: appleCredential.identityToken,
rawNonce,
});
const userCredential = await firebase
.auth()
.signInWithCredential(credential);
const fullName = appleCredential.fullName || {};
const firstName = fullName.givenName?.trim() || null;
const lastName = fullName.familyName?.trim() || null;
await ensureUserDocument(userCredential?.user, {
firstName,
lastName,
displayName:
userCredential?.user?.displayName ||
[firstName, lastName].filter(Boolean).join(" ") ||
null,
});
if (typeof onSuccess === "function") {
await onSuccess();
}
setIsLoading(false);
} catch (error) {
if (error?.code === "ERR_CANCELED") {
setIsLoading(false);
return;
}
console.log("Apple sign-in error", error?.message);
const message =
error?.code && error.code.startsWith("auth/")
? handleFirebaseError(error.code)
: error?.message ||
"Connexion Apple impossible. Merci de réessayer.";
setTooltip({ text: message, type: "error" });
setIsLoading(false);
}
}, [isAppleSignInAvailable, isLoading, onSuccess, setTooltip]);
return {
signInWithGoogle,
signInWithApple,
isAppleSignInAvailable,
isLoading,
};
};
export default useSocialAuth;