more tickets

This commit is contained in:
Thomas Demirdjian
2025-11-13 18:31:38 +01:00
parent dd4cf9b4d4
commit 5457030d4a
32 changed files with 913 additions and 558 deletions
+2
View File
@@ -99,8 +99,10 @@ export default ({ navigation }) => {
<Input
label="Adresse email"
placeholder="Votre adresse email"
type="email"
value={email}
setValue={setEmail}
isBlur
containerStyle={{ marginBottom: 20 }}
textInputProps={{
keyboardType: "email-address",
+117 -63
View File
@@ -5,7 +5,7 @@ import React, {
useRef,
useState,
} from "react";
import { StyleSheet, Text, View } from "react-native";
import { ScrollView, 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, cardsImg, icons } from "../../assets";
@@ -14,7 +14,6 @@ import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu";
import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
import ShareBtn from "../../components/ShareBtn/ShareBtn";
import { projectsRef, usersRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType.js";
import Page from "../../layouts/Page";
@@ -22,7 +21,7 @@ import LandingPage from "../LandingPage";
import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation/Routes";
import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import {
getCreationStageStates,
@@ -43,6 +42,8 @@ const isProjectEmpty = (project) => {
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520;
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360;
const HOME_BACKGROUND_STYLE_WIDTH = HOME_BACKGROUND_WIDTH + 120;
const HOME_BACKGROUND_STYLE_HEIGHT = HOME_BACKGROUND_HEIGHT + 80;
const STAGE_CARD_CONTENT = [
{
@@ -59,7 +60,7 @@ const STAGE_CARD_CONTENT = [
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!",
"Bienvenue au studio MusicLand.\nNous mettons en musique tes paroles en fonction de tes inspirations, ça va être top !",
image: cardsImg.studio,
imagePosition: "right",
textAlign: "right",
@@ -79,7 +80,7 @@ const STAGE_CARD_CONTENT = [
key: "publisher",
step: "ÉTAPE 4",
description:
"Je suis Mr Benhaï, Producteur de MusicLand et je vais te faire une proposition qui pourrait tintéresser, on se retrouve à la sotie du studio!",
"À la sortie du studio, l'équipe production te propose les prochaines étapes pour partager ta musique avec le monde.",
image: cardsImg.production,
imagePosition: "right",
textAlign: "right",
@@ -292,14 +293,14 @@ const Home = ({ navigation, route }) => {
: null;
Alert(
"Céline",
"Bravo ! Vous venez de terminer de créer les paroles de votre musique ! Maintenant vous pouvez passer à la prochaine étape : le Studio avec Malik. Dans cette étape vous allez pouvoir donner vie à votre chanson !",
"Bravo ! Vous venez de terminer de créer les paroles de votre musique ! Maintenant vous pouvez passer à la prochaine étape : le Studio pour donner vie à votre chanson !",
[
{
text: "Retour à l'accueil",
style: "cancel",
},
{
text: "Continuer avec Malik",
text: "Continuer vers le Studio",
onPress: () => {
if (!currentProject?.id) {
return;
@@ -448,6 +449,68 @@ const Home = ({ navigation, route }) => {
navigate(Routes.Payments);
}, []);
const stageCardContainerStyle = isWeb ? styles.cardsGrid : styles.cardsStack;
const stageCardItemStyle = isWeb
? styles.webStageCard
: styles.mobileStageCard;
const stageCardsList = (
<View style={stageCardContainerStyle}>
{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)}
containerStyle={stageCardItemStyle}
variant={isWeb ? "web" : "mobile"}
/>
))}
</View>
);
const journeyContent = (
<>
<View style={styles.topBar}>
<ProjectDropDown
style={styles.projectDropDown}
projects={projects}
selectedProject={currentProject}
allowEmptySelection
onSelectProject={handleSelectProject}
onModifyProject={handleModifyProject}
onCreateProject={handleStartNew}
formatDate={formatDate}
/>
</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>
{stageCardsList}
<ClubCard
image={CLUB_CARD_IMAGE}
onPress={handleClubPress}
hasActiveSubscription={hasActiveSubscription}
/>
</>
);
return adventureStarted ? (
<View style={styles.root}>
<Page
@@ -466,54 +529,17 @@ const Home = ({ navigation, route }) => {
contentFit="cover"
style={styles.centerImage}
/>
<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}
hasActiveSubscription={hasActiveSubscription}
/>
{isWeb ? (
<View style={styles.contentOverlay}>{journeyContent}</View>
) : (
<ScrollView
style={[styles.mobileScrollView, styles.contentOverlay]}
contentContainerStyle={styles.mobileScrollContent}
showsVerticalScrollIndicator={false}
>
{journeyContent}
</ScrollView>
)}
</View>
</Page>
</View>
@@ -534,7 +560,7 @@ const Home = ({ navigation, route }) => {
}}
>
<GradientButton
url={videos.landing}
url={videoUrl}
title="Commencer l'aventure MusicLand"
onPress={handleStartVisit}
/>
@@ -572,20 +598,27 @@ const styles = StyleSheet.create({
alignSelf: "stretch",
alignItems: "stretch",
justifyContent: "flex-start",
position: "relative",
},
centerImage: {
width: HOME_BACKGROUND_WIDTH + 120,
height: HOME_BACKGROUND_HEIGHT + 80,
width: HOME_BACKGROUND_STYLE_WIDTH,
height: HOME_BACKGROUND_STYLE_HEIGHT,
borderRadius: 22,
overflow: "hidden",
position: "absolute",
top: "45%",
top: isWeb ? "45%" : "50%",
left: "50%",
transform: [
{ translateX: -HOME_BACKGROUND_WIDTH / 2 },
{ translateY: -HOME_BACKGROUND_HEIGHT / 2 },
{ translateX: -HOME_BACKGROUND_STYLE_WIDTH / 2 },
{ translateY: -HOME_BACKGROUND_STYLE_HEIGHT / 2 },
],
pointerEvents: "none",
zIndex: 0,
},
contentOverlay: {
position: "relative",
width: "100%",
flexGrow: 1,
},
topBar: {
width: "100%",
@@ -594,8 +627,8 @@ const styles = StyleSheet.create({
alignItems: "center",
justifyContent: "center",
gap: 12,
marginTop: 24,
marginBottom: 8,
marginTop: isWeb ? 24 : 0,
marginBottom: isWeb ? 8 : 16,
zIndex: 10,
},
projectDropDown: {
@@ -603,6 +636,15 @@ const styles = StyleSheet.create({
maxWidth: 420,
flexGrow: 1,
},
mobileScrollView: {
flex: 1,
width: "100%",
},
mobileScrollContent: {
paddingHorizontal: gutters,
paddingTop: 24,
paddingBottom: gutters * 6,
},
subtitle: {
width: "100%",
fontFamily: FONT_FAMILY.InterSemiBold,
@@ -621,4 +663,16 @@ const styles = StyleSheet.create({
justifyContent: "space-between",
rowGap: 20,
},
cardsStack: {
width: "100%",
flexDirection: "column",
alignItems: "stretch",
},
webStageCard: {
width: "48%",
},
mobileStageCard: {
width: "100%",
marginBottom: 20,
},
});
+107 -49
View File
@@ -14,21 +14,41 @@ const StageCard = ({
textAlign = "left",
isLocked,
onPress,
containerStyle = null,
variant = "web",
}) => {
const isImageOnLeft = imagePosition !== "right";
const isTextRight = textAlign === "right";
const isMobileVariant = variant === "mobile";
const isImageOnLeft = !isMobileVariant && imagePosition !== "right";
const isTextRight = !isMobileVariant && textAlign === "right";
const imageBlock = (
<View
style={[
{
flex: 1,
alignItems: "center",
justifyContent: "center",
const basePressableStyle =
variant === "web"
? { width: "48%" }
: {
width: "100%",
borderRadius: 24,
overflow: "hidden",
zIndex: 10,
},
isImageOnLeft
};
const imageContainerBaseStyle = {
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
zIndex: 10,
};
const imageContainerVariantStyle = isMobileVariant
? {
width: "100%",
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
marginBottom: 0,
}
: {
flex: 1,
...(isImageOnLeft
? {
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
@@ -40,22 +60,26 @@ const StageCard = ({
borderBottomRightRadius: 20,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
},
]}
>
}),
};
const imageBlock = (
<View style={[imageContainerBaseStyle, imageContainerVariantStyle]}>
<ExpoImage
source={image}
contentFit="contain"
style={[
{
width: "100%",
height: "100%",
minHeight: 210,
height: isMobileVariant ? 190 : "100%",
minHeight: isMobileVariant ? 190 : 210,
borderRadius: 0,
},
isImageOnLeft
? { alignSelf: "flex-end" }
: { alignSelf: "flex-start" },
isMobileVariant
? { alignSelf: "center" }
: isImageOnLeft
? { alignSelf: "flex-end" }
: { alignSelf: "flex-start" },
]}
/>
</View>
@@ -70,9 +94,11 @@ const StageCard = ({
alignItems: "center",
marginBottom: 8,
},
isTextRight
? { justifyContent: "flex-end" }
: { justifyContent: "flex-start" },
isMobileVariant
? { justifyContent: "center" }
: isTextRight
? { justifyContent: "flex-end" }
: { justifyContent: "flex-start" },
]}
>
<Text
@@ -83,7 +109,11 @@ const StageCard = ({
color: Palette.white,
flexShrink: 1,
},
isTextRight ? { textAlign: "right" } : { textAlign: "left" },
isMobileVariant
? { textAlign: "center" }
: isTextRight
? { textAlign: "right" }
: { textAlign: "left" },
]}
numberOfLines={1}
>
@@ -92,21 +122,29 @@ const StageCard = ({
</View>
);
const textBlock = (
<BlurView
tint="dark"
intensity={30}
style={[
{
flexGrow: 1,
flexShrink: 1,
maxWidth: 380,
paddingVertical: 20,
paddingHorizontal: 20,
justifyContent: "center",
alignSelf: "center",
},
isImageOnLeft
const textBlockBaseStyle = {
flexGrow: 1,
flexShrink: 1,
paddingVertical: 20,
paddingHorizontal: 20,
justifyContent: "center",
alignSelf: "center",
};
const textBlockVariantStyle = isMobileVariant
? {
width: "100%",
borderBottomLeftRadius: 24,
borderBottomRightRadius: 24,
alignItems: "center",
paddingVertical: 20,
paddingHorizontal: 20,
marginTop: 0,
gap: 6,
}
: {
maxWidth: 380,
...(isImageOnLeft
? {
borderTopRightRadius: 20,
borderBottomRightRadius: 20,
@@ -114,9 +152,17 @@ const StageCard = ({
: {
borderTopLeftRadius: 20,
borderBottomLeftRadius: 20,
},
isTextRight ? { alignItems: "flex-end" } : { alignItems: "flex-start" },
]}
}),
...(isTextRight
? { alignItems: "flex-end" }
: { alignItems: "flex-start" }),
};
const textBlock = (
<BlurView
tint="dark"
intensity={30}
style={[textBlockBaseStyle, textBlockVariantStyle]}
>
{isLocked ? (
<View
@@ -132,7 +178,11 @@ const StageCard = ({
justifyContent: "center",
zIndex: 2,
},
isTextRight ? { left: 10 } : { right: 10 },
isMobileVariant
? { right: 10 }
: isTextRight
? { left: 10 }
: { right: 10 },
]}
>
<FontAwesome name="lock" size={18} color={Palette.primary} />
@@ -141,7 +191,11 @@ const StageCard = ({
{header}
<Text
style={[
isTextRight ? { textAlign: "right" } : { textAlign: "left" },
isMobileVariant
? { textAlign: "center" }
: isTextRight
? { textAlign: "right" }
: { textAlign: "left" },
{
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
@@ -155,7 +209,12 @@ const StageCard = ({
</BlurView>
);
const content = isImageOnLeft ? (
const content = isMobileVariant ? (
<>
{imageBlock}
{textBlock}
</>
) : isImageOnLeft ? (
<>
{imageBlock}
{textBlock}
@@ -172,15 +231,14 @@ const StageCard = ({
onPress={onPress}
disabled={isLocked}
style={({ pressed }) => [
{
width: "48%",
},
basePressableStyle,
containerStyle,
pressed && !isLocked && { opacity: 0.85 },
]}
>
<View
style={{
flexDirection: "row",
flexDirection: isMobileVariant ? "column" : "row",
}}
>
{content}
+23 -21
View File
@@ -159,7 +159,7 @@ const MusicDetails = ({ route }) => {
title,
artist,
}),
[projectId, title, artist]
[projectId, title, artist],
);
const handleShare = useCallback(() => {
@@ -488,7 +488,13 @@ const MusicDetails = ({ route }) => {
console.log("MusicDetails seek error", e?.message);
}
},
[trackDescriptor, sliderDurationMs, isCurrentTrack, ensureLoaded, seekTrackTo]
[
trackDescriptor,
sliderDurationMs,
isCurrentTrack,
ensureLoaded,
seekTrackTo,
],
);
const handleToggleLoop = useCallback(async () => {
@@ -582,13 +588,7 @@ const MusicDetails = ({ route }) => {
wasPlayingBeforeSeek.current = false;
lastSeekTargetMs.current = null;
}
}, [
ensureLoaded,
isCurrentTrack,
positionMs,
resumeTrack,
seekTrackTo,
]);
}, [ensureLoaded, isCurrentTrack, positionMs, resumeTrack, seekTrackTo]);
const handleSeekBySeconds = useCallback(
async (deltaSeconds) => {
@@ -605,7 +605,7 @@ const MusicDetails = ({ route }) => {
console.log("MusicDetails seekBy error", e?.message);
}
},
[trackDescriptor, positionMs, isCurrentTrack, ensureLoaded, seekTrackBy]
[trackDescriptor, positionMs, isCurrentTrack, ensureLoaded, seekTrackBy],
);
const handleLyricsSeek = useCallback(
@@ -638,7 +638,7 @@ const MusicDetails = ({ route }) => {
seekTrackTo,
isTrackPlaying,
resumeTrack,
]
],
);
const description = useMemo(() => {
// Build a readable text from lyrics with section labels
@@ -676,12 +676,12 @@ const MusicDetails = ({ route }) => {
// Current time in seconds for highlighting
const currentTimeS = useMemo(
() => Math.max(0, (positionMs || 0) / 1000),
[positionMs]
[positionMs],
);
// Preview lead: show words 0.5s earlier
const visibleTimeS = useMemo(
() => Math.max(0, currentTimeS + 0.5),
[currentTimeS]
[currentTimeS],
);
// Helpers to group aligned words into sections and timed lines
const SECTION_TAG_REGEX = /^\s*\[([^\]]+)\]\s*$/i;
@@ -844,11 +844,11 @@ const MusicDetails = ({ route }) => {
const startS = lines.reduce(
(acc, line) =>
Math.min(acc, Number.isFinite(line.startS) ? line.startS : acc),
Number.POSITIVE_INFINITY
Number.POSITIVE_INFINITY,
);
const endS = lines.reduce(
(acc, line) => Math.max(acc, Number(line.endS || 0)),
0
0,
);
grouped.push({
key: current.key,
@@ -910,7 +910,7 @@ const MusicDetails = ({ route }) => {
const flatLines = useMemo(
() => sections.flatMap((section) => section.lines),
[sections]
[sections],
);
const currentLineIdx = useMemo(() => {
if (!flatLines || flatLines.length === 0) return -1;
@@ -941,14 +941,14 @@ const MusicDetails = ({ route }) => {
lineRelativeYRef.current[lineIdx] = { sectionKey, relativeY };
updateLineAbsoluteOffset(lineIdx);
},
[updateLineAbsoluteOffset]
[updateLineAbsoluteOffset],
);
const registerSectionOffset = useCallback(
(sectionKey, y, lines = []) => {
sectionYRef.current[sectionKey] = y;
lines.forEach((line) => updateLineAbsoluteOffset(line.globalIdx));
},
[updateLineAbsoluteOffset]
[updateLineAbsoluteOffset],
);
useEffect(() => {
const y = lineYRef.current?.[currentLineIdx];
@@ -966,7 +966,9 @@ const MusicDetails = ({ route }) => {
topStickyContent={renderWebBackButton}
title={action === "userProfile" ? "Mon profil" : "Détail musique"}
backgroundImg={
action === "userProfile" ? background.profileBG : background.libraryBG2
action === "userProfile"
? background.profileBG
: background.libraryBG2Web
}
shareBtn={
sharePayload
@@ -1179,7 +1181,7 @@ const MusicDetails = ({ route }) => {
registerSectionOffset(
section.key,
e.nativeEvent?.layout?.y,
section.lines
section.lines,
)
}
style={[
@@ -1198,7 +1200,7 @@ const MusicDetails = ({ route }) => {
registerLineRelativeOffset(
line.globalIdx,
section.key,
e.nativeEvent?.layout?.y ?? 0
e.nativeEvent?.layout?.y ?? 0,
);
}}
style={{
+5 -5
View File
@@ -2,7 +2,7 @@ import FontAwesome from "@expo/vector-icons/FontAwesome";
import { BlurView } from "expo-blur";
import React, { useCallback, useMemo } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { ai, background } from "../assets";
import { ai, background, cardsImg } from "../assets";
import Page from "../layouts/Page";
import { navigate } from "../navigation/NavigationService";
import { useUserData } from "../providers/UserDataProvider";
@@ -32,15 +32,15 @@ const CREATE_DATA = [
stageKey: "director",
img: ai.john,
bg: background.playbackBG,
label: "John",
desc: "Come back when your\naudio is ready!",
label: "Theo",
desc: "Theo t'accompagne pour créer ton playback.",
type: "Director",
},
{
stageKey: "publisher",
img: ai.bena,
img: cardsImg.production,
bg: background.productionBG2,
label: "Bena",
label: "Publication",
desc: "Publions ta vidéo sur YouTube !",
type: "Producteur",
},
+86 -125
View File
@@ -1,7 +1,6 @@
import React from "react";
import {
ActivityIndicator,
Linking,
Pressable,
StyleSheet,
Text,
@@ -17,13 +16,7 @@ import { background, subBadges } from "../assets";
import { Palette, gutters } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { isWeb } from "../hooks/useLayoutType";
import { getFunctionsClient } from "../config/firebase";
const FUNCTIONS_REGION = "europe-west1";
const STRIPE_SUCCESS_URL =
"https://dashboard.stripe.com/test/billing/starter-guide/checkout-success";
const STRIPE_CANCEL_URL = STRIPE_SUCCESS_URL;
import { useStripe } from "../providers/StripeProvider";
const formatCurrency = (amount, currency = "eur") => {
if (typeof amount !== "number") {
@@ -298,120 +291,114 @@ export default function Payments() {
initialPack && initialPack !== "packs" ? initialPack : null;
const backgroundImage = background.bgTrans;
const [plans, setPlans] = React.useState({ monthly: [], annual: [] });
const {
subscriptions,
isCatalogLoading,
catalogError,
createSubscriptionCheckout,
} = useStripe();
const [selectedPriceIds, setSelectedPriceIds] = React.useState({
monthly: null,
annual: null,
});
const [billingPeriod, setBillingPeriod] = React.useState("monthly");
const [isLoadingPlans, setIsLoadingPlans] = React.useState(true);
const [processingPriceId, setProcessingPriceId] = React.useState(null);
const [errorMessage, setErrorMessage] = React.useState(null);
const initialPackHandledRef = React.useRef(false);
const normalizedPlans = React.useMemo(
() => ({
monthly: normalizePlans(subscriptions?.monthly, "monthly"),
annual: normalizePlans(subscriptions?.annual, "annual"),
}),
[subscriptions?.monthly, subscriptions?.annual],
);
const hasAnyPlan =
(normalizedPlans?.monthly?.length || 0) > 0 ||
(normalizedPlans?.annual?.length || 0) > 0;
const isLoadingPlans = isCatalogLoading && !hasAnyPlan;
const combinedErrorMessage = errorMessage || catalogError;
React.useEffect(() => {
initialPackHandledRef.current = false;
}, [initialSubscriptionPack]);
const fetchPlans = React.useCallback(async () => {
setIsLoadingPlans(true);
setErrorMessage(null);
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
"subscription-listSubscriptionPlans",
);
const { data } = await callable();
const normalizedPlans = {
monthly: normalizePlans(data?.plans?.monthly, "monthly"),
annual: normalizePlans(data?.plans?.annual, "annual"),
};
React.useEffect(() => {
const shouldApplyPack =
Boolean(initialSubscriptionPack) && !initialPackHandledRef.current;
setPlans(normalizedPlans);
let matchedPeriod = null;
let matchedPriceId = null;
let matchedPeriod = null;
let matchedPriceId = null;
const shouldApplyPack =
Boolean(initialSubscriptionPack) && !initialPackHandledRef.current;
if (shouldApplyPack) {
const desired = initialSubscriptionPack.trim().toLowerCase();
for (const [periodKey, mapping] of Object.entries(
PACK_PRICE_ID_BY_PERIOD,
)) {
const candidateId = mapping[desired];
if (!candidateId) continue;
const exists = (normalizedPlans[periodKey] || []).some(
(plan) => plan?.priceId === candidateId,
);
if (exists) {
matchedPeriod = periodKey;
matchedPriceId = candidateId;
break;
}
if (shouldApplyPack && normalizedPlans) {
const desired = initialSubscriptionPack.trim().toLowerCase();
for (const [periodKey, mapping] of Object.entries(
PACK_PRICE_ID_BY_PERIOD,
)) {
const candidateId = mapping[desired];
if (!candidateId) continue;
const exists = (normalizedPlans[periodKey] || []).some(
(plan) => plan?.priceId === candidateId,
);
if (exists) {
matchedPeriod = periodKey;
matchedPriceId = candidateId;
break;
}
}
}
setSelectedPriceIds((current) => {
const next = { ...current };
setSelectedPriceIds((current) => {
const next = { ...current };
PLAN_SEGMENTS.forEach(({ key }) => {
const periodPlans = normalizedPlans[key] || [];
if (!periodPlans.length) {
next[key] = null;
return;
}
const alreadySelected = periodPlans.some(
(plan) => plan.priceId === next[key],
);
if (shouldApplyPack && matchedPeriod === null) {
const matched = periodPlans.find((plan) => {
const label = (plan?.product?.name || plan?.nickname || "")
.toString()
.toLowerCase();
return label.includes(initialSubscriptionPack);
});
if (matched) {
matchedPeriod = key;
matchedPriceId = matched.priceId;
}
}
next[key] = alreadySelected ? next[key] : periodPlans[0].priceId;
});
if (matchedPeriod && matchedPriceId) {
next[matchedPeriod] = matchedPriceId;
PLAN_SEGMENTS.forEach(({ key }) => {
const periodPlans = normalizedPlans[key] || [];
if (!periodPlans.length) {
next[key] = null;
return;
}
return next;
const alreadySelected = periodPlans.some(
(plan) => plan.priceId === next[key],
);
if (shouldApplyPack && matchedPeriod === null) {
const matched = periodPlans.find((plan) => {
const label = (plan?.product?.name || plan?.nickname || "")
.toString()
.toLowerCase();
return label.includes(initialSubscriptionPack);
});
if (matched) {
matchedPeriod = key;
matchedPriceId = matched.priceId;
}
}
next[key] = alreadySelected ? next[key] : periodPlans[0].priceId;
});
setProcessingPriceId(null);
if (matchedPeriod && matchedPriceId) {
setBillingPeriod(matchedPeriod);
initialPackHandledRef.current = true;
} else if (shouldApplyPack) {
initialPackHandledRef.current = true;
next[matchedPeriod] = matchedPriceId;
}
} catch (error) {
console.error("[Payments] fetchPlans error", error);
setErrorMessage(
error?.message || "Impossible de récupérer les abonnements Stripe.",
);
} finally {
setIsLoadingPlans(false);
return next;
});
if (matchedPeriod && matchedPriceId) {
setBillingPeriod(matchedPeriod);
initialPackHandledRef.current = true;
} else if (shouldApplyPack && !isCatalogLoading) {
initialPackHandledRef.current = true;
}
}, [initialSubscriptionPack]);
}, [
normalizedPlans,
initialSubscriptionPack,
isCatalogLoading,
]);
React.useEffect(() => {
fetchPlans();
}, [fetchPlans]);
const currentPlans = plans[billingPeriod] || [];
const currentPlans = normalizedPlans[billingPeriod] || [];
const selectedPriceId = selectedPriceIds[billingPeriod];
const handleSelect = React.useCallback(
@@ -433,33 +420,7 @@ export default function Payments() {
setProcessingPriceId(selectedPriceId);
setErrorMessage(null);
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
"subscription-createSubscriptionCheckoutSession",
);
const { data } = await callable({
priceId: selectedPriceId,
returnUrls: {
successUrl: STRIPE_SUCCESS_URL,
cancelUrl: STRIPE_CANCEL_URL,
},
});
const checkoutUrl = data?.url;
if (!checkoutUrl) {
throw new Error("Session Stripe introuvable.");
}
if (isWeb) {
if (typeof window !== "undefined") {
window.location.assign(checkoutUrl);
}
} else {
const canOpen = await Linking.canOpenURL(checkoutUrl);
if (!canOpen) {
throw new Error("Impossible d'ouvrir l'URL de paiement.");
}
await Linking.openURL(checkoutUrl);
}
await createSubscriptionCheckout(selectedPriceId);
} catch (error) {
console.error("[Payments] checkout error", error);
setErrorMessage(
@@ -469,7 +430,7 @@ export default function Payments() {
} finally {
setProcessingPriceId(null);
}
}, [isWeb, selectedPriceId]);
}, [selectedPriceId, createSubscriptionCheckout]);
React.useEffect(() => {
setSelectedPriceIds((current) => {
@@ -523,8 +484,8 @@ export default function Payments() {
</Text>
</View>
{errorMessage ? (
<Text style={styles.errorText}>{errorMessage}</Text>
{combinedErrorMessage ? (
<Text style={styles.errorText}>{combinedErrorMessage}</Text>
) : null}
{isLoadingPlans && !currentPlans.length ? (
+2 -13
View File
@@ -1,7 +1,7 @@
import { Image, StyleSheet, View } from "react-native";
import { View } from "react-native";
import React from "react";
import Page from "../../layouts/Page";
import { ai, background } from "../../assets";
import { background } from "../../assets";
import { gutters } from "../../styles";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
@@ -12,7 +12,6 @@ import { Routes } from "../../navigation";
const Production = () => {
return (
<Page headerType="NONE" backgroundImg={background.productionBG}>
<Image source={ai.bena} style={styles.img} resizeMode="contain" />
<MusicLandHeader onPressBack={goBack} progress={25} />
<View
style={{
@@ -43,13 +42,3 @@ const Production = () => {
};
export default Production;
const styles = StyleSheet.create({
img: {
width: "100%",
height: "70%",
position: "absolute",
bottom: -40,
right: -30,
},
});
+1 -43
View File
@@ -11,6 +11,7 @@ import { Routes } from "../../navigation/Routes";
import { useUserData } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { formatDate, toDate } from "../../utils/dateFormatting";
const FUNCTIONS_REGION = "europe-west1";
@@ -73,49 +74,6 @@ const getStatusColors = (status) => {
}
};
const toDate = (value) => {
if (!value) {
return null;
}
if (typeof value.toDate === "function") {
try {
return value.toDate();
} catch (_error) {
return null;
}
}
if (value instanceof Date) {
return value;
}
if (typeof value === "number" && Number.isFinite(value)) {
if (value > 1e12) {
return new Date(value);
}
return new Date(value * 1000);
}
if (typeof value === "object" && Number.isFinite(value.seconds)) {
return new Date(value.seconds * 1000);
}
return null;
};
const formatDate = (date) => {
if (!date) {
return "À déterminer";
}
try {
return new Intl.DateTimeFormat("fr-FR", {
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
} catch (_error) {
return date.toString();
}
};
const capitalize = (value) => {
if (typeof value !== "string" || !value) {
return null;
+4 -10
View File
@@ -1,9 +1,8 @@
import { useRoute } from "@react-navigation/native";
import { Image } from "expo-image";
import React, { useCallback, useMemo, useState } from "react";
import { Linking, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { ai, background } from "../../assets";
import { background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
@@ -58,10 +57,10 @@ const PublishYoutube = () => {
: "Publier sur YouTube";
const statusMessage = useMemo(() => {
if (!playbackReady) {
return "Termine la création de ton playback avec John pour débloquer la publication YouTube.";
return "Termine la création de ton playback avec Theo pour débloquer la publication YouTube.";
}
if (isPublishing) {
return "Bena s'occupe de mettre ta vidéo en ligne... Patiente un instant.";
return "La publication de ta vidéo est en cours... Patiente un instant.";
}
if (youtubeError || youtubeStatus === "FAILED") {
return "La dernière tentative de publication a échoué. Tu peux réessayer ci-dessous.";
@@ -69,7 +68,7 @@ const PublishYoutube = () => {
if (hasYoutubePublication) {
return "Ta vidéo est publiée sur la chaîne YouTube MusicLand. Tu peux la partager dès maintenant.";
}
return "Bena taccompagne pour partager ton playback sur la chaîne YouTube MusicLand.";
return "Partage ton playback sur la chaîne YouTube MusicLand.";
}, [
hasYoutubePublication,
isPublishing,
@@ -175,11 +174,6 @@ const PublishYoutube = () => {
}}
>
<View style={{ alignItems: "center", gap: 18 }}>
<Image
source={ai.bena}
style={{ width: 220, height: 320 }}
contentFit="contain"
/>
<Text
style={{
fontSize: 22,
+2 -13
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { Image, StyleSheet, View } from "react-native";
import { ai, background } from "../../assets";
import { View } from "react-native";
import { background } from "../../assets";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
@@ -21,7 +21,6 @@ const Compose = () => {
}, []);
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
<Image source={ai.malik} style={styles.img} resizeMode="contain" />
<MusicLandHeader
onPressBack={goBack}
progress={9}
@@ -53,13 +52,3 @@ const Compose = () => {
};
export default Compose;
const styles = StyleSheet.create({
img: {
width: "100%",
height: "70%",
position: "absolute",
bottom: -40,
right: -30,
},
});
+1 -17
View File
@@ -3,7 +3,7 @@ import moment from "moment";
import React, { useEffect, useState } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { ai, icons } from "../../assets";
import { icons } from "../../assets";
import AppAlert from "../../components/Alert";
import GradientButton from "../../components/GradientButton";
import ProgressBar from "../../components/ProgressBar";
@@ -194,22 +194,6 @@ const CreatingSong = ({ active, config }) => {
height: "50%",
}}
>
<View
style={{
zIndex: 1,
position: "absolute",
top: -150,
width: "50%",
height: "80%",
alignSelf: "center",
}}
>
<Image
source={ai.malik}
style={{ width: "100%", height: "100%", right: -10 }}
resizeMode="contain"
/>
</View>
<View
style={{
flex: 1,
+6 -2
View File
@@ -338,7 +338,7 @@ const SongReady = () => {
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
title="Ta chanson est prête !"
subTitle="Quen penses-tu ?"
subTitle="Choisis ton morceau"
containerStyle={{
marginBottom: responsiveHeight(2),
}}
@@ -432,7 +432,11 @@ const SongReady = () => {
try {
const player = idx === 0 ? player0 : player1;
if (player && wasPlayingBeforeSeek.current[idx]) {
await player.play?.();
if (player.resume) {
await player.resume?.();
} else {
await player.play?.();
}
setIsPlaying((p) => ({ ...p, [idx]: true }));
}
wasPlayingBeforeSeek.current[idx] = false;
+2 -2
View File
@@ -286,7 +286,7 @@ const Lyrics = ({ navigation }) => {
await setIsLoading(false);
alert(
"Céline",
"Bravo ! Vous venez de terminer de créer les paroles de votre musique ! Maintenant vous pouvez passer à la prochaine étape : le Studio avec Malik. Dans cette étape vous allez pouvoir donner vie à votre chanson !",
"Bravo ! Vous venez de terminer de créer les paroles de votre musique ! Maintenant vous pouvez passer à la prochaine étape : le Studio pour donner vie à votre chanson !",
[
{
text: "Retour à l'accueil",
@@ -300,7 +300,7 @@ const Lyrics = ({ navigation }) => {
}),
},
{
text: "Continuer avec Malik",
text: "Continuer vers le Studio",
onPress: () => {
const targetRoute = beatmakerStage?.route || Routes.Compose;
navigate(targetRoute, beatmakerStage?.params);
+1 -1
View File
@@ -8,7 +8,7 @@ const SongTo = ({ audience, setAudience }) => {
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader title="À qui sadresse ta chanson ?" />
<CustomInput
placeholder="Ecrire mon contexte"
placeholder="Écris à qui s'adresse ta chanson"
height={283}
value={audience}
setValue={setAudience}
+1 -1
View File
@@ -11,7 +11,7 @@ const SpecificityContext = ({ context, setContext }) => {
subTitle="Dis-nous en un peu plus pour quon puisse mieux taider."
/>
<CustomInput
placeholder="Ecrire mon contexte"
placeholder="Écris à qui s'adresse ta chanson"
height={283}
value={context}
setValue={setContext}
+1 -13
View File
@@ -1,8 +1,7 @@
import { BlurView } from "expo-blur";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Image, Modal, Pressable, StyleSheet, Text, View } from "react-native";
import { Modal, Pressable, StyleSheet, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit";
import { ai } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton";
@@ -277,17 +276,6 @@ export const ChooseCoverType = () => {
backgroundColor={"#2A2E33"}
headerType="NONE"
>
<Image
source={ai.bena}
style={{
width: "100%",
height: "70%",
position: "absolute",
bottom: -40,
right: -30,
}}
resizeMode="contain"
/>
<MusicLandHeader onPressBack={goBack} progress={9} />
<View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
+3 -3
View File
@@ -252,8 +252,8 @@ const PouchReady = () => {
const playbackStage = getStageAction("director", projectForStage);
await setLoading(false);
alert(
"Malik",
"Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec John pour produire ton playback.",
"Playback prêt",
"Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec Theo pour produire ton playback.",
[
{
text: "Retour à l'accueil",
@@ -261,7 +261,7 @@ const PouchReady = () => {
onPress: () => navigate(Routes.Home),
},
{
text: "Continuer avec John",
text: "Continuer avec Theo",
onPress: () => {
const targetRoute = playbackStage?.route || Routes.Playback;
const params = playbackStage?.params || {
+3 -3
View File
@@ -117,8 +117,8 @@ const ValidateCover = () => {
const playbackStage = getStageAction("director", projectForStage);
await setIsLoading(false);
alert(
"Malik",
"Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec John pour produire ton playback.",
"Playback prêt",
"Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec Theo pour produire ton playback.",
[
{
text: "Retour à l'accueil",
@@ -126,7 +126,7 @@ const ValidateCover = () => {
onPress: () => navigate(Routes.Home),
},
{
text: "Continuer avec John",
text: "Continuer avec Theo",
onPress: () => {
const targetRoute = playbackStage?.route || Routes.Playback;
const params =