Fixes and tickets
This commit is contained in:
@@ -8,7 +8,7 @@ import React, {
|
||||
} from "react";
|
||||
import {
|
||||
Animated,
|
||||
Image,
|
||||
ImageBackground,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
View,
|
||||
@@ -43,8 +43,6 @@ const STAGE_CARD_CONTENT = [
|
||||
|
||||
const WEB_SCROLL_INACTIVE_DELTA = 0.05;
|
||||
const HOME_BACKGROUND_COLOR = "#425B87"; // Derived from the bottom color of the home hero image
|
||||
const AnimatedImage = Animated.createAnimatedComponent(Image);
|
||||
|
||||
const FeatureCarousel = ({
|
||||
style,
|
||||
selectedProject,
|
||||
@@ -117,16 +115,6 @@ const FeatureCarousel = ({
|
||||
);
|
||||
const onActiveIndexChangeRef = useRef(onActiveIndexChange);
|
||||
const scrollY = useRef(new Animated.Value(0)).current;
|
||||
const parallaxTranslateY = useMemo(() => {
|
||||
if (!itemHeight) {
|
||||
return null;
|
||||
}
|
||||
return scrollY.interpolate({
|
||||
inputRange: [0, itemHeight],
|
||||
outputRange: [0, -itemHeight * 0.25],
|
||||
extrapolate: "clamp",
|
||||
});
|
||||
}, [itemHeight, scrollY]);
|
||||
|
||||
useEffect(() => {
|
||||
onActiveIndexChangeRef.current = onActiveIndexChange;
|
||||
@@ -319,10 +307,19 @@ const FeatureCarousel = ({
|
||||
[updateSnapHeight]
|
||||
);
|
||||
|
||||
const hasBackgroundImage = !!backgroundImage;
|
||||
|
||||
const renderContent = useMemo(
|
||||
() =>
|
||||
carouselItems.map((item, index) => {
|
||||
const card = (
|
||||
carouselItems.map((item, index) => (
|
||||
<View
|
||||
key={item.key}
|
||||
style={[
|
||||
styles.slide,
|
||||
hasBackgroundImage ? styles.slideTransparent : styles.slideColored,
|
||||
itemHeight ? { height: itemHeight } : { minHeight: 1 },
|
||||
]}
|
||||
>
|
||||
<View style={styles.cardWrapper}>
|
||||
<PersonaCard
|
||||
item={item}
|
||||
@@ -331,55 +328,9 @@ const FeatureCarousel = ({
|
||||
height={itemHeight}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const containerStyle = [styles.backgroundItem, { height: itemHeight }];
|
||||
const isFirstItem = index === 0 && backgroundImage;
|
||||
|
||||
if (isFirstItem) {
|
||||
const parallaxStyles = [
|
||||
styles.parallaxImage,
|
||||
itemHeight ? { height: itemHeight * 1.2 } : null,
|
||||
itemHeight ? { top: -itemHeight * 0.1 } : null,
|
||||
{ width: "100%" },
|
||||
].filter(Boolean);
|
||||
if (parallaxTranslateY) {
|
||||
parallaxStyles.push({
|
||||
transform: [{ translateY: parallaxTranslateY }],
|
||||
});
|
||||
}
|
||||
return (
|
||||
<View
|
||||
key={item.key}
|
||||
style={[...containerStyle, styles.parallaxContainer]}
|
||||
>
|
||||
<AnimatedImage
|
||||
source={backgroundImage}
|
||||
style={parallaxStyles}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
"rgba(66, 91, 135, 0)",
|
||||
"rgba(66, 91, 135, 0.4)",
|
||||
HOME_BACKGROUND_COLOR,
|
||||
]}
|
||||
locations={[0, 0.75, 1]}
|
||||
style={styles.backgroundGradient}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
{card}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={item.key} style={[containerStyle, styles.plainBackground]}>
|
||||
{card}
|
||||
</View>
|
||||
);
|
||||
}),
|
||||
[backgroundImage, carouselItems, itemHeight, parallaxTranslateY]
|
||||
</View>
|
||||
)),
|
||||
[carouselItems, hasBackgroundImage, itemHeight]
|
||||
);
|
||||
|
||||
const snapOffsets = useMemo(() => {
|
||||
@@ -398,11 +349,39 @@ const FeatureCarousel = ({
|
||||
[isWeb]
|
||||
);
|
||||
|
||||
const containerProps = hasBackgroundImage
|
||||
? {
|
||||
source: backgroundImage,
|
||||
resizeMode: "cover",
|
||||
imageStyle: styles.backgroundImage,
|
||||
}
|
||||
: {};
|
||||
|
||||
const ContainerComponent = hasBackgroundImage ? ImageBackground : View;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[styles.container, isWeb && styles.containerWeb, style]}
|
||||
<ContainerComponent
|
||||
{...containerProps}
|
||||
style={[
|
||||
styles.container,
|
||||
isWeb && styles.containerWeb,
|
||||
hasBackgroundImage ? styles.containerTransparent : styles.containerColored,
|
||||
style,
|
||||
]}
|
||||
onLayout={handleLayout}
|
||||
>
|
||||
{hasBackgroundImage && (
|
||||
<LinearGradient
|
||||
colors={[
|
||||
"rgba(66, 91, 135, 0)",
|
||||
"rgba(66, 91, 135, 0.4)",
|
||||
HOME_BACKGROUND_COLOR,
|
||||
]}
|
||||
locations={[0, 0.75, 1]}
|
||||
style={styles.backgroundGradient}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
)}
|
||||
<Animated.ScrollView
|
||||
ref={scrollViewRef}
|
||||
showsVerticalScrollIndicator={false}
|
||||
@@ -440,7 +419,7 @@ const FeatureCarousel = ({
|
||||
baseDotSize={8}
|
||||
/>
|
||||
</BlurView>
|
||||
</View>
|
||||
</ContainerComponent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -451,37 +430,38 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
backgroundColor: HOME_BACKGROUND_COLOR,
|
||||
},
|
||||
containerWeb: {
|
||||
// paddingRight: 56,
|
||||
},
|
||||
containerColored: {
|
||||
backgroundColor: HOME_BACKGROUND_COLOR,
|
||||
},
|
||||
containerTransparent: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
list: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
backgroundItem: {
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
parallaxContainer: {
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
backgroundColor: HOME_BACKGROUND_COLOR,
|
||||
},
|
||||
parallaxImage: {
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundImage: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
backgroundGradient: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
},
|
||||
plainBackground: {
|
||||
slide: {
|
||||
width: "100%",
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
slideColored: {
|
||||
backgroundColor: HOME_BACKGROUND_COLOR,
|
||||
},
|
||||
slideTransparent: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
cardWrapper: {
|
||||
flex: 1,
|
||||
width: "60%",
|
||||
|
||||
@@ -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;
|
||||
@@ -2,20 +2,29 @@ import { Motion } from "@legendapp/motion";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import React, { useState } from "react";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||
import useLayoutType from "../hooks/useLayoutType";
|
||||
import { responsiveHeight } from "../actions/responsiveSizes";
|
||||
import { gutters, Palette, Style } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
|
||||
const TabBar = ({ state = {}, descriptors = {}, navigation = {} }) => {
|
||||
const { isWeb } = useLayoutType();
|
||||
const { isWeb, isIOS } = useLayoutType();
|
||||
const [pressedIndex, setPressedIndex] = useState(null);
|
||||
const { bottom: bottomInset = 0 } = useSafeAreaInsets();
|
||||
const fallbackBottomOffset = responsiveHeight(2, true);
|
||||
const bottomOffset = isWeb
|
||||
? gutters
|
||||
: isIOS
|
||||
? Math.max(bottomInset, fallbackBottomOffset)
|
||||
: bottomInset + fallbackBottomOffset;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: isWeb ? gutters : 5,
|
||||
bottom: bottomOffset,
|
||||
width: "90%",
|
||||
maxWidth: 500,
|
||||
alignSelf: "center",
|
||||
|
||||
@@ -46,11 +46,17 @@ export default ({ navigation }) => {
|
||||
type: "string",
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
title: "Notifications push",
|
||||
key: "notifications",
|
||||
value: !!currentUserData?.notifications,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
title: "Notifications par email",
|
||||
key: "emailNotifications",
|
||||
value: !!currentUserData?.emailNotifications,
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
title: "Langue",
|
||||
key: "language",
|
||||
|
||||
@@ -371,13 +371,19 @@ const Home = ({ navigation, route }) => {
|
||||
simpleRef: true,
|
||||
});
|
||||
const videoUrl = isWeb ? video?.landingWeb : video?.landing;
|
||||
const homeBackgroundImage = isWeb
|
||||
? background.homeBGWeb
|
||||
: background.homeBG;
|
||||
const landingBackgroundImage = homeBackgroundImage;
|
||||
|
||||
return adventureStarted ? (
|
||||
<Page
|
||||
shareBtn
|
||||
headerType="NONE"
|
||||
containerStyle={{ width: "100%", maxWidth: "100%", paddingHorizontal: 0 }}
|
||||
backgroundImg={homeBackgroundImage}
|
||||
>
|
||||
<View style={styles.root}>
|
||||
<View style={[styles.root, isWeb ? styles.rootWeb : styles.rootNative]}>
|
||||
<View
|
||||
style={{
|
||||
zIndex: 10,
|
||||
@@ -418,7 +424,7 @@ const Home = ({ navigation, route }) => {
|
||||
stageStates={stageStates}
|
||||
activeIndex={activeStageIndex}
|
||||
onActiveIndexChange={setActiveStageIndex}
|
||||
backgroundImage={background.homeBGWeb}
|
||||
backgroundImage={homeBackgroundImage}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
@@ -464,7 +470,11 @@ const Home = ({ navigation, route }) => {
|
||||
</Page>
|
||||
) : (
|
||||
<>
|
||||
<Page shareBtn title="Landing Page" backgroundImg={background.homeBGWeb}>
|
||||
<Page
|
||||
shareBtn
|
||||
title="Landing Page"
|
||||
backgroundImg={landingBackgroundImage}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -493,7 +503,12 @@ export default Home;
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
backgroundColor: "#425B87",
|
||||
},
|
||||
rootWeb: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
rootNative: {
|
||||
backgroundColor: "rgba(66, 91, 135, 0.3)",
|
||||
},
|
||||
heroImage: {
|
||||
position: "absolute",
|
||||
|
||||
+21
-37
@@ -1,13 +1,18 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
Platform,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { background } from "../assets";
|
||||
import FullscreenIntroVideo from "../components/FullscreenIntroVideo";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import { usersRef, videosRef } from "../config/firebase";
|
||||
import { videosRef } from "../config/firebase";
|
||||
import useDataFromRef from "../hooks/useDataFromRef";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import Page from "../layouts/Page";
|
||||
import { Routes } from "../navigation/Routes";
|
||||
import { useUser } from "../providers/UserDataProvider";
|
||||
@@ -19,13 +24,9 @@ const LANGUAGE_STORAGE_KEY = "preferredLanguage";
|
||||
export default function LandingPage() {
|
||||
const navigation = useNavigation();
|
||||
const { currentUID } = useUser();
|
||||
const [isVideoVisible, setIsVideoVisible] = useState(false);
|
||||
const [videoUrl, setVideoUrl] = useState(null);
|
||||
const [selectedLanguage, setSelectedLanguage] = useState(null);
|
||||
|
||||
const handleStartVisit = useCallback(() => {
|
||||
setIsVideoVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleSelectLanguage = useCallback((language) => {
|
||||
setSelectedLanguage(language);
|
||||
AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, language).catch((error) => {
|
||||
@@ -33,33 +34,10 @@ export default function LandingPage() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSashaHome = useCallback(() => {
|
||||
// Placeholder handler until the Sasha experience is available.
|
||||
}, []);
|
||||
|
||||
const handleLaunchAdventure = useCallback(() => {
|
||||
navigation.navigate(Routes.Register);
|
||||
}, [navigation]);
|
||||
|
||||
const persistAdventureStarted = useCallback(async () => {
|
||||
if (!currentUID) {
|
||||
return;
|
||||
}
|
||||
await usersRef
|
||||
.doc(currentUID)
|
||||
.set({ adventureStarted: true }, { merge: true });
|
||||
}, [currentUID]);
|
||||
|
||||
const handleVideoClose = useCallback(() => {
|
||||
setIsVideoVisible(false);
|
||||
persistAdventureStarted()
|
||||
.catch((error) => {
|
||||
console.warn("LandingPage: adventureStarted update failed", error);
|
||||
})
|
||||
.finally(() => {
|
||||
navigation.navigate(Routes.Login);
|
||||
});
|
||||
}, [navigation, persistAdventureStarted]);
|
||||
const { data: video } = useDataFromRef({
|
||||
ref: videosRef.doc("fr"),
|
||||
simpleRef: true,
|
||||
@@ -118,12 +96,18 @@ export default function LandingPage() {
|
||||
<View style={styles.actions}>
|
||||
<GradientButton
|
||||
title="Découvrir Musicland"
|
||||
onPress={handleStartVisit}
|
||||
onPress={() => setVideoUrl(video?.introWeb)}
|
||||
containerStyle={styles.actionButton}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Accueil avec Sasha"
|
||||
onPress={handleSashaHome}
|
||||
title="Présentation de Musicland"
|
||||
onPress={() =>
|
||||
setVideoUrl(
|
||||
Platform.OS === "web"
|
||||
? video?.landingWeb
|
||||
: video?.landing,
|
||||
)
|
||||
}
|
||||
containerStyle={styles.actionButton}
|
||||
/>
|
||||
<GradientButton
|
||||
@@ -137,9 +121,9 @@ export default function LandingPage() {
|
||||
</View>
|
||||
</Page>
|
||||
<FullscreenIntroVideo
|
||||
url={isWeb ? video?.landingWeb : video?.landing}
|
||||
visible={isVideoVisible}
|
||||
onClose={handleVideoClose}
|
||||
url={videoUrl}
|
||||
visible={!!videoUrl}
|
||||
onClose={() => setVideoUrl(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+100
-233
@@ -1,18 +1,7 @@
|
||||
/* 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 * as AppleAuthentication from "expo-apple-authentication";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { ActivityIndicator, Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
import { useGlobal } from "reactn";
|
||||
import { handleFirebaseError } from "../actions/signupActions.js";
|
||||
@@ -21,11 +10,7 @@ import GradientButton from "../components/GradientButton.js";
|
||||
import { Input } from "../components/Input.js";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||
import firebase from "../config/firebase";
|
||||
import {
|
||||
GOOGLE_ANDROID_CLIENT_ID,
|
||||
GOOGLE_IOS_CLIENT_ID,
|
||||
GOOGLE_WEB_CLIENT_ID,
|
||||
} from "../data/keys";
|
||||
import useSocialAuth from "../hooks/useSocialAuth";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import Page from "../layouts/Page.js";
|
||||
import { Routes } from "../navigation";
|
||||
@@ -34,37 +19,26 @@ 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 afterLoginNavigate = useCallback(async () => {
|
||||
const uid = firebase.auth().currentUser?.uid;
|
||||
if (!uid) throw new Error("Aucun utilisateur après connexion");
|
||||
navigation.reset({ index: 0, routes: [{ name: Routes.BottomTab }] });
|
||||
};
|
||||
}, [navigation]);
|
||||
|
||||
const {
|
||||
signInWithGoogle,
|
||||
signInWithApple,
|
||||
isAppleSignInAvailable,
|
||||
isLoading: socialLoading,
|
||||
} = useSocialAuth({
|
||||
onSuccess: afterLoginNavigate,
|
||||
});
|
||||
|
||||
const isBusy = loading || socialLoading;
|
||||
|
||||
const onLogin = async () => {
|
||||
try {
|
||||
@@ -83,110 +57,6 @@ export default ({ navigation }) => {
|
||||
}
|
||||
};
|
||||
|
||||
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}
|
||||
@@ -241,97 +111,95 @@ export default ({ navigation }) => {
|
||||
<GradientButton
|
||||
title="Se connecter"
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
width: "90%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={onLogin}
|
||||
disabled={loading || !email || !password}
|
||||
disabled={isBusy || !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 style={styles.socialWrapper}>
|
||||
{isAppleSignInAvailable ? (
|
||||
<AppleAuthButton
|
||||
onPress={signInWithApple}
|
||||
disabled={isBusy}
|
||||
loading={socialLoading}
|
||||
/>
|
||||
) : null}
|
||||
<GoogleAuthButton
|
||||
onPress={signInWithGoogle}
|
||||
disabled={isBusy}
|
||||
loading={socialLoading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</ItemContainer>
|
||||
<View style={styles.bottomFooter}>
|
||||
<Text style={styles.bottomFooterText}>
|
||||
Tu n'as pas de compte ?{" "}
|
||||
<Text
|
||||
style={styles.bottomFooterLink}
|
||||
onPress={() => navigate(Routes.Register)}
|
||||
>
|
||||
Créer un compte
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
const GoogleAuthButton = ({ onPress, disabled, loading }) => {
|
||||
const AppleAuthButton = ({ 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 null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.nativeButtonWrapper}>
|
||||
<GoogleSigninButton
|
||||
<AppleAuthentication.AppleAuthenticationButton
|
||||
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
|
||||
buttonStyle={
|
||||
AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
|
||||
}
|
||||
style={styles.nativeButton}
|
||||
size={GoogleSigninButton.Size.Wide}
|
||||
color={GoogleSigninButton.Color.Dark}
|
||||
cornerRadius={12}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{loading ? (
|
||||
<View style={styles.nativeLoadingOverlay}>
|
||||
<ActivityIndicator size="small" color={Palette.white} />
|
||||
<ActivityIndicator size="small" color={Palette.black} />
|
||||
</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 GoogleAuthButton = ({ onPress, disabled, loading }) => (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
style={({ pressed }) => [
|
||||
styles.socialButton,
|
||||
pressed && !disabled ? styles.socialButtonPressed : null,
|
||||
disabled ? styles.socialButtonDisabled : null,
|
||||
]}
|
||||
>
|
||||
<View style={styles.socialButtonContent}>
|
||||
<View style={styles.socialIconContainer}>
|
||||
<GoogleLogo />
|
||||
</View>
|
||||
<Text style={styles.socialButtonText}>Continuer avec Google</Text>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color="#4285F4" />
|
||||
) : (
|
||||
<View style={styles.socialRightSpacer} />
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
@@ -347,6 +215,7 @@ const styles = StyleSheet.create({
|
||||
cardContent: {
|
||||
gap: 28,
|
||||
paddingTop: 8,
|
||||
paddingHorizontal: 5,
|
||||
width: "100%",
|
||||
},
|
||||
greetingSection: {
|
||||
@@ -384,31 +253,29 @@ const styles = StyleSheet.create({
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "right",
|
||||
},
|
||||
signupWrapper: {
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
bottomFooter: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
signupPrompt: {
|
||||
bottomFooterText: {
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
|
||||
textAlign: "center",
|
||||
},
|
||||
signupPressable: {
|
||||
paddingHorizontal: 2,
|
||||
},
|
||||
signupLink: {
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
bottomFooterLink: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
color: Palette.white,
|
||||
},
|
||||
webButton: {
|
||||
socialWrapper: {
|
||||
width: "100%",
|
||||
gap: 16,
|
||||
alignItems: "center",
|
||||
},
|
||||
socialButton: {
|
||||
alignSelf: "center",
|
||||
borderRadius: 12,
|
||||
width: "80%",
|
||||
|
||||
width: "90%",
|
||||
backgroundColor: Palette.white,
|
||||
borderWidth: 1,
|
||||
borderColor: "#E3E7EF",
|
||||
@@ -420,38 +287,38 @@ const styles = StyleSheet.create({
|
||||
shadowRadius: 16,
|
||||
elevation: 2,
|
||||
},
|
||||
webButtonPressed: {
|
||||
socialButtonPressed: {
|
||||
backgroundColor: "#F4F7FF",
|
||||
},
|
||||
webButtonDisabled: {
|
||||
socialButtonDisabled: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
webContent: {
|
||||
socialButtonContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
},
|
||||
webIconContainer: {
|
||||
socialIconContainer: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
},
|
||||
webText: {
|
||||
socialButtonText: {
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
fontSize: 15,
|
||||
color: "#202124",
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
webRightSpacer: {
|
||||
socialRightSpacer: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
nativeButtonWrapper: {
|
||||
alignSelf: "center",
|
||||
width: "80%",
|
||||
width: "90%",
|
||||
height: 50,
|
||||
justifyContent: "center",
|
||||
},
|
||||
|
||||
@@ -17,21 +17,28 @@ export default (props) => {
|
||||
const [currentUserData] = useGlobal("currentUserData");
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
|
||||
const [isSwitch, setIsSwitch] = useState(false);
|
||||
const [isPushSwitch, setIsPushSwitch] = useState(false);
|
||||
const [isEmailSwitch, setIsEmailSwitch] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsSwitch(!!currentUserData?.notifications);
|
||||
setIsPushSwitch(!!currentUserData?.notifications);
|
||||
}, [currentUserData?.notifications]);
|
||||
|
||||
const onToggle = async (next) => {
|
||||
useEffect(() => {
|
||||
setIsEmailSwitch(!!currentUserData?.emailNotifications);
|
||||
}, [currentUserData?.emailNotifications]);
|
||||
|
||||
const updateNotificationPreference = async (next, field, setState) => {
|
||||
try {
|
||||
setIsSwitch(next);
|
||||
setState(next);
|
||||
const uid = currentUID || firebase.auth().currentUser?.uid;
|
||||
if (!uid) return;
|
||||
await usersRef.doc(uid).set({ notifications: next }, { merge: true });
|
||||
if (!uid) {
|
||||
throw new Error("Utilisateur non connecté");
|
||||
}
|
||||
await usersRef.doc(uid).set({ [field]: next }, { merge: true });
|
||||
setTooltip({ type: "success", text: "Préférence enregistrée" });
|
||||
} catch (e) {
|
||||
setIsSwitch(!next);
|
||||
setState(!next);
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: e?.message || "Enregistrement impossible",
|
||||
@@ -39,6 +46,12 @@ export default (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const onTogglePush = (next) =>
|
||||
updateNotificationPreference(next, "notifications", setIsPushSwitch);
|
||||
|
||||
const onToggleEmail = (next) =>
|
||||
updateNotificationPreference(next, "emailNotifications", setIsEmailSwitch);
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerType="NAVIGATE"
|
||||
@@ -74,9 +87,9 @@ export default (props) => {
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
|
||||
}}
|
||||
>
|
||||
Notifications
|
||||
Notifications push
|
||||
</Text>
|
||||
<Switch value={isSwitch} setValue={onToggle} />
|
||||
<Switch value={isPushSwitch} setValue={onTogglePush} />
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
@@ -88,6 +101,28 @@ export default (props) => {
|
||||
Nous t’encourageons à activer les notifications pour découvrir les
|
||||
derniers classements, les nouveaux sons et les meilleurs playbacks.
|
||||
</Text>
|
||||
<View style={Style.containerSpaceBetween}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueMedium,
|
||||
}}
|
||||
>
|
||||
Notifications par email
|
||||
</Text>
|
||||
<Switch value={isEmailSwitch} setValue={onToggleEmail} />
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Reste informé en recevant nos nouveautés et rappels directement dans
|
||||
ta boîte mail.
|
||||
</Text>
|
||||
</BlurView>
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
+345
-104
@@ -1,23 +1,32 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import * as AppleAuthentication from "expo-apple-authentication";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
|
||||
import { BlurView } from "expo-blur";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
import { background } from "../assets";
|
||||
import BorderGradientButton from "../components/BorderGradientButton";
|
||||
import { Input } from "../components/Input";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||
import COUNTRIES from "../constants/countries";
|
||||
import firebase from "../config/firebase";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import useSocialAuth from "../hooks/useSocialAuth";
|
||||
import Page from "../layouts/Page";
|
||||
import { Routes } from "../navigation";
|
||||
import { navigate } from "../navigation/NavigationService";
|
||||
import { navigate, reset } from "../navigation/NavigationService";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
|
||||
@@ -33,6 +42,23 @@ const Register = () => {
|
||||
const [isCountryModalVisible, setIsCountryModalVisible] = useState(false);
|
||||
const [preferredLanguage, setPreferredLanguage] = useState(null);
|
||||
|
||||
const afterSocialAuth = useCallback(async () => {
|
||||
const uid = firebase.auth().currentUser?.uid;
|
||||
if (!uid) throw new Error("Aucun utilisateur après connexion");
|
||||
reset({ index: 0, routes: [{ name: Routes.BottomTab }] });
|
||||
}, []);
|
||||
|
||||
const {
|
||||
signInWithGoogle,
|
||||
signInWithApple,
|
||||
isAppleSignInAvailable,
|
||||
isLoading: socialLoading,
|
||||
} = useSocialAuth({
|
||||
onSuccess: afterSocialAuth,
|
||||
});
|
||||
|
||||
const isBusy = socialLoading;
|
||||
|
||||
useEffect(() => {
|
||||
AsyncStorage.getItem(LANGUAGE_STORAGE_KEY)
|
||||
.then((storedLanguage) => {
|
||||
@@ -51,7 +77,7 @@ const Register = () => {
|
||||
return COUNTRIES;
|
||||
}
|
||||
return COUNTRIES.filter((country) =>
|
||||
country.name.toLowerCase().includes(query)
|
||||
country.name.toLowerCase().includes(query),
|
||||
);
|
||||
}, [countrySearch]);
|
||||
|
||||
@@ -73,6 +99,118 @@ const Register = () => {
|
||||
setCountrySearch("");
|
||||
};
|
||||
|
||||
const renderFormContent = () => (
|
||||
<View style={styles.formContent}>
|
||||
<Text style={styles.title}>Renseigne tes informations</Text>
|
||||
{preferredLanguage && !isWeb ? (
|
||||
<Text style={styles.languageInfo}>
|
||||
Langue choisie :{" "}
|
||||
<Text style={styles.languageInfoValue}>
|
||||
{preferredLanguage === "fr" ? "Français" : "English"}
|
||||
</Text>
|
||||
</Text>
|
||||
) : null}
|
||||
<View style={{ gap: 16 }}>
|
||||
<View style={{ gap: 12 }}>
|
||||
<Input
|
||||
placeholder="Prénom"
|
||||
label="Prénom"
|
||||
isBlur
|
||||
value={firstName}
|
||||
setValue={setFirstName}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Nom"
|
||||
label="Nom"
|
||||
isBlur
|
||||
value={lastName}
|
||||
setValue={setLastName}
|
||||
/>
|
||||
<View style={styles.countryField}>
|
||||
<Text style={styles.inputLabel}>Pays</Text>
|
||||
<BlurView
|
||||
tint="dark"
|
||||
style={[
|
||||
styles.countryBlurWrapper,
|
||||
selectedCountry ? styles.countryButtonFilled : null,
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.countryButton,
|
||||
pressed ? styles.countryButtonPressed : null,
|
||||
]}
|
||||
onPress={() => setIsCountryModalVisible(true)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.countryButtonText,
|
||||
!selectedCountry && styles.countryButtonPlaceholder,
|
||||
]}
|
||||
>
|
||||
{selectedCountry ? selectedCountry.name : "Choisis ton pays"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</BlurView>
|
||||
</View>
|
||||
<Input
|
||||
placeholder="Ville"
|
||||
label="Ville"
|
||||
isBlur
|
||||
value={city}
|
||||
setValue={setCity}
|
||||
/>
|
||||
</View>
|
||||
<Input
|
||||
placeholder="Adresse mail"
|
||||
label="Adresse mail"
|
||||
isBlur
|
||||
type="email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
/>
|
||||
</View>
|
||||
<BorderGradientButton
|
||||
title="Créer mon compte"
|
||||
containerStyle={{
|
||||
width: "90%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.CreatePassword, {
|
||||
email: email.trim(),
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
city: city.trim(),
|
||||
country: selectedCountry,
|
||||
preferredLanguage,
|
||||
})
|
||||
}
|
||||
disabled={!isFormValid || isBusy}
|
||||
/>
|
||||
<View style={styles.socialWrapper}>
|
||||
{isAppleSignInAvailable ? (
|
||||
<AppleAuthButton
|
||||
onPress={signInWithApple}
|
||||
disabled={isBusy}
|
||||
loading={socialLoading}
|
||||
/>
|
||||
) : null}
|
||||
<GoogleAuthButton
|
||||
onPress={signInWithGoogle}
|
||||
disabled={isBusy}
|
||||
loading={socialLoading}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.terms}>
|
||||
En t’inscrivant, tu acceptes nos conditions générales{"\n"}
|
||||
d’utilisation et notre politique de confidentialité.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page
|
||||
width={isWeb ? 600 : null}
|
||||
@@ -81,103 +219,39 @@ const Register = () => {
|
||||
title="Inscription"
|
||||
>
|
||||
<View style={styles.pageContent}>
|
||||
<ItemContainer height={680} disableKeyboardHeight>
|
||||
<View style={styles.formContent}>
|
||||
<Text style={styles.title}>Renseigne tes informations</Text>
|
||||
{preferredLanguage && (
|
||||
<Text style={styles.languageInfo}>
|
||||
Langue choisie :{" "}
|
||||
<Text style={styles.languageInfoValue}>
|
||||
{preferredLanguage === "fr" ? "Français" : "English"}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
<View style={{ gap: 16 }}>
|
||||
<View style={{ gap: 12 }}>
|
||||
<Input
|
||||
placeholder="Prénom"
|
||||
label="Prénom"
|
||||
isBlur
|
||||
value={firstName}
|
||||
setValue={setFirstName}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Nom"
|
||||
label="Nom"
|
||||
isBlur
|
||||
value={lastName}
|
||||
setValue={setLastName}
|
||||
/>
|
||||
<View style={styles.countryField}>
|
||||
<Text style={styles.inputLabel}>Pays</Text>
|
||||
<Pressable
|
||||
style={[
|
||||
styles.countryButton,
|
||||
selectedCountry ? styles.countryButtonFilled : null,
|
||||
]}
|
||||
onPress={() => setIsCountryModalVisible(true)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.countryButtonText,
|
||||
!selectedCountry && styles.countryButtonPlaceholder,
|
||||
]}
|
||||
>
|
||||
{selectedCountry
|
||||
? selectedCountry.name
|
||||
: "Choisis ton pays"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Input
|
||||
placeholder="Ville"
|
||||
label="Ville"
|
||||
isBlur
|
||||
value={city}
|
||||
setValue={setCity}
|
||||
/>
|
||||
</View>
|
||||
<Input
|
||||
placeholder="Adresse mail"
|
||||
label="Adresse mail"
|
||||
isBlur
|
||||
type="email"
|
||||
value={email}
|
||||
setValue={setEmail}
|
||||
/>
|
||||
</View>
|
||||
<BorderGradientButton
|
||||
title="Créer mon compte"
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.CreatePassword, {
|
||||
email: email.trim(),
|
||||
firstName: firstName.trim(),
|
||||
lastName: lastName.trim(),
|
||||
city: city.trim(),
|
||||
country: selectedCountry,
|
||||
preferredLanguage,
|
||||
})
|
||||
}
|
||||
disabled={!isFormValid}
|
||||
/>
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.terms}>
|
||||
En t’inscrivant, tu acceptes nos conditions générales{"\n"}
|
||||
d’utilisation et notre politique de confidentialité.
|
||||
</Text>
|
||||
<Pressable onPress={() => navigate(Routes.Login)}>
|
||||
<Text style={styles.loginText}>
|
||||
Tu as déjà un compte?{" "}
|
||||
<Text style={styles.loginLink}>Se connecter</Text>
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
<ItemContainer
|
||||
height={isWeb ? 720 : undefined}
|
||||
disableKeyboardHeight
|
||||
style={!isWeb ? styles.mobileItemContainer : styles.webItemContainer}
|
||||
>
|
||||
{isWeb ? (
|
||||
renderFormContent()
|
||||
) : (
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
style={styles.mobileAvoider}
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
contentContainerStyle={styles.mobileScrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{renderFormContent()}
|
||||
</KeyboardAwareScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
)}
|
||||
</ItemContainer>
|
||||
<View style={styles.bottomLoginPrompt}>
|
||||
<Text style={styles.bottomFooterText}>
|
||||
Tu as déjà un compte ?{" "}
|
||||
<Text
|
||||
style={styles.bottomFooterLink}
|
||||
onPress={() => navigate(Routes.Login)}
|
||||
>
|
||||
Se connecter
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Modal
|
||||
transparent
|
||||
@@ -224,6 +298,76 @@ const Register = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const AppleAuthButton = ({ onPress, disabled, loading }) => {
|
||||
if (isWeb) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.nativeButtonWrapper}>
|
||||
<AppleAuthentication.AppleAuthenticationButton
|
||||
buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_UP}
|
||||
buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.WHITE}
|
||||
style={styles.nativeButton}
|
||||
cornerRadius={12}
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{loading ? (
|
||||
<View style={styles.nativeLoadingOverlay}>
|
||||
<ActivityIndicator size="small" color={Palette.black} />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const GoogleAuthButton = ({ onPress, disabled, loading }) => (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
style={({ pressed }) => [
|
||||
styles.socialButton,
|
||||
pressed && !disabled ? styles.socialButtonPressed : null,
|
||||
disabled ? styles.socialButtonDisabled : null,
|
||||
]}
|
||||
>
|
||||
<View style={styles.socialButtonContent}>
|
||||
<View style={styles.socialIconContainer}>
|
||||
<GoogleLogo />
|
||||
</View>
|
||||
<Text style={styles.socialButtonText}>Continuer avec Google</Text>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color="#4285F4" />
|
||||
) : (
|
||||
<View style={styles.socialRightSpacer} />
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
export default Register;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -232,6 +376,22 @@ const styles = StyleSheet.create({
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
webItemContainer: {
|
||||
width: "100%",
|
||||
maxWidth: 680,
|
||||
},
|
||||
mobileItemContainer: {
|
||||
width: "100%",
|
||||
minHeight: 720,
|
||||
},
|
||||
mobileAvoider: {
|
||||
flex: 1,
|
||||
},
|
||||
mobileScrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
paddingVertical: 24,
|
||||
},
|
||||
formContent: {
|
||||
gap: 32,
|
||||
paddingTop: 5,
|
||||
@@ -251,17 +411,19 @@ const styles = StyleSheet.create({
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
countryButton: {
|
||||
countryBlurWrapper: {
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: Palette.ultraLightWhite,
|
||||
backgroundColor: Palette.glass,
|
||||
overflow: "hidden",
|
||||
},
|
||||
countryButton: {
|
||||
minHeight: 50,
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 16,
|
||||
width: "100%",
|
||||
},
|
||||
countryButtonFilled: {
|
||||
borderColor: Palette.primary,
|
||||
countryButtonPressed: {
|
||||
opacity: 0.85,
|
||||
},
|
||||
countryButtonText: {
|
||||
fontSize: 14,
|
||||
@@ -297,6 +459,85 @@ const styles = StyleSheet.create({
|
||||
loginLink: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
bottomLoginPrompt: {
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 24,
|
||||
},
|
||||
bottomFooterText: {
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueRegular,
|
||||
textAlign: "center",
|
||||
},
|
||||
bottomFooterLink: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
color: Palette.white,
|
||||
},
|
||||
socialWrapper: {
|
||||
width: "100%",
|
||||
gap: 16,
|
||||
alignItems: "center",
|
||||
},
|
||||
socialButton: {
|
||||
alignSelf: "center",
|
||||
borderRadius: 12,
|
||||
width: "90%",
|
||||
|
||||
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,
|
||||
},
|
||||
socialButtonPressed: {
|
||||
backgroundColor: "#F4F7FF",
|
||||
},
|
||||
socialButtonDisabled: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
socialButtonContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
},
|
||||
socialIconContainer: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: 28,
|
||||
height: 28,
|
||||
},
|
||||
socialButtonText: {
|
||||
flex: 1,
|
||||
textAlign: "center",
|
||||
fontSize: 15,
|
||||
color: "#202124",
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
socialRightSpacer: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
nativeButtonWrapper: {
|
||||
alignSelf: "center",
|
||||
width: "90%",
|
||||
height: 50,
|
||||
justifyContent: "center",
|
||||
},
|
||||
nativeButton: {
|
||||
width: "100%",
|
||||
height: 50,
|
||||
},
|
||||
nativeLoadingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: Palette.transparentBlack,
|
||||
|
||||
Reference in New Issue
Block a user