Fixes and tickets
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user