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
@@ -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%",
+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;
+11 -2
View File
@@ -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",
+7 -1
View File
@@ -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",
+19 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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",
},
+44 -9
View File
@@ -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 tencourageons à 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
View File
@@ -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 tinscrivant, tu acceptes nos conditions générales{"\n"}
dutilisation 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 tinscrivant, tu acceptes nos conditions générales{"\n"}
dutilisation 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,