495 lines
14 KiB
JavaScript
495 lines
14 KiB
JavaScript
/* eslint-disable react/display-name */
|
|
import { GoogleSigninButton } from "@react-native-google-signin/google-signin";
|
|
import * as AuthSession from "expo-auth-session";
|
|
import * as GoogleAuth from "expo-auth-session/providers/google";
|
|
import Constants from "expo-constants";
|
|
import * as WebBrowser from "expo-web-browser";
|
|
import React, { useEffect, useState } from "react";
|
|
import {
|
|
ActivityIndicator,
|
|
Platform,
|
|
Pressable,
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from "react-native";
|
|
import Svg, { Path } from "react-native-svg";
|
|
import { useGlobal } from "reactn";
|
|
import { handleFirebaseError } from "../actions/signupActions.js";
|
|
import { background } from "../assets";
|
|
import GradientButton from "../components/GradientButton.js";
|
|
import { Input } from "../components/Input.js";
|
|
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
|
import firebase, { usersRef } from "../config/firebase";
|
|
import {
|
|
GOOGLE_ANDROID_CLIENT_ID,
|
|
GOOGLE_IOS_CLIENT_ID,
|
|
GOOGLE_WEB_CLIENT_ID,
|
|
} from "../data/keys";
|
|
import { isWeb } from "../hooks/useLayoutType";
|
|
import Page from "../layouts/Page.js";
|
|
import { Routes } from "../navigation";
|
|
import { navigate } from "../navigation/NavigationService.js";
|
|
import { FONT_FAMILY } from "../styles/Fonts.js";
|
|
import Palette from "../styles/Palette.js";
|
|
|
|
export default ({ navigation }) => {
|
|
WebBrowser.maybeCompleteAuthSession();
|
|
const [, setTooltip] = useGlobal("_tooltip");
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
// Let the provider compute a compliant redirect URI for native (com.googleusercontent.apps.<client-id>:/oauth2redirect)
|
|
// Avoid forcing a custom scheme like musicland:// which Google can reject for native apps.
|
|
|
|
const isExpoGo = Constants?.appOwnership === "expo";
|
|
const expoRedirectUri = AuthSession.makeRedirectUri({ useProxy: true });
|
|
const nativeRedirectUri = getNativeGoogleRedirectUri();
|
|
const redirectUri =
|
|
isWeb || isExpoGo ? expoRedirectUri : nativeRedirectUri || expoRedirectUri;
|
|
|
|
const [request, response, promptAsync] = GoogleAuth.useAuthRequest({
|
|
iosClientId: GOOGLE_IOS_CLIENT_ID,
|
|
androidClientId: GOOGLE_ANDROID_CLIENT_ID,
|
|
webClientId: GOOGLE_WEB_CLIENT_ID,
|
|
expoClientId: GOOGLE_WEB_CLIENT_ID,
|
|
redirectUri,
|
|
// Use Authorization Code + PKCE to comply with Google OAuth for native apps
|
|
responseType: AuthSession.ResponseType.Code,
|
|
usePKCE: true,
|
|
scopes: ["openid", "profile", "email"],
|
|
});
|
|
|
|
const afterLoginNavigate = async () => {
|
|
const uid = firebase.auth().currentUser?.uid;
|
|
if (!uid) throw new Error("Aucun utilisateur après connexion");
|
|
const snap = await usersRef.doc(uid).get();
|
|
const hasUserName = !!snap.data()?.userName;
|
|
if (hasUserName) {
|
|
navigation.reset({ index: 0, routes: [{ name: Routes.BottomTab }] });
|
|
} else {
|
|
navigation.reset({ index: 0, routes: [{ name: Routes.CreatePseudo }] });
|
|
}
|
|
};
|
|
|
|
const onLogin = async () => {
|
|
try {
|
|
setLoading(true);
|
|
await firebase.auth().signInWithEmailAndPassword(email.trim(), password);
|
|
await afterLoginNavigate();
|
|
} catch (e) {
|
|
console.log("Login error", e?.message);
|
|
const message =
|
|
e?.code && e.code.startsWith("auth/")
|
|
? handleFirebaseError(e.code)
|
|
: e?.message || "Connexion impossible";
|
|
setTooltip({ text: message, type: "error" });
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const onLoginWithGoogle = async () => {
|
|
try {
|
|
setLoading(true);
|
|
if (isWeb) {
|
|
const provider = new firebase.auth.GoogleAuthProvider();
|
|
provider.addScope("profile");
|
|
provider.addScope("email");
|
|
await firebase.auth().signInWithPopup(provider);
|
|
await afterLoginNavigate();
|
|
setLoading(false);
|
|
} else {
|
|
// Use defaults from the request; don't override with proxy here
|
|
const result = await promptAsync({ useProxy: isExpoGo });
|
|
if (result?.type !== "success") {
|
|
// Cancelled or errored during the browser flow
|
|
setLoading(false);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.log("Google Login error", e?.message);
|
|
const message =
|
|
e?.code && e.code.startsWith("auth/")
|
|
? handleFirebaseError(e.code)
|
|
: e?.message;
|
|
setTooltip({
|
|
text: message || "Connexion Google impossible. Merci de réessayer.",
|
|
type: "error",
|
|
});
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Handle native Google OAuth response
|
|
useEffect(() => {
|
|
const handleNativeGoogleResponse = async () => {
|
|
try {
|
|
if (response?.type === "success") {
|
|
// If using Expo proxy, tokens can be present already.
|
|
let idToken =
|
|
response?.authentication?.idToken || response?.params?.id_token;
|
|
|
|
if (!idToken) {
|
|
// Otherwise, exchange the authorization code for tokens using PKCE.
|
|
const code =
|
|
response?.params?.code || response?.authentication?.code;
|
|
if (!code) throw new Error("Code d'autorisation Google manquant");
|
|
|
|
// Use the exact clientId used during the authorize request.
|
|
const clientId = request?.clientId;
|
|
const discovery = {
|
|
authorizationEndpoint:
|
|
"https://accounts.google.com/o/oauth2/v2/auth",
|
|
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
|
revocationEndpoint: "https://oauth2.googleapis.com/revoke",
|
|
};
|
|
|
|
// Light debug: helps identify redirect/client mismatches in dev.
|
|
console.log("Google token exchange", {
|
|
clientId,
|
|
redirectUri: request?.redirectUri,
|
|
hasCodeVerifier: !!request?.codeVerifier,
|
|
});
|
|
|
|
const tokenResponse = await AuthSession.exchangeCodeAsync(
|
|
{
|
|
clientId,
|
|
code,
|
|
redirectUri: request?.redirectUri,
|
|
extraParams: { code_verifier: request?.codeVerifier },
|
|
},
|
|
discovery
|
|
);
|
|
idToken = tokenResponse?.id_token;
|
|
}
|
|
|
|
if (!idToken)
|
|
throw new Error("Jeton Google manquant (échange/retour)");
|
|
|
|
const credential =
|
|
firebase.auth.GoogleAuthProvider.credential(idToken);
|
|
await firebase.auth().signInWithCredential(credential);
|
|
await afterLoginNavigate();
|
|
}
|
|
} catch (e) {
|
|
console.log("Google native sign-in error", e?.message);
|
|
const message =
|
|
e?.code && e.code.startsWith("auth/")
|
|
? handleFirebaseError(e.code)
|
|
: e?.message;
|
|
setTooltip({
|
|
text: message || "Connexion Google impossible",
|
|
type: "error",
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (!isWeb && response) {
|
|
handleNativeGoogleResponse();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [response]);
|
|
|
|
return (
|
|
<Page
|
|
width={isWeb ? 600 : null}
|
|
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
|
|
headerType="NAVIGATION"
|
|
title={isWeb ? "" : "Connexion"}
|
|
hideBackButton
|
|
>
|
|
<View style={styles.screen}>
|
|
<ItemContainer
|
|
height={isWeb ? 520 : 550}
|
|
disableKeyboardHeight
|
|
style={styles.loginCard}
|
|
>
|
|
<View style={styles.cardContent}>
|
|
<View style={styles.greetingSection}>
|
|
<Text style={styles.greetingTitle}>{"Bonjour !"}</Text>
|
|
<Text style={styles.greetingSubtitle}>
|
|
Nous sommes heureux de te revoir parmi nous. Prêt·e à reprendre
|
|
le rythme ?
|
|
</Text>
|
|
</View>
|
|
<View style={styles.formFields}>
|
|
<Input
|
|
placeholder="Adresse mail"
|
|
label="Adresse mail"
|
|
type="email"
|
|
value={email}
|
|
setValue={setEmail}
|
|
isBlur
|
|
/>
|
|
<View style={styles.passwordField}>
|
|
<Input
|
|
placeholder="Mot de passe"
|
|
label="Mot de passe"
|
|
type="password"
|
|
value={password}
|
|
setValue={setPassword}
|
|
isBlur
|
|
/>
|
|
<Pressable
|
|
onPress={() => navigate(Routes.ForgotPassword)}
|
|
style={({ pressed }) => [
|
|
styles.linkPressable,
|
|
pressed && styles.linkPressed,
|
|
]}
|
|
>
|
|
<Text style={styles.linkText}>Mot de passe oublié ?</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
<GradientButton
|
|
title="Se connecter"
|
|
containerStyle={{
|
|
width: "80%",
|
|
alignSelf: "center",
|
|
}}
|
|
onPress={onLogin}
|
|
disabled={loading || !email || !password}
|
|
/>
|
|
<GoogleAuthButton
|
|
onPress={onLoginWithGoogle}
|
|
disabled={loading}
|
|
loading={loading}
|
|
/>
|
|
|
|
<View style={styles.signupWrapper}>
|
|
<Text style={styles.signupPrompt}>Pas encore de compte ?</Text>
|
|
<Pressable
|
|
onPress={() => navigate(Routes.Register)}
|
|
style={({ pressed }) => [
|
|
styles.signupPressable,
|
|
pressed && styles.linkPressed,
|
|
]}
|
|
>
|
|
<Text style={styles.signupLink}>Créer un compte</Text>
|
|
</Pressable>
|
|
</View>
|
|
</View>
|
|
</ItemContainer>
|
|
</View>
|
|
</Page>
|
|
);
|
|
};
|
|
|
|
const GoogleAuthButton = ({ onPress, disabled, loading }) => {
|
|
if (isWeb) {
|
|
return (
|
|
<Pressable
|
|
accessibilityRole="button"
|
|
onPress={onPress}
|
|
disabled={disabled}
|
|
style={({ pressed }) => [
|
|
styles.webButton,
|
|
pressed && !disabled ? styles.webButtonPressed : null,
|
|
disabled ? styles.webButtonDisabled : null,
|
|
]}
|
|
>
|
|
<View style={styles.webContent}>
|
|
<View style={styles.webIconContainer}>
|
|
<GoogleLogo />
|
|
</View>
|
|
<Text style={styles.webText}>Continuer avec Google</Text>
|
|
{loading ? (
|
|
<ActivityIndicator size="small" color="#4285F4" />
|
|
) : (
|
|
<View style={styles.webRightSpacer} />
|
|
)}
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.nativeButtonWrapper}>
|
|
<GoogleSigninButton
|
|
style={styles.nativeButton}
|
|
size={GoogleSigninButton.Size.Wide}
|
|
color={GoogleSigninButton.Color.Dark}
|
|
onPress={onPress}
|
|
disabled={disabled}
|
|
/>
|
|
{loading ? (
|
|
<View style={styles.nativeLoadingOverlay}>
|
|
<ActivityIndicator size="small" color={Palette.white} />
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
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 styles = StyleSheet.create({
|
|
screen: {
|
|
flex: 1,
|
|
width: "100%",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
loginCard: {
|
|
width: "100%",
|
|
maxWidth: 520,
|
|
},
|
|
cardContent: {
|
|
gap: 28,
|
|
paddingTop: 8,
|
|
width: "100%",
|
|
},
|
|
greetingSection: {
|
|
gap: 6,
|
|
width: "100%",
|
|
},
|
|
greetingTitle: {
|
|
fontSize: 22,
|
|
color: Palette.white,
|
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
},
|
|
greetingSubtitle: {
|
|
fontSize: 14,
|
|
color: Palette.gray,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
lineHeight: 20,
|
|
},
|
|
formFields: {
|
|
gap: 20,
|
|
width: "100%",
|
|
},
|
|
passwordField: {
|
|
gap: 8,
|
|
width: "100%",
|
|
},
|
|
linkPressable: {
|
|
alignSelf: "flex-end",
|
|
},
|
|
linkPressed: {
|
|
opacity: 0.6,
|
|
},
|
|
linkText: {
|
|
fontSize: 12,
|
|
color: Palette.white,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
textAlign: "right",
|
|
},
|
|
signupWrapper: {
|
|
flexDirection: "row",
|
|
flexWrap: "wrap",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
gap: 6,
|
|
},
|
|
signupPrompt: {
|
|
fontSize: 14,
|
|
color: Palette.white,
|
|
fontFamily: FONT_FAMILY.InterRegular,
|
|
},
|
|
signupPressable: {
|
|
paddingHorizontal: 2,
|
|
},
|
|
signupLink: {
|
|
fontSize: 14,
|
|
color: Palette.white,
|
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
},
|
|
webButton: {
|
|
alignSelf: "center",
|
|
borderRadius: 12,
|
|
width: "80%",
|
|
|
|
backgroundColor: Palette.white,
|
|
borderWidth: 1,
|
|
borderColor: "#E3E7EF",
|
|
paddingVertical: 12,
|
|
paddingHorizontal: 20,
|
|
shadowColor: "rgba(12, 6, 34, 0.08)",
|
|
shadowOffset: { width: 0, height: 8 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 16,
|
|
elevation: 2,
|
|
},
|
|
webButtonPressed: {
|
|
backgroundColor: "#F4F7FF",
|
|
},
|
|
webButtonDisabled: {
|
|
opacity: 0.7,
|
|
},
|
|
webContent: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: 12,
|
|
},
|
|
webIconContainer: {
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
width: 28,
|
|
height: 28,
|
|
},
|
|
webText: {
|
|
flex: 1,
|
|
textAlign: "center",
|
|
fontSize: 15,
|
|
color: "#202124",
|
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
|
},
|
|
webRightSpacer: {
|
|
width: 24,
|
|
height: 24,
|
|
},
|
|
nativeButtonWrapper: {
|
|
alignSelf: "center",
|
|
width: "80%",
|
|
height: 50,
|
|
justifyContent: "center",
|
|
},
|
|
nativeButton: {
|
|
width: "100%",
|
|
height: 50,
|
|
},
|
|
nativeLoadingOverlay: {
|
|
...StyleSheet.absoluteFillObject,
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
},
|
|
});
|
|
|
|
const GoogleLogo = () => (
|
|
<Svg width={24} height={24} viewBox="0 0 18 18">
|
|
<Path
|
|
fill="#4285F4"
|
|
d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.71v2.26h2.9c1.68-1.55 2.7-3.83 2.7-6.61Z"
|
|
/>
|
|
<Path
|
|
fill="#34A853"
|
|
d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.9-2.26c-.8.54-1.83.86-3.06.86a5.31 5.31 0 0 1-5.02-3.69H.96v2.32A9 9 0 0 0 9 18Z"
|
|
/>
|
|
<Path
|
|
fill="#FBBC05"
|
|
d="M3.98 10.73c-.18-.54-.28-1.12-.28-1.73s.1-1.19.28-1.73V4.95H.96A9 9 0 0 0 0 9c0 1.45.35 2.82.96 4.05l3.02-2.32Z"
|
|
/>
|
|
<Path
|
|
fill="#EA4335"
|
|
d="M9 3.54c1.32 0 2.5.45 3.43 1.33l2.57-2.57C13.47.9 11.43 0 9 0A9 9 0 0 0 .96 4.95l3.02 2.32A5.31 5.31 0 0 1 9 3.54Z"
|
|
/>
|
|
</Svg>
|
|
);
|