home, subscription and other fixes
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useGlobal } from "reactn";
|
||||
import React, { useState, useGlobal } from "reactn";
|
||||
import { Text, KeyboardAvoidingView } from "react-native";
|
||||
import { responsiveHeight } from "../actions/responsiveSizes.js";
|
||||
|
||||
@@ -11,6 +11,10 @@ import { Fonts, Style } from "../styles";
|
||||
import Page from "../layouts/Page";
|
||||
import { background } from "../assets";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import {
|
||||
checkIfEmailIsValid,
|
||||
handleFirebaseError,
|
||||
} from "../actions/signupActions.js";
|
||||
|
||||
export default ({ navigation }) => {
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
@@ -19,10 +23,27 @@ export default ({ navigation }) => {
|
||||
const [email, setEmail] = useState(__DEV__ ? "hello@minuit.agency" : "");
|
||||
|
||||
const onResetPassword = async () => {
|
||||
const trimmedEmail = (email || "").trim();
|
||||
if (!trimmedEmail?.length) {
|
||||
setTooltip({
|
||||
text: "Renseigne ton adresse e-mail.",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkIfEmailIsValid({ email: trimmedEmail })) {
|
||||
setTooltip({
|
||||
text: "Adresse e-mail invalide.",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
await firebase.auth().sendPasswordResetEmail(email);
|
||||
await firebase.auth().sendPasswordResetEmail(trimmedEmail);
|
||||
|
||||
setTooltip({
|
||||
text: "Email de réinitialisation envoyé!",
|
||||
@@ -31,9 +52,13 @@ export default ({ navigation }) => {
|
||||
|
||||
navigation.goBack();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log("ForgotPassword error", error?.message);
|
||||
const message =
|
||||
error?.code && error.code.startsWith("auth/")
|
||||
? handleFirebaseError(error.code)
|
||||
: error?.message || "Une erreur est survenue";
|
||||
setTooltip({
|
||||
text: "Une erreur est survenue",
|
||||
text: message,
|
||||
type: "error",
|
||||
});
|
||||
} finally {
|
||||
|
||||
+226
-226
@@ -5,13 +5,11 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import { background, videos } from "../../assets";
|
||||
import { background, cardsImg, icons, videos } from "../../assets";
|
||||
import Alert from "../../components/Alert";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel";
|
||||
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MoreMenu from "../../components/MoreMenu";
|
||||
@@ -26,11 +24,13 @@ import { navigate } from "../../navigation/NavigationService";
|
||||
import { Routes } from "../../navigation/Routes";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import {
|
||||
findFirstUnlockedStageIndex,
|
||||
getCreationStageStates,
|
||||
getStageAction,
|
||||
} from "../../utils/projectStages";
|
||||
import StageCard from "./components/StageCard";
|
||||
import ClubCard from "./components/ClubCard";
|
||||
|
||||
const isProjectEmpty = (project) => {
|
||||
if (!project) {
|
||||
@@ -42,8 +42,55 @@ const isProjectEmpty = (project) => {
|
||||
);
|
||||
};
|
||||
|
||||
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520;
|
||||
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360;
|
||||
|
||||
const STAGE_CARD_CONTENT = [
|
||||
{
|
||||
key: "songwriter",
|
||||
step: "ÉTAPE 1",
|
||||
description:
|
||||
"Ensemble, nous allons écrire ta chanson.\nMéthodiquement je vais te guider pour construire une oeuvre unique et authentique.",
|
||||
image: cardsImg.writing,
|
||||
imagePosition: "left",
|
||||
textAlign: "left",
|
||||
lockSide: "right",
|
||||
},
|
||||
{
|
||||
key: "beatmaker",
|
||||
step: "ÉTAPE 2",
|
||||
description:
|
||||
"Je suis Malik, responsable du studio de musicland, je vais mettre en musique tes paroles en fonction de tes goûts musicaux, ça va être top!",
|
||||
image: cardsImg.studio,
|
||||
imagePosition: "right",
|
||||
textAlign: "right",
|
||||
lockSide: "left",
|
||||
},
|
||||
{
|
||||
key: "director",
|
||||
step: "ÉTAPE 3",
|
||||
description:
|
||||
"Tu vas faire une expérience extraordinaire, tu vas te filmer en train d’interpreter ta chanson en Play Back et je vais te guider pour te faciliter la tâche!",
|
||||
image: cardsImg.video,
|
||||
imagePosition: "left",
|
||||
textAlign: "left",
|
||||
lockSide: "right",
|
||||
},
|
||||
{
|
||||
key: "publisher",
|
||||
step: "ÉTAPE 4",
|
||||
description:
|
||||
"Je suis Mr Benhaï, Producteur de MusicLand et je vais te faire une proposition qui pourrait t’intéresser, on se retrouve à la sotie du studio!",
|
||||
image: cardsImg.production,
|
||||
imagePosition: "right",
|
||||
textAlign: "right",
|
||||
lockSide: "left",
|
||||
},
|
||||
];
|
||||
|
||||
const CLUB_CARD_IMAGE = icons.clubIcon;
|
||||
|
||||
const Home = ({ navigation, route }) => {
|
||||
const isFocused = useIsFocused();
|
||||
const {
|
||||
userProjects = [],
|
||||
selectProject,
|
||||
@@ -61,7 +108,7 @@ const Home = ({ navigation, route }) => {
|
||||
|
||||
const projects = useMemo(
|
||||
() => (Array.isArray(userProjects) ? userProjects : []),
|
||||
[userProjects]
|
||||
[userProjects],
|
||||
);
|
||||
|
||||
const currentProject = useMemo(() => {
|
||||
@@ -94,73 +141,34 @@ const Home = ({ navigation, route }) => {
|
||||
|
||||
const stageStates = useMemo(
|
||||
() => getCreationStageStates(currentProject),
|
||||
[currentProject]
|
||||
[currentProject],
|
||||
);
|
||||
|
||||
const preferredStageIndex = useMemo(() => {
|
||||
if (!stageStates.length) {
|
||||
return 0;
|
||||
const stageStatesByKey = useMemo(() => {
|
||||
if (!Array.isArray(stageStates)) {
|
||||
return {};
|
||||
}
|
||||
const actionableIndex = stageStates.findIndex(
|
||||
(stage) => !stage.isCompleted && !stage.isLocked
|
||||
);
|
||||
if (actionableIndex !== -1) {
|
||||
return actionableIndex;
|
||||
}
|
||||
const firstIncomplete = stageStates.findIndex(
|
||||
(stage) => !stage.isCompleted
|
||||
);
|
||||
if (firstIncomplete !== -1) {
|
||||
return firstIncomplete;
|
||||
}
|
||||
const firstUnlocked = stageStates.findIndex((stage) => !stage.isLocked);
|
||||
if (firstUnlocked !== -1) {
|
||||
return firstUnlocked;
|
||||
}
|
||||
return stageStates.length - 1;
|
||||
return stageStates.reduce((acc, stage) => {
|
||||
if (stage?.key) {
|
||||
acc[stage.key] = stage;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}, [stageStates]);
|
||||
|
||||
const [activeStageIndex, setActiveStageIndex] = useState(preferredStageIndex);
|
||||
const [menuVisible, setMenuVisible] = useState(false);
|
||||
const [menuAnchor, setMenuAnchor] = useState(null);
|
||||
const [menuProject, setMenuProject] = useState(null);
|
||||
const menuAnchorRef = useRef(null);
|
||||
const testLoaderTimeoutRef = useRef(null);
|
||||
const [isIntroVideoVisible, setIsIntroVideoVisible] = useState(false);
|
||||
const [hasLocalAdventureFlag, setHasLocalAdventureFlag] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveStageIndex((prev) => {
|
||||
const fallbackIndex =
|
||||
preferredStageIndex >= 0 && preferredStageIndex < stageStates.length
|
||||
? preferredStageIndex
|
||||
: 0;
|
||||
|
||||
if (prev == null || prev >= stageStates.length) {
|
||||
return fallbackIndex;
|
||||
}
|
||||
const current = stageStates[prev];
|
||||
if (!current) {
|
||||
return fallbackIndex;
|
||||
}
|
||||
if (
|
||||
(current.isLocked || current.isCompleted) &&
|
||||
fallbackIndex !== prev &&
|
||||
stageStates[fallbackIndex]
|
||||
) {
|
||||
return fallbackIndex;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, [stageStates, preferredStageIndex]);
|
||||
|
||||
const handleSelectProject = useCallback(
|
||||
(project) => {
|
||||
if (!project?.id) return;
|
||||
selectProject(project.id);
|
||||
setActiveStageIndex(findFirstUnlockedStageIndex(project));
|
||||
},
|
||||
[selectProject]
|
||||
[selectProject],
|
||||
);
|
||||
|
||||
const handleCloseMenu = useCallback(() => {
|
||||
@@ -228,7 +236,7 @@ const Home = ({ navigation, route }) => {
|
||||
},
|
||||
},
|
||||
],
|
||||
{ cancelable: true }
|
||||
{ cancelable: true },
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -262,7 +270,7 @@ const Home = ({ navigation, route }) => {
|
||||
navigate(targetRoute, beatmakerStage?.params);
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
);
|
||||
navigation?.setParams?.({ showLyricsCongrats: false });
|
||||
}, [
|
||||
@@ -273,19 +281,6 @@ const Home = ({ navigation, route }) => {
|
||||
route?.params?.showLyricsCongrats,
|
||||
]);
|
||||
|
||||
const activeStage =
|
||||
stageStates[activeStageIndex] || stageStates[preferredStageIndex];
|
||||
const stageAction = useMemo(() => {
|
||||
if (!activeStage) {
|
||||
return null;
|
||||
}
|
||||
return getStageAction(activeStage.key, currentProject);
|
||||
}, [activeStage, currentProject]);
|
||||
|
||||
const stageRoute = stageAction?.route || null;
|
||||
const stageParams = stageAction?.params;
|
||||
const stageLocked = activeStage?.isLocked ?? true;
|
||||
|
||||
const hasActiveProject = !!currentProject;
|
||||
|
||||
const ensureProjectSelected = useCallback(() => {
|
||||
@@ -297,32 +292,9 @@ const Home = ({ navigation, route }) => {
|
||||
}
|
||||
}, [currentProject?.id, selectProject, selectedProject?.id]);
|
||||
|
||||
const handleStageAction = useCallback(() => {
|
||||
if (!hasActiveProject || stageLocked || !stageRoute) {
|
||||
return;
|
||||
}
|
||||
ensureProjectSelected();
|
||||
navigate(stageRoute, stageParams);
|
||||
}, [
|
||||
ensureProjectSelected,
|
||||
hasActiveProject,
|
||||
stageLocked,
|
||||
stageParams,
|
||||
stageRoute,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (testLoaderTimeoutRef.current) {
|
||||
clearTimeout(testLoaderTimeoutRef.current);
|
||||
testLoaderTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const songwriterAction = useMemo(
|
||||
() => getStageAction("songwriter", null),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const handleStartNew = useCallback(async () => {
|
||||
@@ -335,7 +307,6 @@ const Home = ({ navigation, route }) => {
|
||||
|
||||
if (emptyProject?.id) {
|
||||
selectProject(emptyProject.id);
|
||||
setActiveStageIndex(findFirstUnlockedStageIndex(emptyProject));
|
||||
navigate(targetRoute, targetParams);
|
||||
return;
|
||||
}
|
||||
@@ -344,7 +315,6 @@ const Home = ({ navigation, route }) => {
|
||||
if (!newProjectId) {
|
||||
return;
|
||||
}
|
||||
setActiveStageIndex(0);
|
||||
navigate(targetRoute, targetParams);
|
||||
} catch (error) {
|
||||
console.warn("Home: unable to start new project", error);
|
||||
@@ -353,17 +323,7 @@ const Home = ({ navigation, route }) => {
|
||||
text: "Impossible de démarrer un nouveau projet",
|
||||
});
|
||||
}
|
||||
}, [
|
||||
createNewProject,
|
||||
projects,
|
||||
selectProject,
|
||||
setActiveStageIndex,
|
||||
setTooltip,
|
||||
songwriterAction,
|
||||
]);
|
||||
|
||||
const continueDisabled = !hasActiveProject || stageLocked || !stageRoute;
|
||||
const startDisabled = !songwriterAction?.route;
|
||||
}, [createNewProject, projects, selectProject, setTooltip, songwriterAction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUserData?.adventureStarted) {
|
||||
@@ -412,104 +372,102 @@ const Home = ({ navigation, route }) => {
|
||||
simpleRef: true,
|
||||
});
|
||||
const videoUrl = isWeb ? video?.landingWeb : video?.landing;
|
||||
const homeBackgroundImage = isWeb
|
||||
? background.homeBGWeb
|
||||
: background.homeBG;
|
||||
const homeBackgroundImage = background.bgTrans;
|
||||
const landingBackgroundImage = homeBackgroundImage;
|
||||
|
||||
const stageCards = useMemo(
|
||||
() =>
|
||||
STAGE_CARD_CONTENT.map((card) => ({
|
||||
...card,
|
||||
isLocked: stageStatesByKey[card.key]?.isLocked ?? true,
|
||||
})),
|
||||
[stageStatesByKey],
|
||||
);
|
||||
|
||||
const handleStagePress = useCallback(
|
||||
(stageKey, isLocked) => {
|
||||
if (!hasActiveProject || isLocked) {
|
||||
return;
|
||||
}
|
||||
ensureProjectSelected();
|
||||
const action = getStageAction(stageKey, currentProject);
|
||||
if (!action?.route) {
|
||||
return;
|
||||
}
|
||||
navigate(action.route, action.params);
|
||||
},
|
||||
[currentProject, ensureProjectSelected, hasActiveProject],
|
||||
);
|
||||
|
||||
const handleClubPress = useCallback(() => {
|
||||
navigate(Routes.Payments);
|
||||
}, []);
|
||||
|
||||
return adventureStarted ? (
|
||||
<Page
|
||||
shareBtn
|
||||
headerType="NONE"
|
||||
containerStyle={{ width: "100%", maxWidth: "100%", paddingHorizontal: 0 }}
|
||||
backgroundImg={homeBackgroundImage}
|
||||
>
|
||||
<View style={[styles.root, isWeb ? styles.rootWeb : styles.rootNative]}>
|
||||
<View
|
||||
style={{
|
||||
zIndex: 10,
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
width: 400,
|
||||
alignSelf: "center",
|
||||
flexDirection: isWeb ? "column" : "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: isWeb ? 0 : 30,
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<ProjectDropDown
|
||||
style={[isWeb ? { width: "100%" } : { flex: 1 }]}
|
||||
projects={projects}
|
||||
selectedProject={currentProject}
|
||||
allowEmptySelection
|
||||
onSelectProject={handleSelectProject}
|
||||
onModifyProject={handleModifyProject}
|
||||
onCreateProject={handleStartNew}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
{!isWeb && <ShareBtn />}
|
||||
</View>
|
||||
<MoreMenu
|
||||
visible={menuVisible}
|
||||
top={menuAnchor?.top ?? 0}
|
||||
position={menuAnchor}
|
||||
onClose={handleCloseMenu}
|
||||
inPlaylist={false}
|
||||
projectId={menuProject?.id || null}
|
||||
extraItems={moreMenuItems}
|
||||
/>
|
||||
<View style={[styles.carouselSection, styles.carouselSectionElevated]}>
|
||||
<FeatureCarousel
|
||||
selectedProject={currentProject}
|
||||
stageStates={stageStates}
|
||||
activeIndex={activeStageIndex}
|
||||
onActiveIndexChange={setActiveStageIndex}
|
||||
backgroundImage={homeBackgroundImage}
|
||||
isFocused={isFocused}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: isWeb ? 180 : 140,
|
||||
flexDirection: isWeb ? "row" : "column",
|
||||
gap: 5,
|
||||
alignSelf: "center",
|
||||
}}
|
||||
>
|
||||
<GradientButton
|
||||
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
|
||||
title={"Commencer à créer"}
|
||||
onPress={handleStartNew}
|
||||
disabled={startDisabled}
|
||||
/>
|
||||
<BorderGradientButton
|
||||
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
|
||||
title={"Continuer la création"}
|
||||
onPress={handleStageAction}
|
||||
disabled={continueDisabled}
|
||||
<View style={styles.root}>
|
||||
<Page
|
||||
shareBtn
|
||||
headerType="NONE"
|
||||
scrollEnabled={false}
|
||||
containerStyle={styles.page}
|
||||
contentContainerStyle={styles.pageContent}
|
||||
width="100%"
|
||||
maxWidth={null}
|
||||
backgroundColor="#303438"
|
||||
>
|
||||
<View style={styles.inner}>
|
||||
<ExpoImage
|
||||
source={homeBackgroundImage}
|
||||
contentFit="cover"
|
||||
style={styles.centerImage}
|
||||
/>
|
||||
|
||||
{/* <BorderGradientButton
|
||||
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
|
||||
title={"Test Alert"}
|
||||
onPress={() =>
|
||||
Alert("Test", "Ceci est un test d'alert", [
|
||||
{ text: "Annuler", style: "cancel" },
|
||||
{ text: "OK", onPress: () => console.log("OK pressé") },
|
||||
])
|
||||
}
|
||||
/> */}
|
||||
{/* <BorderGradientButton
|
||||
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
|
||||
title={"Continuer la création"}
|
||||
onPress={() => navigate(Routes.SongReady)}
|
||||
disabled={continueDisabled}
|
||||
/> */}
|
||||
<View style={styles.topBar}>
|
||||
<ProjectDropDown
|
||||
style={styles.projectDropDown}
|
||||
projects={projects}
|
||||
selectedProject={currentProject}
|
||||
allowEmptySelection
|
||||
onSelectProject={handleSelectProject}
|
||||
onModifyProject={handleModifyProject}
|
||||
onCreateProject={handleStartNew}
|
||||
formatDate={formatDate}
|
||||
/>
|
||||
{!isWeb && <ShareBtn />}
|
||||
</View>
|
||||
|
||||
<MoreMenu
|
||||
visible={menuVisible}
|
||||
top={menuAnchor?.top ?? 0}
|
||||
position={menuAnchor}
|
||||
onClose={handleCloseMenu}
|
||||
inPlaylist={false}
|
||||
projectId={menuProject?.id || null}
|
||||
extraItems={moreMenuItems}
|
||||
/>
|
||||
|
||||
<Text style={styles.subtitle}>5 espaces à découvrir</Text>
|
||||
|
||||
<View style={styles.cardsGrid}>
|
||||
{stageCards.map((card) => (
|
||||
<StageCard
|
||||
key={card.key}
|
||||
step={card.step}
|
||||
description={card.description}
|
||||
image={card.image}
|
||||
imagePosition={card.imagePosition}
|
||||
textAlign={card.textAlign}
|
||||
isLocked={card.isLocked}
|
||||
lockSide={card.lockSide}
|
||||
onPress={() => handleStagePress(card.key, card.isLocked)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<ClubCard image={CLUB_CARD_IMAGE} onPress={handleClubPress} />
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
</Page>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Page
|
||||
@@ -545,33 +503,75 @@ export default Home;
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
backgroundColor: "#303438",
|
||||
},
|
||||
rootWeb: {
|
||||
page: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
rootNative: {
|
||||
backgroundColor: "rgba(66, 91, 135, 0.3)",
|
||||
},
|
||||
heroImage: {
|
||||
position: "absolute",
|
||||
top: -60,
|
||||
alignSelf: "center",
|
||||
width: "80%",
|
||||
maxWidth: 920,
|
||||
height: 420,
|
||||
opacity: 0.9,
|
||||
},
|
||||
shareIcon: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
tintColor: Palette.white,
|
||||
},
|
||||
carouselSection: {
|
||||
padding: 0,
|
||||
paddingLeft: isWeb ? 32 : 0,
|
||||
paddingRight: isWeb ? 32 : 0,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
width: "100%",
|
||||
flex: 1,
|
||||
alignSelf: "stretch",
|
||||
},
|
||||
carouselSectionElevated: {
|
||||
marginTop: -100,
|
||||
paddingTop: 80,
|
||||
pageContent: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
inner: {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
alignSelf: "stretch",
|
||||
alignItems: "stretch",
|
||||
justifyContent: "flex-start",
|
||||
position: "relative",
|
||||
},
|
||||
centerImage: {
|
||||
width: HOME_BACKGROUND_WIDTH + 120,
|
||||
height: HOME_BACKGROUND_HEIGHT + 80,
|
||||
borderRadius: 22,
|
||||
overflow: "hidden",
|
||||
position: "absolute",
|
||||
top: "45%",
|
||||
left: "50%",
|
||||
transform: [
|
||||
{ translateX: -HOME_BACKGROUND_WIDTH / 2 },
|
||||
{ translateY: -HOME_BACKGROUND_HEIGHT / 2 },
|
||||
],
|
||||
pointerEvents: "none",
|
||||
},
|
||||
topBar: {
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 12,
|
||||
marginTop: 24,
|
||||
marginBottom: 8,
|
||||
zIndex: 10,
|
||||
},
|
||||
projectDropDown: {
|
||||
width: isWeb ? 420 : "100%",
|
||||
maxWidth: 420,
|
||||
flexGrow: 1,
|
||||
},
|
||||
subtitle: {
|
||||
width: "100%",
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 18,
|
||||
color: Palette.white,
|
||||
textAlign: "center",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 1,
|
||||
marginBottom: 16,
|
||||
},
|
||||
cardsGrid: {
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
rowGap: 20,
|
||||
columnGap: 20,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { memo } from "react";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import { Palette } from "../../../styles";
|
||||
import { icons } from "../../../assets";
|
||||
|
||||
const ClubCard = ({ image, onPress }) => (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={({ pressed }) => [styles.card, pressed && styles.cardPressed]}
|
||||
>
|
||||
<View style={styles.inner}>
|
||||
<ExpoImage
|
||||
source={icons.club}
|
||||
contentFit="contain"
|
||||
style={styles.clubLogo}
|
||||
/>
|
||||
<ExpoImage source={image} contentFit="contain" style={styles.image} />
|
||||
<Text style={styles.subtitle}>Rejoins le club !</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
export default memo(ClubCard);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginTop: 28,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#252438",
|
||||
overflow: "hidden",
|
||||
width: 200,
|
||||
alignSelf: "center",
|
||||
},
|
||||
cardPressed: {
|
||||
opacity: 0.85,
|
||||
},
|
||||
inner: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 14,
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
},
|
||||
clubLogo: {
|
||||
width: 160,
|
||||
height: 36,
|
||||
},
|
||||
image: {
|
||||
width: 52,
|
||||
height: 52,
|
||||
},
|
||||
subtitle: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
import React, { memo } from "react";
|
||||
import { Pressable, StyleSheet, Text, View } from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||
import { Palette } from "../../../styles";
|
||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
|
||||
const StageCard = ({
|
||||
step,
|
||||
description,
|
||||
image,
|
||||
imagePosition = "left",
|
||||
textAlign = "left",
|
||||
isLocked,
|
||||
onPress,
|
||||
lockSide = "right",
|
||||
}) => {
|
||||
const isImageOnLeft = imagePosition !== "right";
|
||||
const isTextRight = textAlign === "right";
|
||||
|
||||
const imageBlock = (
|
||||
<View
|
||||
style={[
|
||||
styles.imageContainer,
|
||||
isImageOnLeft ? styles.imageLeft : styles.imageRight,
|
||||
]}
|
||||
>
|
||||
<ExpoImage
|
||||
source={image}
|
||||
contentFit="contain"
|
||||
style={[
|
||||
styles.image,
|
||||
isImageOnLeft ? styles.imageAlignRight : styles.imageAlignLeft,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const header = (
|
||||
<View
|
||||
style={[
|
||||
styles.textHeader,
|
||||
isTextRight ? styles.textHeaderAlignEnd : styles.textHeaderAlignStart,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.step,
|
||||
isTextRight ? styles.textRight : styles.textLeft,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{step}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const textBlock = (
|
||||
<BlurView
|
||||
tint="dark"
|
||||
intensity={30}
|
||||
style={[
|
||||
styles.textContainer,
|
||||
isImageOnLeft ? styles.textContainerRight : styles.textContainerLeft,
|
||||
isTextRight ? styles.alignEnd : styles.alignStart,
|
||||
]}
|
||||
>
|
||||
{isLocked ? (
|
||||
<View
|
||||
style={[
|
||||
styles.lockBadge,
|
||||
isTextRight ? styles.lockBadgeLeft : styles.lockBadgeRight,
|
||||
]}
|
||||
>
|
||||
<FontAwesome name="lock" size={18} color={Palette.primary} />
|
||||
</View>
|
||||
) : null}
|
||||
{header}
|
||||
<Text
|
||||
style={[
|
||||
styles.description,
|
||||
isTextRight ? styles.textRight : styles.textLeft,
|
||||
]}
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
</BlurView>
|
||||
);
|
||||
|
||||
const content = isImageOnLeft ? (
|
||||
<>
|
||||
{imageBlock}
|
||||
{textBlock}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{textBlock}
|
||||
{imageBlock}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
disabled={isLocked}
|
||||
style={({ pressed }) => [
|
||||
styles.card,
|
||||
pressed && !isLocked && styles.cardPressed,
|
||||
]}
|
||||
>
|
||||
<View style={styles.inner}>{content}</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(StageCard);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
width: "48%",
|
||||
minWidth: 260,
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
borderRadius: 20,
|
||||
},
|
||||
cardPressed: {
|
||||
opacity: 0.85,
|
||||
},
|
||||
inner: {
|
||||
flexDirection: "row",
|
||||
alignItems: "stretch",
|
||||
},
|
||||
imageContainer: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
minHeight: 200,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
},
|
||||
imageLeft: {
|
||||
borderTopLeftRadius: 20,
|
||||
borderBottomLeftRadius: 20,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
imageRight: {
|
||||
borderTopRightRadius: 20,
|
||||
borderBottomRightRadius: 20,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
image: {
|
||||
width: "85%",
|
||||
height: "85%",
|
||||
minHeight: 200,
|
||||
borderRadius: 0,
|
||||
},
|
||||
imageAlignRight: {
|
||||
alignSelf: "flex-end",
|
||||
},
|
||||
imageAlignLeft: {
|
||||
alignSelf: "flex-start",
|
||||
},
|
||||
textContainer: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 240,
|
||||
maxWidth: 320,
|
||||
minHeight: 120,
|
||||
maxHeight: 160,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: 18,
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
},
|
||||
textContainerRight: {
|
||||
borderTopRightRadius: 18,
|
||||
borderBottomRightRadius: 18,
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
marginLeft: -32,
|
||||
},
|
||||
textContainerLeft: {
|
||||
borderTopLeftRadius: 18,
|
||||
borderBottomLeftRadius: 18,
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
marginRight: -32,
|
||||
},
|
||||
alignStart: {
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
alignEnd: {
|
||||
alignItems: "flex-end",
|
||||
},
|
||||
textHeader: {
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
},
|
||||
textHeaderAlignStart: {
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
textHeaderAlignEnd: {
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
step: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
flexShrink: 1,
|
||||
},
|
||||
description: {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 13,
|
||||
lineHeight: 20,
|
||||
color: Palette.white,
|
||||
},
|
||||
textLeft: {
|
||||
textAlign: "left",
|
||||
},
|
||||
textRight: {
|
||||
textAlign: "right",
|
||||
},
|
||||
lockBadge: {
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.55)",
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
zIndex: 2,
|
||||
},
|
||||
lockBadgeLeft: {
|
||||
left: 10,
|
||||
},
|
||||
lockBadgeRight: {
|
||||
right: 10,
|
||||
},
|
||||
});
|
||||
+67
-3
@@ -12,7 +12,7 @@ import { BlurView } from "expo-blur";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import Page from "../layouts/Page";
|
||||
import { background } from "../assets";
|
||||
import { background, subBadges } from "../assets";
|
||||
import { Palette, gutters } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
@@ -82,6 +82,9 @@ function SubscriptionCard({ plan, selected, onSelect }) {
|
||||
typeof plan?.coinsPerMonth === "number" && Number.isFinite(plan.coinsPerMonth)
|
||||
? Math.round(plan.coinsPerMonth)
|
||||
: null;
|
||||
const planBadgeKey = getPlanKeyForBadge(plan);
|
||||
const planBadgeSource =
|
||||
planBadgeKey && subBadges[planBadgeKey] ? subBadges[planBadgeKey] : null;
|
||||
const handleSelect = React.useCallback(() => {
|
||||
if (typeof onSelect === "function" && plan?.priceId) {
|
||||
onSelect(plan.priceId);
|
||||
@@ -110,7 +113,16 @@ function SubscriptionCard({ plan, selected, onSelect }) {
|
||||
) : null}
|
||||
|
||||
<View style={styles.cardHeader}>
|
||||
<Text style={styles.planName}>{planName}</Text>
|
||||
<View style={styles.titleRow}>
|
||||
<Text style={styles.planName}>{planName}</Text>
|
||||
{planBadgeSource ? (
|
||||
<ExpoImage
|
||||
source={planBadgeSource}
|
||||
style={styles.planBadgeImage}
|
||||
contentFit="contain"
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{plan?.nickname ? (
|
||||
<Text style={styles.cardSubtitle}>{plan.nickname}</Text>
|
||||
) : null}
|
||||
@@ -169,6 +181,17 @@ const PACK_PRICE_ID_BY_PERIOD = {
|
||||
},
|
||||
};
|
||||
|
||||
const PRICE_ID_TO_PLAN_KEY = {};
|
||||
|
||||
Object.values(PACK_PRICE_ID_BY_PERIOD).forEach((mapping) => {
|
||||
Object.entries(mapping).forEach(([planKey, priceId]) => {
|
||||
if (!priceId) {
|
||||
return;
|
||||
}
|
||||
PRICE_ID_TO_PLAN_KEY[priceId] = planKey === "prop" ? "pro" : planKey;
|
||||
});
|
||||
});
|
||||
|
||||
const PLAN_FALLBACK_ORDER = ["starter", "pro", "premium"];
|
||||
|
||||
const PLAN_SYNONYMS = {
|
||||
@@ -177,6 +200,33 @@ const PLAN_SYNONYMS = {
|
||||
premium: ["premium"],
|
||||
};
|
||||
|
||||
const getPlanKeyForBadge = (plan) => {
|
||||
if (!plan || typeof plan !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const priceId =
|
||||
typeof plan?.priceId === "string" ? plan.priceId : plan?.id || null;
|
||||
|
||||
if (priceId && PRICE_ID_TO_PLAN_KEY[priceId]) {
|
||||
return PRICE_ID_TO_PLAN_KEY[priceId];
|
||||
}
|
||||
|
||||
const label = (plan?.product?.name || plan?.nickname || "")
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
|
||||
if (!label) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match = Object.entries(PLAN_SYNONYMS).find(([, variants]) =>
|
||||
variants.some((variant) => label.includes(variant)),
|
||||
);
|
||||
|
||||
return match ? match[0] : null;
|
||||
};
|
||||
|
||||
const getPlanPriority = (plan, periodKey) => {
|
||||
const priceId =
|
||||
typeof plan?.priceId === "string" ? plan.priceId : plan?.id || null;
|
||||
@@ -685,7 +735,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
cardBlur: {
|
||||
flex: 1,
|
||||
padding: gutters * 1.2,
|
||||
paddingHorizontal: gutters * 1.2,
|
||||
paddingVertical: gutters,
|
||||
gap: 20,
|
||||
justifyContent: "center",
|
||||
backgroundColor: "rgba(48, 52, 56, 0.55)",
|
||||
@@ -697,10 +748,23 @@ const styles = StyleSheet.create({
|
||||
cardHeader: {
|
||||
gap: 6,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 12,
|
||||
width: "100%",
|
||||
},
|
||||
planName: {
|
||||
fontFamily: FONT_FAMILY.InterBold,
|
||||
fontSize: 24,
|
||||
color: Palette.white,
|
||||
flexShrink: 1,
|
||||
},
|
||||
planBadgeImage: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
flexShrink: 0,
|
||||
},
|
||||
cardSubtitle: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
|
||||
@@ -132,6 +132,62 @@ const capitalize = (value) => {
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
};
|
||||
|
||||
const SCHEDULE_COMPARISON_HOUR = 2;
|
||||
const SCHEDULE_COMPARISON_MINUTE = 30;
|
||||
const SCHEDULE_DISPLAY_HOUR = 3;
|
||||
const SCHEDULE_DISPLAY_MINUTE = 30;
|
||||
|
||||
const addMonthsSafe = (date, months = 1) => {
|
||||
if (!(date instanceof Date) || !Number.isFinite(months)) {
|
||||
return null;
|
||||
}
|
||||
const result = new Date(date.getTime());
|
||||
const initialDay = result.getDate();
|
||||
result.setMonth(result.getMonth() + months);
|
||||
if (result.getDate() !== initialDay) {
|
||||
result.setDate(0);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const addDaysSafe = (date, days = 1) => {
|
||||
if (!(date instanceof Date) || !Number.isFinite(days)) {
|
||||
return null;
|
||||
}
|
||||
const result = new Date(date.getTime());
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
};
|
||||
|
||||
const alignToScheduleTime = (date) => {
|
||||
if (!(date instanceof Date)) {
|
||||
return null;
|
||||
}
|
||||
const aligned = new Date(date.getTime());
|
||||
aligned.setHours(SCHEDULE_DISPLAY_HOUR, SCHEDULE_DISPLAY_MINUTE, 0, 0);
|
||||
return aligned;
|
||||
};
|
||||
|
||||
const computeFirstAnnualGrantFromCreation = (creationDate) => {
|
||||
if (!(creationDate instanceof Date)) {
|
||||
return null;
|
||||
}
|
||||
const cutoff = new Date(creationDate.getTime());
|
||||
cutoff.setHours(SCHEDULE_COMPARISON_HOUR, SCHEDULE_COMPARISON_MINUTE, 0, 0);
|
||||
|
||||
let base = null;
|
||||
if (creationDate <= cutoff) {
|
||||
base = addMonthsSafe(creationDate, 1);
|
||||
} else {
|
||||
base = addDaysSafe(creationDate, 1);
|
||||
}
|
||||
|
||||
if (!base) {
|
||||
return null;
|
||||
}
|
||||
return alignToScheduleTime(base);
|
||||
};
|
||||
|
||||
const ManageSubscription = ({ navigation }) => {
|
||||
const { currentUserData } = useUserData() || {};
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
@@ -334,9 +390,40 @@ const ManageSubscription = ({ navigation }) => {
|
||||
currentUserData?.subscriptionNextGrantAt ||
|
||||
currentUserData?.subscriptionGrantNextAt ||
|
||||
null;
|
||||
const nextGrantDate = toDate(nextGrantTimestamp);
|
||||
const nextGrantRawDate = toDate(nextGrantTimestamp);
|
||||
const now = new Date();
|
||||
|
||||
const alignedStoredNextGrant = alignToScheduleTime(nextGrantRawDate);
|
||||
const futureStoredNextGrant =
|
||||
alignedStoredNextGrant &&
|
||||
alignedStoredNextGrant.getTime() >= now.getTime()
|
||||
? alignedStoredNextGrant
|
||||
: null;
|
||||
|
||||
const fallbackInitialGrant =
|
||||
isAnnual && createdAtDate
|
||||
? computeFirstAnnualGrantFromCreation(createdAtDate)
|
||||
: null;
|
||||
const futureFallbackGrant =
|
||||
fallbackInitialGrant &&
|
||||
fallbackInitialGrant.getTime() >= now.getTime()
|
||||
? fallbackInitialGrant
|
||||
: null;
|
||||
|
||||
let resolvedNextGrantDate =
|
||||
futureStoredNextGrant || futureFallbackGrant || null;
|
||||
|
||||
if (futureStoredNextGrant && futureFallbackGrant) {
|
||||
resolvedNextGrantDate =
|
||||
futureFallbackGrant.getTime() < futureStoredNextGrant.getTime()
|
||||
? futureFallbackGrant
|
||||
: futureStoredNextGrant;
|
||||
}
|
||||
|
||||
const nextGrantLabel =
|
||||
nextGrantDate && isAnnual ? formatDate(nextGrantDate) : null;
|
||||
resolvedNextGrantDate && isAnnual
|
||||
? formatDate(resolvedNextGrantDate)
|
||||
: null;
|
||||
|
||||
const helperMessages = [];
|
||||
if (!hasActiveSubscription && hasAnySubscription) {
|
||||
@@ -374,7 +461,7 @@ const ManageSubscription = ({ navigation }) => {
|
||||
coinsPerMonth: normalizedCoins,
|
||||
coinsPerMonthLabel,
|
||||
isAnnual,
|
||||
nextGrantDate,
|
||||
nextGrantDate: resolvedNextGrantDate,
|
||||
nextGrantLabel,
|
||||
};
|
||||
}, [currentUserData, remoteSubscription]);
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import React, { useEffect, useMemo, useState, useGlobal } from "reactn";
|
||||
import { Text, View } from "react-native";
|
||||
import {
|
||||
checkIfPasswordIsStrongEnough,
|
||||
handleFirebaseError,
|
||||
} from "../actions/signupActions";
|
||||
import { background } from "../assets";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import { Input } from "../components/Input";
|
||||
import ItemContainer from "../components/ItemContainer/ItemContainer";
|
||||
import firebase from "../config/firebase";
|
||||
import Page from "../layouts/Page";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
import { Routes } from "../navigation";
|
||||
import { navigate } from "../navigation/NavigationService";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
|
||||
const PASSWORD_ERROR_MESSAGE =
|
||||
"Ton mot de passe doit contenir au moins 6 caractères et combiner plusieurs types de caractères.";
|
||||
const PASSWORD_MISMATCH_MESSAGE = "Les mots de passe ne correspondent pas.";
|
||||
const INVALID_LINK_MESSAGE =
|
||||
"Ce lien de réinitialisation n'est plus valide. Demande un nouveau mot de passe.";
|
||||
|
||||
const ResetPassword = () => {
|
||||
const route = useRoute();
|
||||
const oobCode = (route?.params?.oobCode || "").trim();
|
||||
|
||||
const [, setTooltip] = useGlobal("_tooltip");
|
||||
const [, setIsLoading] = useGlobal("_isLoading");
|
||||
|
||||
const [email, setEmail] = useState(route?.params?.email || "");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [passwordError, setPasswordError] = useState("");
|
||||
const [confirmError, setConfirmError] = useState("");
|
||||
const [codeError, setCodeError] = useState("");
|
||||
const [isVerifyingCode, setIsVerifyingCode] = useState(true);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const trimmedPassword = useMemo(() => (password || "").trim(), [password]);
|
||||
const trimmedConfirmPassword = useMemo(
|
||||
() => (confirmPassword || "").trim(),
|
||||
[confirmPassword]
|
||||
);
|
||||
|
||||
const isPasswordValid = useMemo(() => {
|
||||
if (!trimmedPassword.length) return false;
|
||||
return checkIfPasswordIsStrongEnough({ password: trimmedPassword });
|
||||
}, [trimmedPassword]);
|
||||
|
||||
const doPasswordsMatch = useMemo(() => {
|
||||
if (!trimmedPassword.length || !trimmedConfirmPassword.length) return false;
|
||||
return trimmedPassword === trimmedConfirmPassword;
|
||||
}, [trimmedPassword, trimmedConfirmPassword]);
|
||||
|
||||
useEffect(() => {
|
||||
const verifyCode = async () => {
|
||||
if (!oobCode) {
|
||||
setCodeError(INVALID_LINK_MESSAGE);
|
||||
setTooltip({ text: INVALID_LINK_MESSAGE, type: "error" });
|
||||
setIsVerifyingCode(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const resetEmail = await firebase
|
||||
.auth()
|
||||
.verifyPasswordResetCode(oobCode);
|
||||
setEmail(resetEmail);
|
||||
setCodeError("");
|
||||
} catch (error) {
|
||||
console.log("ResetPassword verify error", error?.message);
|
||||
const message =
|
||||
error?.code && error.code.startsWith("auth/")
|
||||
? handleFirebaseError(error.code)
|
||||
: error?.message || INVALID_LINK_MESSAGE;
|
||||
setCodeError(message || INVALID_LINK_MESSAGE);
|
||||
setTooltip({ text: message || INVALID_LINK_MESSAGE, type: "error" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsVerifyingCode(false);
|
||||
}
|
||||
};
|
||||
|
||||
verifyCode();
|
||||
}, [oobCode, setIsLoading, setTooltip]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!password?.length) {
|
||||
setPasswordError("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPasswordValid) {
|
||||
setPasswordError(PASSWORD_ERROR_MESSAGE);
|
||||
} else {
|
||||
setPasswordError("");
|
||||
}
|
||||
}, [password, isPasswordValid]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!confirmPassword?.length) {
|
||||
setConfirmError("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doPasswordsMatch) {
|
||||
setConfirmError(PASSWORD_MISMATCH_MESSAGE);
|
||||
} else {
|
||||
setConfirmError("");
|
||||
}
|
||||
}, [confirmPassword, doPasswordsMatch]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!isPasswordValid) {
|
||||
setPasswordError(PASSWORD_ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (!doPasswordsMatch) {
|
||||
setConfirmError(PASSWORD_MISMATCH_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
setIsLoading(true);
|
||||
await firebase.auth().confirmPasswordReset(oobCode, trimmedPassword);
|
||||
setTooltip({
|
||||
text: "Ton mot de passe a été mis à jour. Tu peux te connecter.",
|
||||
type: "success",
|
||||
});
|
||||
navigate(Routes.Login);
|
||||
} catch (error) {
|
||||
console.log("ResetPassword submit error", error?.message);
|
||||
const message =
|
||||
error?.code && error.code.startsWith("auth/")
|
||||
? handleFirebaseError(error.code)
|
||||
: error?.message || "Impossible de mettre à jour le mot de passe.";
|
||||
setTooltip({ text: message, type: "error" });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderForm = () => (
|
||||
<>
|
||||
<View style={{ gap: 2 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
Définis un nouveau mot de passe
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.gray,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Utilise au moins 6 caractères pour sécuriser ton compte.
|
||||
</Text>
|
||||
{email ? (
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 8,
|
||||
fontSize: 12,
|
||||
color: Palette.gray,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Compte concerné : {email}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
<View style={{ gap: 16 }}>
|
||||
<Input
|
||||
placeholder="Nouveau mot de passe"
|
||||
label="Nouveau mot de passe"
|
||||
type="password"
|
||||
value={password}
|
||||
setValue={setPassword}
|
||||
isBlur
|
||||
/>
|
||||
{passwordError ? (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.red,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{passwordError}
|
||||
</Text>
|
||||
) : null}
|
||||
<Input
|
||||
placeholder="Confirmer le mot de passe"
|
||||
label="Confirmer le mot de passe"
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
setValue={setConfirmPassword}
|
||||
isBlur
|
||||
/>
|
||||
{confirmError ? (
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.red,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{confirmError}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
const renderCodeError = () => (
|
||||
<View style={{ gap: 12 }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
}}
|
||||
>
|
||||
Lien invalide ou expiré
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.gray,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{codeError || INVALID_LINK_MESSAGE}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const handleFallbackNavigation = () => {
|
||||
navigate(Routes.ForgotPassword);
|
||||
};
|
||||
|
||||
const buttonDisabled = codeError
|
||||
? isSubmitting
|
||||
: isVerifyingCode ||
|
||||
isSubmitting ||
|
||||
!isPasswordValid ||
|
||||
!doPasswordsMatch;
|
||||
|
||||
const buttonLabel = codeError
|
||||
? "Demander un nouveau lien"
|
||||
: isSubmitting
|
||||
? "Mise à jour..."
|
||||
: isVerifyingCode
|
||||
? "Vérification..."
|
||||
: "Mettre à jour le mot de passe";
|
||||
|
||||
const buttonAction = codeError ? handleFallbackNavigation : handleSubmit;
|
||||
|
||||
return (
|
||||
<Page
|
||||
width={isWeb ? 600 : null}
|
||||
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
|
||||
headerType="NAVIGATION"
|
||||
title="Réinitialisation"
|
||||
>
|
||||
<View style={{ flex: 1, paddingTop: 20 }}>
|
||||
<ItemContainer height={360} disableKeyboardHeight>
|
||||
<View style={{ gap: 32, paddingTop: 5, paddingHorizontal: 5 }}>
|
||||
{codeError ? renderCodeError() : renderForm()}
|
||||
<GradientButton
|
||||
title={buttonLabel}
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={buttonAction}
|
||||
disabled={buttonDisabled}
|
||||
/>
|
||||
</View>
|
||||
</ItemContainer>
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResetPassword;
|
||||
@@ -6,6 +6,7 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import AppAlert from "../../components/Alert";
|
||||
import CoinIcon from "../../components/CoinIcon";
|
||||
import firebase, { getFunctionsClient } from "../../config/firebase";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
@@ -338,27 +339,69 @@ const ComposeSong = () => {
|
||||
>
|
||||
Générer la musique ?
|
||||
</Text>
|
||||
<Text
|
||||
<View style={{ alignItems: "center", gap: 8 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Cette action coûte{" "}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{MUSIC_GENERATION_COIN_COST}
|
||||
</Text>
|
||||
<CoinIcon size={18} />
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Souhaites-tu les utiliser pour lancer la génération ?
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
Cette action coûte {MUSIC_GENERATION_COIN_COST} pièces.
|
||||
{"\n"}Souhaites-tu les utiliser pour lancer la génération ?
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Solde disponible : {formattedCoinBalance} pièces
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Solde disponible : {formattedCoinBalance}
|
||||
</Text>
|
||||
<CoinIcon size={16} />
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
|
||||
<GradientButton
|
||||
|
||||
@@ -11,6 +11,7 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import AppAlert from "../../components/Alert";
|
||||
import CoinIcon from "../../components/CoinIcon";
|
||||
import firebase, { getFunctionsClient } from "../../config/firebase";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
@@ -367,27 +368,69 @@ const ComposeSong = () => {
|
||||
>
|
||||
Générer la musique ?
|
||||
</Text>
|
||||
<Text
|
||||
<View style={{ alignItems: "center", gap: 8 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexWrap: "wrap",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Cette action coûte{" "}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{MUSIC_GENERATION_COIN_COST}
|
||||
</Text>
|
||||
<CoinIcon size={18} />
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Souhaites-tu les utiliser pour lancer la génération ?
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
Cette action coûte {MUSIC_GENERATION_COIN_COST} pièces.
|
||||
{"\n"}Souhaites-tu les utiliser pour lancer la génération ?
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Solde disponible : {formattedCoinBalance} pièces
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Solde disponible : {formattedCoinBalance}
|
||||
</Text>
|
||||
<CoinIcon size={16} />
|
||||
</View>
|
||||
</View>
|
||||
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
|
||||
<GradientButton
|
||||
|
||||
@@ -55,12 +55,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
return FAKE_PROGRESS_MAX;
|
||||
}
|
||||
|
||||
const increment =
|
||||
prevProgress < 60
|
||||
? 1
|
||||
: prevProgress < 80
|
||||
? 0.6
|
||||
: 0.3;
|
||||
const increment = prevProgress < 60 ? 1 : prevProgress < 80 ? 0.6 : 0.3;
|
||||
|
||||
const next = prevProgress + increment;
|
||||
return next >= FAKE_PROGRESS_MAX ? FAKE_PROGRESS_MAX : next;
|
||||
@@ -79,7 +74,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
});
|
||||
const sanitizedStructure = sanitizeStructureList(
|
||||
config?.structure,
|
||||
CUSTOM_SANITIZE_OPTIONS
|
||||
CUSTOM_SANITIZE_OPTIONS,
|
||||
);
|
||||
const callable = firebase
|
||||
.functions()
|
||||
@@ -142,7 +137,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
const rawMessage = typeof e?.message === "string" ? e.message : "";
|
||||
const cleanedMessage = rawMessage.replace(
|
||||
/^functions error: \w+-\w+:\s*/i,
|
||||
""
|
||||
"",
|
||||
);
|
||||
setError({
|
||||
message:
|
||||
@@ -172,7 +167,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
console.log("💾 [CreatingLyrics] Sauvegarde automatique des paroles");
|
||||
const sanitizedStructure = sanitizeStructureList(
|
||||
result?.structure || config?.structure,
|
||||
CUSTOM_SANITIZE_OPTIONS
|
||||
CUSTOM_SANITIZE_OPTIONS,
|
||||
);
|
||||
const baseConfig =
|
||||
config && typeof config === "object" && !Array.isArray(config)
|
||||
@@ -180,26 +175,30 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
: {};
|
||||
if (sanitizedStructure.length > 0) {
|
||||
baseConfig.structure = sanitizedStructure;
|
||||
} else if (Object.prototype.hasOwnProperty.call(baseConfig, "structure")) {
|
||||
} else if (
|
||||
Object.prototype.hasOwnProperty.call(baseConfig, "structure")
|
||||
) {
|
||||
delete baseConfig.structure;
|
||||
}
|
||||
const persistedConfig =
|
||||
Object.keys(baseConfig).length > 0 ? baseConfig : null;
|
||||
|
||||
let persistedSelections =
|
||||
selections && typeof selections === "object" && !Array.isArray(selections)
|
||||
selections &&
|
||||
typeof selections === "object" &&
|
||||
!Array.isArray(selections)
|
||||
? { ...selections }
|
||||
: null;
|
||||
if (persistedSelections) {
|
||||
if ("customStructure" in persistedSelections) {
|
||||
persistedSelections.customStructure = sanitizeStructureList(
|
||||
persistedSelections.customStructure,
|
||||
CUSTOM_SANITIZE_OPTIONS
|
||||
CUSTOM_SANITIZE_OPTIONS,
|
||||
);
|
||||
}
|
||||
if ("parsedStructure" in persistedSelections) {
|
||||
persistedSelections.parsedStructure = sanitizeStructureList(
|
||||
persistedSelections.parsedStructure
|
||||
persistedSelections.parsedStructure,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -252,7 +251,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
onPress: redirect,
|
||||
},
|
||||
],
|
||||
{ cancelable: false }
|
||||
{ cancelable: false },
|
||||
);
|
||||
|
||||
if (Platform.OS === "web") {
|
||||
@@ -274,22 +273,6 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
height: "50%",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
zIndex: 1,
|
||||
position: "absolute",
|
||||
top: -150,
|
||||
width: "50%",
|
||||
height: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={ai.nathalie}
|
||||
style={{ width: "100%", height: "100%", right: -10 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -344,7 +327,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
setSaved(true);
|
||||
await setIsLoading(true);
|
||||
console.log(
|
||||
"📄 [CreatingLyrics] Consultation manuelle du texte"
|
||||
"📄 [CreatingLyrics] Consultation manuelle du texte",
|
||||
);
|
||||
await updateProjectData({
|
||||
title: result?.title || "",
|
||||
@@ -359,7 +342,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"⚠️ [CreatingLyrics] Erreur lors de l'ouverture manuelle",
|
||||
e
|
||||
e,
|
||||
);
|
||||
} finally {
|
||||
await setIsLoading(false);
|
||||
|
||||
Reference in New Issue
Block a user