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
+20 -3
View File
@@ -4,8 +4,19 @@ exports.generatePicturePrompt = (project = {}) => {
lyrics: lyricsRaw, lyrics: lyricsRaw,
musicConfig = {}, musicConfig = {},
coverStyle: coverStyleRaw = "", coverStyle: coverStyleRaw = "",
artistName: artistNameRaw = "",
} = project || {}; } = project || {};
const sanitizeInline = (value = "") => {
if (typeof value !== "string") return "";
return value.replace(/[\r\n]+/g, " ").replace(/[<>]/g, "").trim();
};
const titleForPrompt = sanitizeInline(title) || "Sans titre";
const artistName =
sanitizeInline(artistNameRaw) || sanitizeInline(project?.userName || "");
const hasArtistName = artistName.length > 0;
const { const {
genres = [], genres = [],
tempo = "", tempo = "",
@@ -55,11 +66,17 @@ exports.generatePicturePrompt = (project = {}) => {
const styleLine = coverStyle const styleLine = coverStyle
? `${coverStyle}. ${paletteHint}` ? `${coverStyle}. ${paletteHint}`
: `${paletteHint}. Style libre, artistique et lumineux.`; : `${paletteHint}. Style libre, artistique et lumineux.`;
const typographyLine = hasArtistName
? `Reproduis strictement le titre de la chanson ${titleForPrompt} sans modification, sans traduction et sans ajout ou suppression de caractères. Ajoute également le nom de l'artiste ${artistName} de manière artistique, parfaitement lisible et hiérarchisée, en harmonie avec le style abstrait et lumineux de la pochette.`
: `Reproduis strictement le titre de la chanson ${titleForPrompt} sans modification, sans traduction et sans ajout ou suppression de caractères. Intègre-le de manière artistique et parfaitement lisible, en harmonie avec le style abstrait et lumineux de la pochette.`;
const artistLine = hasArtistName
? ` <ARTISTE>${artistName}</ARTISTE>\n`
: "";
const prompt = `<BRIEF> const prompt = `<BRIEF>
<OBJECTIF>Créer une pochette d'album en adéquation avec les paroles de la musique</OBJECTIF> <OBJECTIF>Créer une pochette d'album en adéquation avec les paroles de la musique</OBJECTIF>
<TITRE>${title || "Sans titre"}</TITRE> <TITRE>${titleForPrompt}</TITRE>
<STYLE_MUSICAL>${tagsLine}</STYLE_MUSICAL> ${artistLine} <STYLE_MUSICAL>${tagsLine}</STYLE_MUSICAL>
<EXTRAITS_PAROLES>${lyricsLine}</EXTRAITS_PAROLES> <EXTRAITS_PAROLES>${lyricsLine}</EXTRAITS_PAROLES>
</BRIEF> </BRIEF>
@@ -67,7 +84,7 @@ exports.generatePicturePrompt = (project = {}) => {
<FORMAT>Image carrée 1024x1024 pixels, résolution haute.</FORMAT> <FORMAT>Image carrée 1024x1024 pixels, résolution haute.</FORMAT>
<STYLE>${styleLine}</STYLE> <STYLE>${styleLine}</STYLE>
<COMPOSITION>La composition doit être dynamique et remplir toute la surface, sans laisser de bordures ni de zones vides.</COMPOSITION> <COMPOSITION>La composition doit être dynamique et remplir toute la surface, sans laisser de bordures ni de zones vides.</COMPOSITION>
<TYPOGRAPHIE>Reproduis strictement le titre de la chanson ${title || ""} sans modification, sans traduction et sans ajout ou suppression de caractères. Intègre-le de manière artistique et parfaitement lisible, en harmonie avec le style abstrait et lumineux de la pochette.</TYPOGRAPHIE> <TYPOGRAPHIE>${typographyLine}</TYPOGRAPHIE>
<ORTHOGRAPHE>Aucune faute d'orthographe n'est tolérée. Respecte la casse et l'orthographe exactes du titre fourni.</ORTHOGRAPHE> <ORTHOGRAPHE>Aucune faute d'orthographe n'est tolérée. Respecte la casse et l'orthographe exactes du titre fourni.</ORTHOGRAPHE>
</CONTEXTES_VISUELS> </CONTEXTES_VISUELS>
+64 -1
View File
@@ -14,6 +14,65 @@ const { sendNotification } = require("./notifications");
const bucket = admin.storage().bucket(); const bucket = admin.storage().bucket();
const LOGO_PATH = path.resolve(__dirname, "../assets/musicLandLogo.png"); const LOGO_PATH = path.resolve(__dirname, "../assets/musicLandLogo.png");
const pickFirstNonEmpty = (...values) => {
for (const value of values) {
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.length > 0) {
return trimmed;
}
}
}
return "";
};
const combineNames = (...parts) =>
parts
.map((part) => (typeof part === "string" ? part.trim() : ""))
.filter(Boolean)
.join(" ")
.trim();
async function resolveArtistName(project = {}) {
const owner = project?.owner || {};
const direct = pickFirstNonEmpty(
project?.artistName,
project?.userName,
owner?.artistName,
owner?.userName,
owner?.displayName
);
if (direct) return direct;
const userId =
typeof project?.userId === "string" && project.userId.trim()
? project.userId.trim()
: "";
if (!userId) return "";
try {
const userSnapshot = await refList.users.doc(userId).get();
if (!userSnapshot?.exists) return "";
const userData = userSnapshot.data() || {};
const fullName = combineNames(userData.firstName, userData.lastName);
return (
pickFirstNonEmpty(
userData.artistName,
userData.userName,
userData.displayName,
fullName
) || ""
);
} catch (error) {
logger.warn("⚠️ [Cover] Unable to resolve artist name", {
projectId: project?.id || null,
userId,
error: error?.message || String(error),
});
return "";
}
}
async function buildCoverWithLogo(backgroundUrl, targetPath) { async function buildCoverWithLogo(backgroundUrl, targetPath) {
logger.info("🖼️ [Cover] Adding logo to generated background"); logger.info("🖼️ [Cover] Adding logo to generated background");
const [{ data: backgroundBuffer }, logoBuffer] = await Promise.all([ const [{ data: backgroundBuffer }, logoBuffer] = await Promise.all([
@@ -66,7 +125,11 @@ async function buildCoverWithLogo(backgroundUrl, targetPath) {
// Shared core for generating and saving the cover, and updating the project // Shared core for generating and saving the cover, and updating the project
async function performCoverGeneration(project) { async function performCoverGeneration(project) {
const t0 = Date.now(); const t0 = Date.now();
const prompt = generatePicturePrompt(project); const artistName = await resolveArtistName(project);
const prompt = generatePicturePrompt({
...project,
artistName,
});
// Utiliser generateImageV2 qui renvoie directement l'URL publique // Utiliser generateImageV2 qui renvoie directement l'URL publique
logger.info("🎨 [Cover] Calling model V2", { logger.info("🎨 [Cover] Calling model V2", {
projectId: project.id, projectId: project.id,
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

+1
View File
@@ -202,6 +202,7 @@ export const background = {
libraryBG, libraryBG,
libraryBgWeb: require("./UI/libraryBgWeb.png"), libraryBgWeb: require("./UI/libraryBgWeb.png"),
libraryBG2, libraryBG2,
libraryBG2Web: require("./UI/libraryBG2Web.png"),
profileBG, profileBG,
profileBgWeb: require("./UI/profileBgWeb.png"), profileBgWeb: require("./UI/profileBgWeb.png"),
hitParadeBG, hitParadeBG,
@@ -15,7 +15,7 @@ import {
useWindowDimensions, useWindowDimensions,
} from "react-native"; } from "react-native";
import { responsiveHeight } from "../../actions/responsiveSizes"; import { responsiveHeight } from "../../actions/responsiveSizes";
import { ai } from "../../assets"; import { ai, cardsImg } from "../../assets";
import { gutters } from "../../styles"; import { gutters } from "../../styles";
import { getCreationStageStates } from "../../utils/projectStages"; import { getCreationStageStates } from "../../utils/projectStages";
import AnimatedPaginationDot from "../AnimatedPaginationDot/AnimatedPaginationDot"; import AnimatedPaginationDot from "../AnimatedPaginationDot/AnimatedPaginationDot";
@@ -36,15 +36,15 @@ const STAGE_CARD_CONTENT = [
}, },
{ {
key: "director", key: "director",
title: "John", title: "Theo",
description: "Come back when your audio is ready!", description: "Theo t'accompagne pour créer ton playback.",
image: ai.rightIcon, image: ai.rightIcon,
}, },
{ {
key: "publisher", key: "publisher",
title: "Bena", title: "Publication",
description: "Ta vidéo est prête ? Direction YouTube !", description: "Ta vidéo est prête ? Direction YouTube !",
image: ai.bena, image: cardsImg.production,
}, },
]; ];
@@ -14,7 +14,7 @@ import {
View, View,
useWindowDimensions, useWindowDimensions,
} from "react-native"; } from "react-native";
import { ai } from "../../assets"; import { ai, cardsImg } from "../../assets";
import { getCreationStageStates } from "../../utils/projectStages"; import { getCreationStageStates } from "../../utils/projectStages";
import AnimatedPaginationDot from "../AnimatedPaginationDot/AnimatedPaginationDot"; import AnimatedPaginationDot from "../AnimatedPaginationDot/AnimatedPaginationDot";
import PersonaCard from "../cards/PersonaCard/PersonaCard"; import PersonaCard from "../cards/PersonaCard/PersonaCard";
@@ -29,21 +29,21 @@ const STAGE_CARD_CONTENT = [
}, },
{ {
key: "beatmaker", key: "beatmaker",
title: "Malik", title: "Theo",
description: "Come back, when you'll have lyrics!", description: "Come back, when you'll have lyrics!",
image: ai.rightIcon, image: ai.rightIcon,
}, },
{ {
key: "director", key: "director",
title: "John", title: "Theo",
description: "Come back when your audio is ready!", description: "Theo t'accompagne pour créer ton playback.",
image: ai.rightIcon, image: ai.rightIcon,
}, },
{ {
key: "publisher", key: "publisher",
title: "Bena", title: "Publication",
description: "Ta vidéo est prête ? Direction YouTube !", description: "Ta vidéo est prête ? Direction YouTube !",
image: ai.bena, image: cardsImg.production,
}, },
]; ];
+56 -76
View File
@@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { import {
ActivityIndicator, ActivityIndicator,
Linking,
Modal, Modal,
Pressable, Pressable,
ScrollView, ScrollView,
@@ -15,16 +14,11 @@ import GradientButton from "../GradientButton";
import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader"; import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader";
import { Palette, gutters } from "../../styles"; import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { getFunctionsClient } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType"; import { isWeb } from "../../hooks/useLayoutType";
import CreditAmount from "../CreditAmount"; import CreditAmount from "../CreditAmount";
import { useStripe } from "../../providers/StripeProvider";
const WEB_MODAL_MAX_WIDTH = 1000; const WEB_MODAL_MAX_WIDTH = 1000;
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;
const formatCurrency = (amount, currency = "eur") => { const formatCurrency = (amount, currency = "eur") => {
if (typeof amount !== "number") { if (typeof amount !== "number") {
return null; return null;
@@ -92,49 +86,60 @@ function CoinPackCard({ pack, selected, onSelect }) {
} }
const CoinPackModal = ({ visible, onClose }) => { const CoinPackModal = ({ visible, onClose }) => {
const [coinPacks, setCoinPacks] = React.useState([]);
const [selectedPackId, setSelectedPackId] = React.useState(null); const [selectedPackId, setSelectedPackId] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [isProcessing, setIsProcessing] = React.useState(false); const [isProcessing, setIsProcessing] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState(null); const [errorMessage, setErrorMessage] = React.useState(null);
const modalMaxWidth = isWeb ? WEB_MODAL_MAX_WIDTH : undefined; const modalMaxWidth = isWeb ? WEB_MODAL_MAX_WIDTH : undefined;
const fetchCoinPacks = React.useCallback(async () => { const {
setIsLoading(true); coinPacks,
setErrorMessage(null); isCatalogLoading,
try { catalogError,
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable( refreshCatalog,
"subscription-listCoinPacks", createCoinPackCheckout,
); } = useStripe();
const { data } = await callable();
const packs = Array.isArray(data?.packs) ? data.packs : []; React.useEffect(() => {
setCoinPacks(packs); if (!coinPacks.length) {
setSelectedPackId((current) => { setSelectedPackId(null);
if ( return;
current &&
packs.some((pack) => pack?.productId && pack.productId === current)
) {
return current;
}
return packs[0]?.productId || null;
});
} catch (error) {
console.error("[CoinPackModal] fetchCoinPacks error", error);
setErrorMessage(
error?.message ||
"Impossible de charger les packs de pièces. Réessaie plus tard.",
);
} finally {
setIsLoading(false);
} }
}, []); setSelectedPackId((current) => {
if (
current &&
coinPacks.some((pack) => pack?.productId && pack.productId === current)
) {
return current;
}
return coinPacks[0]?.productId || null;
});
}, [coinPacks]);
React.useEffect(() => { React.useEffect(() => {
if (!visible) { if (!visible) {
return; return;
} }
fetchCoinPacks(); if (!coinPacks.length && !isCatalogLoading && !catalogError) {
}, [visible, fetchCoinPacks]); refreshCatalog();
}
}, [
visible,
coinPacks.length,
isCatalogLoading,
catalogError,
refreshCatalog,
]);
const closeModal = React.useCallback(
({ force = false } = {}) => {
if (isProcessing && !force) {
return;
}
setIsProcessing(false);
onClose?.();
},
[isProcessing, onClose],
);
const handleCheckout = React.useCallback(async () => { const handleCheckout = React.useCallback(async () => {
if (!selectedPackId) { if (!selectedPackId) {
@@ -145,33 +150,8 @@ const CoinPackModal = ({ visible, onClose }) => {
setErrorMessage(null); setErrorMessage(null);
try { try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable( await createCoinPackCheckout(selectedPackId);
"subscription-createCoinPackCheckoutSession", closeModal({ force: true });
);
const { data } = await callable({
productId: selectedPackId,
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);
}
} catch (error) { } catch (error) {
console.error("[CoinPackModal] checkout error", error); console.error("[CoinPackModal] checkout error", error);
setErrorMessage( setErrorMessage(
@@ -181,17 +161,17 @@ const CoinPackModal = ({ visible, onClose }) => {
} finally { } finally {
setIsProcessing(false); setIsProcessing(false);
} }
}, [selectedPackId, isWeb]); }, [selectedPackId, createCoinPackCheckout, closeModal]);
const handleClose = React.useCallback(() => { const handleClose = React.useCallback(() => {
if (isProcessing) { closeModal();
return; }, [closeModal]);
}
onClose?.(); const isLoadingCoinPacks = isCatalogLoading && !coinPacks.length;
}, [isProcessing, onClose]); const combinedErrorMessage = errorMessage || catalogError;
const renderContent = () => { const renderContent = () => {
if (isLoading) { if (isLoadingCoinPacks) {
return ( return (
<View style={styles.loaderContainer}> <View style={styles.loaderContainer}>
<ActivityIndicator color={Palette.white} /> <ActivityIndicator color={Palette.white} />
@@ -250,8 +230,8 @@ const CoinPackModal = ({ visible, onClose }) => {
</Text> </Text>
</View> </View>
{errorMessage ? ( {combinedErrorMessage ? (
<Text style={styles.errorText}>{errorMessage}</Text> <Text style={styles.errorText}>{combinedErrorMessage}</Text>
) : null} ) : null}
<View style={styles.content}>{renderContent()}</View> <View style={styles.content}>{renderContent()}</View>
@@ -263,7 +243,7 @@ const CoinPackModal = ({ visible, onClose }) => {
disabled={ disabled={
!selectedPackId || !selectedPackId ||
isProcessing || isProcessing ||
isLoading || isLoadingCoinPacks ||
!coinPacks.length !coinPacks.length
} }
/> />
+1
View File
@@ -475,6 +475,7 @@ export const INSTRUMENTS = [
"Synthétiseur", "Synthétiseur",
"Guitare acoustique", "Guitare acoustique",
"Guitare électrique", "Guitare électrique",
"Batterie",
"Banjo", "Banjo",
"Violon", "Violon",
"Saxophone", "Saxophone",
+4 -4
View File
@@ -1,10 +1,10 @@
import { useGlobal, useContext } from "reactn"; import { useGlobal } from "reactn";
import firebase from "../config/firebase"; import firebase from "../config/firebase";
import { checkBillingDetails } from "../helpers"; import { checkBillingDetails } from "../helpers";
import { StripeEmbeddedContext } from "../providers/StripeEmbeddedProvider";
import { useWebView } from "../providers/WebViewProvider"; import { useWebView } from "../providers/WebViewProvider";
import { useStripe } from "../providers/StripeProvider";
import useLayoutType from "./useLayoutType"; import useLayoutType from "./useLayoutType";
@@ -17,7 +17,7 @@ const usePaymentSession = () => {
const [currentProjectID] = useGlobal("currentProjectID"); const [currentProjectID] = useGlobal("currentProjectID");
const { setClientSecret } = useContext(StripeEmbeddedContext); const { openEmbeddedCheckout } = useStripe();
const onCreatePaymentSession = async ({ const onCreatePaymentSession = async ({
numberOfCoin = 0, numberOfCoin = 0,
@@ -48,7 +48,7 @@ const usePaymentSession = () => {
if (isNative) { if (isNative) {
setWebViewUrl(data); setWebViewUrl(data);
} else { } else {
setClientSecret(data); openEmbeddedCheckout(data);
} }
} else { } else {
throw new Error("Erreur lors de la création du paiement"); throw new Error("Erreur lors de la création du paiement");
+2 -2
View File
@@ -7,7 +7,7 @@ import { SheetProvider } from "react-native-actions-sheet";
import NotificationProvider from "./NotificationProvider"; import NotificationProvider from "./NotificationProvider";
import PlayerProvider from "./PlayerProvider"; import PlayerProvider from "./PlayerProvider";
import SplashAnimationProvider from "./SplashAnimationProvider"; import SplashAnimationProvider from "./SplashAnimationProvider";
import StripeEmbeddedProvider from "./StripeEmbeddedProvider"; import StripeProvider from "./StripeProvider";
import UniversalLinkProvider from "./UniversalLinkProvider"; import UniversalLinkProvider from "./UniversalLinkProvider";
import UserDataProvider from "./UserDataProvider"; import UserDataProvider from "./UserDataProvider";
import WebViewProvider from "./WebViewProvider"; import WebViewProvider from "./WebViewProvider";
@@ -19,7 +19,7 @@ const SharedProviders = ({ children }) => {
[SplashAnimationProvider, {}], [SplashAnimationProvider, {}],
[WebViewProvider, {}], [WebViewProvider, {}],
[UserDataProvider, {}], [UserDataProvider, {}],
[StripeEmbeddedProvider, {}], [StripeProvider, {}],
[UniversalLinkProvider, {}], [UniversalLinkProvider, {}],
[BottomSheetModalProvider, {}], [BottomSheetModalProvider, {}],
[SheetProvider, {}], [SheetProvider, {}],
-73
View File
@@ -1,73 +0,0 @@
import {
EmbeddedCheckoutProvider,
EmbeddedCheckout,
} from "@stripe/react-stripe-js";
import { useState, createContext } from "react";
import { View, StyleSheet } from "react-native";
import { loadStripe } from "@stripe/stripe-js";
import {
STRIPE_PUBLISHABLE_KEY_LIVE,
STRIPE_PUBLISHABLE_KEY_TEST,
} from "../data/keys";
import { mainBorderRadius } from "../styles/Style";
import Overlay from "../components/Overlay";
import Button from "../components/Button";
import useLayoutType from "../hooks/useLayoutType";
const isStripeTesting = false;
const stripePromise = loadStripe(
isStripeTesting ? STRIPE_PUBLISHABLE_KEY_TEST : STRIPE_PUBLISHABLE_KEY_LIVE
);
export const StripeEmbeddedContext = createContext();
export default ({ children }) => {
const { isMobile } = useLayoutType();
const [clientSecret, setClientSecret] = useState(null);
return (
<StripeEmbeddedContext.Provider value={{ setClientSecret }}>
{children}
<Overlay
isVisible={clientSecret !== null}
setIsVisible={() => setClientSecret(null)}
>
<View
style={{
position: "absolute",
...StyleSheet.absoluteFillObject,
justifyContent: "center",
alignItems: "center",
}}
>
<View
style={{
alignSelf: "center",
width: isMobile ? "90%" : "80%",
height: "70vh",
borderRadius: mainBorderRadius,
overflow: "scroll",
}}
>
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{ clientSecret }}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
</View>
<Button
type="secondary"
isAbsoluteBottom
text="Fermer"
onPress={() => setClientSecret(null)}
/>
</View>
</Overlay>
</StripeEmbeddedContext.Provider>
);
};
+342
View File
@@ -0,0 +1,342 @@
import React from "react";
import { Linking, StyleSheet, View } from "react-native";
import {
EmbeddedCheckout,
EmbeddedCheckoutProvider,
} from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import Overlay from "../components/Overlay";
import Button from "../components/Button";
import useLayoutType from "../hooks/useLayoutType";
import { mainBorderRadius } from "../styles/Style";
import {
STRIPE_PUBLISHABLE_KEY_LIVE,
STRIPE_PUBLISHABLE_KEY_TEST,
} from "../data/keys";
import { getFunctionsClient } from "../config/firebase";
import { isWeb } from "../hooks/useLayoutType";
import { useUserData } from "./UserDataProvider";
import { formatDate, toDate } from "../utils/dateFormatting";
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;
const isStripeTesting = false;
const stripePromise = loadStripe(
isStripeTesting ? STRIPE_PUBLISHABLE_KEY_TEST : STRIPE_PUBLISHABLE_KEY_LIVE,
);
export const StripeContext = React.createContext(null);
export const useStripe = () => {
const context = React.useContext(StripeContext);
if (!context) {
throw new Error("useStripe must be used within a StripeProvider");
}
return context;
};
const StripeProvider = ({ children }) => {
const { isMobile } = useLayoutType();
const { currentUserData } = useUserData() || {};
const [clientSecret, setClientSecret] = React.useState(null);
const [subscriptions, setSubscriptions] = React.useState({
monthly: [],
annual: [],
});
const [coinPacks, setCoinPacks] = React.useState([]);
const [isCatalogLoading, setIsCatalogLoading] = React.useState(true);
const [catalogError, setCatalogError] = React.useState(null);
const [activeSubscription, setActiveSubscription] = React.useState(null);
const [isActiveSubscriptionLoading, setIsActiveSubscriptionLoading] =
React.useState(false);
const [activeSubscriptionError, setActiveSubscriptionError] =
React.useState(null);
const stripeCustomerId = currentUserData?.stripeCustomerId || null;
const localSubscriptionId =
currentUserData?.stripeSubscription?.id ||
currentUserData?.stripeSubscription?.subscriptionId ||
null;
const closeEmbeddedCheckout = React.useCallback(() => {
setClientSecret(null);
}, []);
const redirectToCheckout = React.useCallback(async (checkoutUrl) => {
if (!checkoutUrl) {
throw new Error("Session Stripe introuvable.");
}
if (isWeb) {
if (typeof window !== "undefined") {
window.location.assign(checkoutUrl);
return;
}
throw new Error("Navigation Stripe impossible dans cet environnement.");
}
const canOpen = await Linking.canOpenURL(checkoutUrl);
if (!canOpen) {
throw new Error("Impossible d'ouvrir l'URL de paiement.");
}
await Linking.openURL(checkoutUrl);
}, []);
const runCheckoutSession = React.useCallback(
async ({ callableName, payload, logTag }) => {
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
callableName,
);
const { data } = await callable(payload);
const checkoutUrl = data?.url;
if (!checkoutUrl) {
throw new Error("Session Stripe introuvable.");
}
await redirectToCheckout(checkoutUrl);
} catch (error) {
console.error(`[StripeProvider] ${logTag}`, error);
throw new Error(
error?.message ||
"Une erreur est survenue lors de la création de la session Stripe.",
);
}
},
[redirectToCheckout],
);
const createSubscriptionCheckout = React.useCallback(
async (priceId) => {
if (!priceId) {
throw new Error("Aucun abonnement sélectionné.");
}
await runCheckoutSession({
callableName: "subscription-createSubscriptionCheckoutSession",
payload: {
priceId,
returnUrls: {
successUrl: STRIPE_SUCCESS_URL,
cancelUrl: STRIPE_CANCEL_URL,
},
},
logTag: "subscription checkout error",
});
},
[runCheckoutSession],
);
const createCoinPackCheckout = React.useCallback(
async (productId) => {
if (!productId) {
throw new Error("Aucun pack sélectionné.");
}
await runCheckoutSession({
callableName: "subscription-createCoinPackCheckoutSession",
payload: {
productId,
returnUrls: {
successUrl: STRIPE_SUCCESS_URL,
cancelUrl: STRIPE_CANCEL_URL,
},
},
logTag: "coin pack checkout error",
});
},
[runCheckoutSession],
);
const fetchActiveSubscription = React.useCallback(async () => {
const hasLookupContext =
Boolean(stripeCustomerId) || Boolean(localSubscriptionId);
if (!hasLookupContext) {
setActiveSubscription(null);
setActiveSubscriptionError(null);
setIsActiveSubscriptionLoading(false);
return;
}
setIsActiveSubscriptionLoading(true);
setActiveSubscriptionError(null);
try {
const callable = getFunctionsClient(
FUNCTIONS_REGION,
).httpsCallable("subscription-getActiveSubscription");
const { data } = await callable();
setActiveSubscription(data?.subscription || null);
} catch (error) {
console.error("[StripeProvider] getActiveSubscription error", error);
setActiveSubscriptionError(
error?.message ||
"Impossible de mettre à jour les informations d'abonnement.",
);
setActiveSubscription(null);
} finally {
setIsActiveSubscriptionLoading(false);
}
}, [stripeCustomerId, localSubscriptionId]);
const fetchStripeCatalog = React.useCallback(async () => {
setIsCatalogLoading(true);
setCatalogError(null);
const functionsClient = getFunctionsClient(FUNCTIONS_REGION);
let lastError = null;
try {
const callable =
functionsClient.httpsCallable("subscription-listSubscriptionPlans");
const { data } = await callable();
const nextPlans = data?.plans || {};
setSubscriptions({
monthly: Array.isArray(nextPlans?.monthly) ? nextPlans.monthly : [],
annual: Array.isArray(nextPlans?.annual) ? nextPlans.annual : [],
});
} catch (error) {
lastError = error;
console.error("[StripeProvider] listSubscriptionPlans error", error);
}
try {
const callable =
functionsClient.httpsCallable("subscription-listCoinPacks");
const { data } = await callable();
setCoinPacks(Array.isArray(data?.packs) ? data.packs : []);
} catch (error) {
lastError = error;
console.error("[StripeProvider] listCoinPacks error", error);
}
if (lastError) {
setCatalogError(
lastError?.message ||
"Impossible de récupérer les informations Stripe pour le moment.",
);
} else {
setCatalogError(null);
}
setIsCatalogLoading(false);
}, []);
React.useEffect(() => {
fetchStripeCatalog();
}, [fetchStripeCatalog]);
React.useEffect(() => {
fetchActiveSubscription();
}, [fetchActiveSubscription]);
const activeSubscriptionInfo = React.useMemo(() => {
const subscription = activeSubscription || null;
const nextRenewalDate =
toDate(subscription?.currentPeriodEnd) ||
toDate(subscription?.current_period_end) ||
null;
const subscribedSinceDate =
toDate(subscription?.created) ||
toDate(subscription?.createdAt) ||
toDate(subscription?.created_at) ||
null;
return {
subscription,
nextRenewalDate,
nextRenewalLabel: nextRenewalDate ? formatDate(nextRenewalDate) : null,
subscribedSinceDate,
subscribedSinceLabel: subscribedSinceDate
? formatDate(subscribedSinceDate)
: null,
};
}, [activeSubscription]);
const providerValue = React.useMemo(
() => ({
subscriptions,
coinPacks,
isCatalogLoading,
catalogError,
refreshCatalog: fetchStripeCatalog,
createSubscriptionCheckout,
createCoinPackCheckout,
openEmbeddedCheckout: setClientSecret,
closeEmbeddedCheckout,
activeSubscription: activeSubscriptionInfo.subscription,
activeSubscriptionInfo,
isActiveSubscriptionLoading,
activeSubscriptionError,
refreshActiveSubscription: fetchActiveSubscription,
}),
[
subscriptions,
coinPacks,
isCatalogLoading,
catalogError,
fetchStripeCatalog,
createSubscriptionCheckout,
createCoinPackCheckout,
setClientSecret,
closeEmbeddedCheckout,
activeSubscriptionInfo,
isActiveSubscriptionLoading,
activeSubscriptionError,
fetchActiveSubscription,
],
);
return (
<StripeContext.Provider value={providerValue}>
{children}
<Overlay
isVisible={clientSecret !== null}
setIsVisible={closeEmbeddedCheckout}
>
<View
style={[StyleSheet.absoluteFillObject, styles.overlayContent]}
>
<View
style={[
styles.embeddedWrapper,
{
width: isMobile ? "90%" : "80%",
},
]}
>
{clientSecret ? (
<EmbeddedCheckoutProvider
stripe={stripePromise}
options={{ clientSecret }}
>
<EmbeddedCheckout />
</EmbeddedCheckoutProvider>
) : null}
</View>
<Button
type="secondary"
isAbsoluteBottom
text="Fermer"
onPress={closeEmbeddedCheckout}
/>
</View>
</Overlay>
</StripeContext.Provider>
);
};
const styles = StyleSheet.create({
overlayContent: {
position: "absolute",
justifyContent: "center",
alignItems: "center",
},
embeddedWrapper: {
alignSelf: "center",
height: "70vh",
borderRadius: mainBorderRadius,
overflow: "scroll",
},
});
export default StripeProvider;
+1 -4
View File
@@ -2,7 +2,6 @@ import React from "react";
import LoadingProvider from "./LoadingProvider"; import LoadingProvider from "./LoadingProvider";
import TooltipProvider from "./TooltipProvider"; import TooltipProvider from "./TooltipProvider";
import StripeEmbeddedProvider from "./StripeEmbeddedProvider";
import SharedProviders from "./SharedProviders"; import SharedProviders from "./SharedProviders";
@@ -10,9 +9,7 @@ export default ({ children }) => {
return ( return (
<LoadingProvider> <LoadingProvider>
<TooltipProvider> <TooltipProvider>
<StripeEmbeddedProvider> <SharedProviders>{children}</SharedProviders>
<SharedProviders>{children}</SharedProviders>
</StripeEmbeddedProvider>
</TooltipProvider> </TooltipProvider>
</LoadingProvider> </LoadingProvider>
); );
+2
View File
@@ -99,8 +99,10 @@ export default ({ navigation }) => {
<Input <Input
label="Adresse email" label="Adresse email"
placeholder="Votre adresse email" placeholder="Votre adresse email"
type="email"
value={email} value={email}
setValue={setEmail} setValue={setEmail}
isBlur
containerStyle={{ marginBottom: 20 }} containerStyle={{ marginBottom: 20 }}
textInputProps={{ textInputProps={{
keyboardType: "email-address", keyboardType: "email-address",
+117 -63
View File
@@ -5,7 +5,7 @@ import React, {
useRef, useRef,
useState, useState,
} from "react"; } 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 { Image as ExpoImage } from "expo-image";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { background, cardsImg, icons } from "../../assets"; import { background, cardsImg, icons } from "../../assets";
@@ -14,7 +14,6 @@ import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu"; import MoreMenu from "../../components/MoreMenu";
import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown"; import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
import ShareBtn from "../../components/ShareBtn/ShareBtn";
import { projectsRef, usersRef } from "../../config/firebase"; import { projectsRef, usersRef } from "../../config/firebase";
import { isWeb } from "../../hooks/useLayoutType.js"; import { isWeb } from "../../hooks/useLayoutType.js";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
@@ -22,7 +21,7 @@ import LandingPage from "../LandingPage";
import { navigate } from "../../navigation/NavigationService"; import { navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation/Routes"; import { Routes } from "../../navigation/Routes";
import { useUser } from "../../providers/UserDataProvider"; import { useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles"; import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { import {
getCreationStageStates, getCreationStageStates,
@@ -43,6 +42,8 @@ const isProjectEmpty = (project) => {
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520; const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520;
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360; 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 = [ const STAGE_CARD_CONTENT = [
{ {
@@ -59,7 +60,7 @@ const STAGE_CARD_CONTENT = [
key: "beatmaker", key: "beatmaker",
step: "ÉTAPE 2", step: "ÉTAPE 2",
description: 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, image: cardsImg.studio,
imagePosition: "right", imagePosition: "right",
textAlign: "right", textAlign: "right",
@@ -79,7 +80,7 @@ const STAGE_CARD_CONTENT = [
key: "publisher", key: "publisher",
step: "ÉTAPE 4", step: "ÉTAPE 4",
description: 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, image: cardsImg.production,
imagePosition: "right", imagePosition: "right",
textAlign: "right", textAlign: "right",
@@ -292,14 +293,14 @@ const Home = ({ navigation, route }) => {
: null; : null;
Alert( Alert(
"Céline", "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", text: "Retour à l'accueil",
style: "cancel", style: "cancel",
}, },
{ {
text: "Continuer avec Malik", text: "Continuer vers le Studio",
onPress: () => { onPress: () => {
if (!currentProject?.id) { if (!currentProject?.id) {
return; return;
@@ -448,6 +449,68 @@ const Home = ({ navigation, route }) => {
navigate(Routes.Payments); 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 ? ( return adventureStarted ? (
<View style={styles.root}> <View style={styles.root}>
<Page <Page
@@ -466,54 +529,17 @@ const Home = ({ navigation, route }) => {
contentFit="cover" contentFit="cover"
style={styles.centerImage} style={styles.centerImage}
/> />
{isWeb ? (
<View style={styles.topBar}> <View style={styles.contentOverlay}>{journeyContent}</View>
<ProjectDropDown ) : (
style={styles.projectDropDown} <ScrollView
projects={projects} style={[styles.mobileScrollView, styles.contentOverlay]}
selectedProject={currentProject} contentContainerStyle={styles.mobileScrollContent}
allowEmptySelection showsVerticalScrollIndicator={false}
onSelectProject={handleSelectProject} >
onModifyProject={handleModifyProject} {journeyContent}
onCreateProject={handleStartNew} </ScrollView>
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}
/>
</View> </View>
</Page> </Page>
</View> </View>
@@ -534,7 +560,7 @@ const Home = ({ navigation, route }) => {
}} }}
> >
<GradientButton <GradientButton
url={videos.landing} url={videoUrl}
title="Commencer l'aventure MusicLand" title="Commencer l'aventure MusicLand"
onPress={handleStartVisit} onPress={handleStartVisit}
/> />
@@ -572,20 +598,27 @@ const styles = StyleSheet.create({
alignSelf: "stretch", alignSelf: "stretch",
alignItems: "stretch", alignItems: "stretch",
justifyContent: "flex-start", justifyContent: "flex-start",
position: "relative",
}, },
centerImage: { centerImage: {
width: HOME_BACKGROUND_WIDTH + 120, width: HOME_BACKGROUND_STYLE_WIDTH,
height: HOME_BACKGROUND_HEIGHT + 80, height: HOME_BACKGROUND_STYLE_HEIGHT,
borderRadius: 22, borderRadius: 22,
overflow: "hidden", overflow: "hidden",
position: "absolute", position: "absolute",
top: "45%", top: isWeb ? "45%" : "50%",
left: "50%", left: "50%",
transform: [ transform: [
{ translateX: -HOME_BACKGROUND_WIDTH / 2 }, { translateX: -HOME_BACKGROUND_STYLE_WIDTH / 2 },
{ translateY: -HOME_BACKGROUND_HEIGHT / 2 }, { translateY: -HOME_BACKGROUND_STYLE_HEIGHT / 2 },
], ],
pointerEvents: "none", pointerEvents: "none",
zIndex: 0,
},
contentOverlay: {
position: "relative",
width: "100%",
flexGrow: 1,
}, },
topBar: { topBar: {
width: "100%", width: "100%",
@@ -594,8 +627,8 @@ const styles = StyleSheet.create({
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
gap: 12, gap: 12,
marginTop: 24, marginTop: isWeb ? 24 : 0,
marginBottom: 8, marginBottom: isWeb ? 8 : 16,
zIndex: 10, zIndex: 10,
}, },
projectDropDown: { projectDropDown: {
@@ -603,6 +636,15 @@ const styles = StyleSheet.create({
maxWidth: 420, maxWidth: 420,
flexGrow: 1, flexGrow: 1,
}, },
mobileScrollView: {
flex: 1,
width: "100%",
},
mobileScrollContent: {
paddingHorizontal: gutters,
paddingTop: 24,
paddingBottom: gutters * 6,
},
subtitle: { subtitle: {
width: "100%", width: "100%",
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
@@ -621,4 +663,16 @@ const styles = StyleSheet.create({
justifyContent: "space-between", justifyContent: "space-between",
rowGap: 20, 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", textAlign = "left",
isLocked, isLocked,
onPress, onPress,
containerStyle = null,
variant = "web",
}) => { }) => {
const isImageOnLeft = imagePosition !== "right"; const isMobileVariant = variant === "mobile";
const isTextRight = textAlign === "right"; const isImageOnLeft = !isMobileVariant && imagePosition !== "right";
const isTextRight = !isMobileVariant && textAlign === "right";
const imageBlock = ( const basePressableStyle =
<View variant === "web"
style={[ ? { width: "48%" }
{ : {
flex: 1, width: "100%",
alignItems: "center", borderRadius: 24,
justifyContent: "center",
overflow: "hidden", 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, borderTopLeftRadius: 20,
borderBottomLeftRadius: 20, borderBottomLeftRadius: 20,
@@ -40,22 +60,26 @@ const StageCard = ({
borderBottomRightRadius: 20, borderBottomRightRadius: 20,
borderTopLeftRadius: 0, borderTopLeftRadius: 0,
borderBottomLeftRadius: 0, borderBottomLeftRadius: 0,
}, }),
]} };
>
const imageBlock = (
<View style={[imageContainerBaseStyle, imageContainerVariantStyle]}>
<ExpoImage <ExpoImage
source={image} source={image}
contentFit="contain" contentFit="contain"
style={[ style={[
{ {
width: "100%", width: "100%",
height: "100%", height: isMobileVariant ? 190 : "100%",
minHeight: 210, minHeight: isMobileVariant ? 190 : 210,
borderRadius: 0, borderRadius: 0,
}, },
isImageOnLeft isMobileVariant
? { alignSelf: "flex-end" } ? { alignSelf: "center" }
: { alignSelf: "flex-start" }, : isImageOnLeft
? { alignSelf: "flex-end" }
: { alignSelf: "flex-start" },
]} ]}
/> />
</View> </View>
@@ -70,9 +94,11 @@ const StageCard = ({
alignItems: "center", alignItems: "center",
marginBottom: 8, marginBottom: 8,
}, },
isTextRight isMobileVariant
? { justifyContent: "flex-end" } ? { justifyContent: "center" }
: { justifyContent: "flex-start" }, : isTextRight
? { justifyContent: "flex-end" }
: { justifyContent: "flex-start" },
]} ]}
> >
<Text <Text
@@ -83,7 +109,11 @@ const StageCard = ({
color: Palette.white, color: Palette.white,
flexShrink: 1, flexShrink: 1,
}, },
isTextRight ? { textAlign: "right" } : { textAlign: "left" }, isMobileVariant
? { textAlign: "center" }
: isTextRight
? { textAlign: "right" }
: { textAlign: "left" },
]} ]}
numberOfLines={1} numberOfLines={1}
> >
@@ -92,21 +122,29 @@ const StageCard = ({
</View> </View>
); );
const textBlock = ( const textBlockBaseStyle = {
<BlurView flexGrow: 1,
tint="dark" flexShrink: 1,
intensity={30} paddingVertical: 20,
style={[ paddingHorizontal: 20,
{ justifyContent: "center",
flexGrow: 1, alignSelf: "center",
flexShrink: 1, };
maxWidth: 380,
paddingVertical: 20, const textBlockVariantStyle = isMobileVariant
paddingHorizontal: 20, ? {
justifyContent: "center", width: "100%",
alignSelf: "center", borderBottomLeftRadius: 24,
}, borderBottomRightRadius: 24,
isImageOnLeft alignItems: "center",
paddingVertical: 20,
paddingHorizontal: 20,
marginTop: 0,
gap: 6,
}
: {
maxWidth: 380,
...(isImageOnLeft
? { ? {
borderTopRightRadius: 20, borderTopRightRadius: 20,
borderBottomRightRadius: 20, borderBottomRightRadius: 20,
@@ -114,9 +152,17 @@ const StageCard = ({
: { : {
borderTopLeftRadius: 20, borderTopLeftRadius: 20,
borderBottomLeftRadius: 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 ? ( {isLocked ? (
<View <View
@@ -132,7 +178,11 @@ const StageCard = ({
justifyContent: "center", justifyContent: "center",
zIndex: 2, zIndex: 2,
}, },
isTextRight ? { left: 10 } : { right: 10 }, isMobileVariant
? { right: 10 }
: isTextRight
? { left: 10 }
: { right: 10 },
]} ]}
> >
<FontAwesome name="lock" size={18} color={Palette.primary} /> <FontAwesome name="lock" size={18} color={Palette.primary} />
@@ -141,7 +191,11 @@ const StageCard = ({
{header} {header}
<Text <Text
style={[ style={[
isTextRight ? { textAlign: "right" } : { textAlign: "left" }, isMobileVariant
? { textAlign: "center" }
: isTextRight
? { textAlign: "right" }
: { textAlign: "left" },
{ {
fontFamily: FONT_FAMILY.InterRegular, fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13, fontSize: 13,
@@ -155,7 +209,12 @@ const StageCard = ({
</BlurView> </BlurView>
); );
const content = isImageOnLeft ? ( const content = isMobileVariant ? (
<>
{imageBlock}
{textBlock}
</>
) : isImageOnLeft ? (
<> <>
{imageBlock} {imageBlock}
{textBlock} {textBlock}
@@ -172,15 +231,14 @@ const StageCard = ({
onPress={onPress} onPress={onPress}
disabled={isLocked} disabled={isLocked}
style={({ pressed }) => [ style={({ pressed }) => [
{ basePressableStyle,
width: "48%", containerStyle,
},
pressed && !isLocked && { opacity: 0.85 }, pressed && !isLocked && { opacity: 0.85 },
]} ]}
> >
<View <View
style={{ style={{
flexDirection: "row", flexDirection: isMobileVariant ? "column" : "row",
}} }}
> >
{content} {content}
+23 -21
View File
@@ -159,7 +159,7 @@ const MusicDetails = ({ route }) => {
title, title,
artist, artist,
}), }),
[projectId, title, artist] [projectId, title, artist],
); );
const handleShare = useCallback(() => { const handleShare = useCallback(() => {
@@ -488,7 +488,13 @@ const MusicDetails = ({ route }) => {
console.log("MusicDetails seek error", e?.message); console.log("MusicDetails seek error", e?.message);
} }
}, },
[trackDescriptor, sliderDurationMs, isCurrentTrack, ensureLoaded, seekTrackTo] [
trackDescriptor,
sliderDurationMs,
isCurrentTrack,
ensureLoaded,
seekTrackTo,
],
); );
const handleToggleLoop = useCallback(async () => { const handleToggleLoop = useCallback(async () => {
@@ -582,13 +588,7 @@ const MusicDetails = ({ route }) => {
wasPlayingBeforeSeek.current = false; wasPlayingBeforeSeek.current = false;
lastSeekTargetMs.current = null; lastSeekTargetMs.current = null;
} }
}, [ }, [ensureLoaded, isCurrentTrack, positionMs, resumeTrack, seekTrackTo]);
ensureLoaded,
isCurrentTrack,
positionMs,
resumeTrack,
seekTrackTo,
]);
const handleSeekBySeconds = useCallback( const handleSeekBySeconds = useCallback(
async (deltaSeconds) => { async (deltaSeconds) => {
@@ -605,7 +605,7 @@ const MusicDetails = ({ route }) => {
console.log("MusicDetails seekBy error", e?.message); console.log("MusicDetails seekBy error", e?.message);
} }
}, },
[trackDescriptor, positionMs, isCurrentTrack, ensureLoaded, seekTrackBy] [trackDescriptor, positionMs, isCurrentTrack, ensureLoaded, seekTrackBy],
); );
const handleLyricsSeek = useCallback( const handleLyricsSeek = useCallback(
@@ -638,7 +638,7 @@ const MusicDetails = ({ route }) => {
seekTrackTo, seekTrackTo,
isTrackPlaying, isTrackPlaying,
resumeTrack, resumeTrack,
] ],
); );
const description = useMemo(() => { const description = useMemo(() => {
// Build a readable text from lyrics with section labels // Build a readable text from lyrics with section labels
@@ -676,12 +676,12 @@ const MusicDetails = ({ route }) => {
// Current time in seconds for highlighting // Current time in seconds for highlighting
const currentTimeS = useMemo( const currentTimeS = useMemo(
() => Math.max(0, (positionMs || 0) / 1000), () => Math.max(0, (positionMs || 0) / 1000),
[positionMs] [positionMs],
); );
// Preview lead: show words 0.5s earlier // Preview lead: show words 0.5s earlier
const visibleTimeS = useMemo( const visibleTimeS = useMemo(
() => Math.max(0, currentTimeS + 0.5), () => Math.max(0, currentTimeS + 0.5),
[currentTimeS] [currentTimeS],
); );
// Helpers to group aligned words into sections and timed lines // Helpers to group aligned words into sections and timed lines
const SECTION_TAG_REGEX = /^\s*\[([^\]]+)\]\s*$/i; const SECTION_TAG_REGEX = /^\s*\[([^\]]+)\]\s*$/i;
@@ -844,11 +844,11 @@ const MusicDetails = ({ route }) => {
const startS = lines.reduce( const startS = lines.reduce(
(acc, line) => (acc, line) =>
Math.min(acc, Number.isFinite(line.startS) ? line.startS : acc), Math.min(acc, Number.isFinite(line.startS) ? line.startS : acc),
Number.POSITIVE_INFINITY Number.POSITIVE_INFINITY,
); );
const endS = lines.reduce( const endS = lines.reduce(
(acc, line) => Math.max(acc, Number(line.endS || 0)), (acc, line) => Math.max(acc, Number(line.endS || 0)),
0 0,
); );
grouped.push({ grouped.push({
key: current.key, key: current.key,
@@ -910,7 +910,7 @@ const MusicDetails = ({ route }) => {
const flatLines = useMemo( const flatLines = useMemo(
() => sections.flatMap((section) => section.lines), () => sections.flatMap((section) => section.lines),
[sections] [sections],
); );
const currentLineIdx = useMemo(() => { const currentLineIdx = useMemo(() => {
if (!flatLines || flatLines.length === 0) return -1; if (!flatLines || flatLines.length === 0) return -1;
@@ -941,14 +941,14 @@ const MusicDetails = ({ route }) => {
lineRelativeYRef.current[lineIdx] = { sectionKey, relativeY }; lineRelativeYRef.current[lineIdx] = { sectionKey, relativeY };
updateLineAbsoluteOffset(lineIdx); updateLineAbsoluteOffset(lineIdx);
}, },
[updateLineAbsoluteOffset] [updateLineAbsoluteOffset],
); );
const registerSectionOffset = useCallback( const registerSectionOffset = useCallback(
(sectionKey, y, lines = []) => { (sectionKey, y, lines = []) => {
sectionYRef.current[sectionKey] = y; sectionYRef.current[sectionKey] = y;
lines.forEach((line) => updateLineAbsoluteOffset(line.globalIdx)); lines.forEach((line) => updateLineAbsoluteOffset(line.globalIdx));
}, },
[updateLineAbsoluteOffset] [updateLineAbsoluteOffset],
); );
useEffect(() => { useEffect(() => {
const y = lineYRef.current?.[currentLineIdx]; const y = lineYRef.current?.[currentLineIdx];
@@ -966,7 +966,9 @@ const MusicDetails = ({ route }) => {
topStickyContent={renderWebBackButton} topStickyContent={renderWebBackButton}
title={action === "userProfile" ? "Mon profil" : "Détail musique"} title={action === "userProfile" ? "Mon profil" : "Détail musique"}
backgroundImg={ backgroundImg={
action === "userProfile" ? background.profileBG : background.libraryBG2 action === "userProfile"
? background.profileBG
: background.libraryBG2Web
} }
shareBtn={ shareBtn={
sharePayload sharePayload
@@ -1179,7 +1181,7 @@ const MusicDetails = ({ route }) => {
registerSectionOffset( registerSectionOffset(
section.key, section.key,
e.nativeEvent?.layout?.y, e.nativeEvent?.layout?.y,
section.lines section.lines,
) )
} }
style={[ style={[
@@ -1198,7 +1200,7 @@ const MusicDetails = ({ route }) => {
registerLineRelativeOffset( registerLineRelativeOffset(
line.globalIdx, line.globalIdx,
section.key, section.key,
e.nativeEvent?.layout?.y ?? 0 e.nativeEvent?.layout?.y ?? 0,
); );
}} }}
style={{ style={{
+5 -5
View File
@@ -2,7 +2,7 @@ import FontAwesome from "@expo/vector-icons/FontAwesome";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native"; 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 Page from "../layouts/Page";
import { navigate } from "../navigation/NavigationService"; import { navigate } from "../navigation/NavigationService";
import { useUserData } from "../providers/UserDataProvider"; import { useUserData } from "../providers/UserDataProvider";
@@ -32,15 +32,15 @@ const CREATE_DATA = [
stageKey: "director", stageKey: "director",
img: ai.john, img: ai.john,
bg: background.playbackBG, bg: background.playbackBG,
label: "John", label: "Theo",
desc: "Come back when your\naudio is ready!", desc: "Theo t'accompagne pour créer ton playback.",
type: "Director", type: "Director",
}, },
{ {
stageKey: "publisher", stageKey: "publisher",
img: ai.bena, img: cardsImg.production,
bg: background.productionBG2, bg: background.productionBG2,
label: "Bena", label: "Publication",
desc: "Publions ta vidéo sur YouTube !", desc: "Publions ta vidéo sur YouTube !",
type: "Producteur", type: "Producteur",
}, },
+86 -125
View File
@@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import { import {
ActivityIndicator, ActivityIndicator,
Linking,
Pressable, Pressable,
StyleSheet, StyleSheet,
Text, Text,
@@ -17,13 +16,7 @@ import { background, subBadges } from "../assets";
import { Palette, gutters } from "../styles"; import { Palette, gutters } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts"; import { FONT_FAMILY } from "../styles/Fonts";
import { isWeb } from "../hooks/useLayoutType"; import { isWeb } from "../hooks/useLayoutType";
import { getFunctionsClient } from "../config/firebase"; import { useStripe } from "../providers/StripeProvider";
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;
const formatCurrency = (amount, currency = "eur") => { const formatCurrency = (amount, currency = "eur") => {
if (typeof amount !== "number") { if (typeof amount !== "number") {
@@ -298,120 +291,114 @@ export default function Payments() {
initialPack && initialPack !== "packs" ? initialPack : null; initialPack && initialPack !== "packs" ? initialPack : null;
const backgroundImage = background.bgTrans; const backgroundImage = background.bgTrans;
const [plans, setPlans] = React.useState({ monthly: [], annual: [] }); const {
subscriptions,
isCatalogLoading,
catalogError,
createSubscriptionCheckout,
} = useStripe();
const [selectedPriceIds, setSelectedPriceIds] = React.useState({ const [selectedPriceIds, setSelectedPriceIds] = React.useState({
monthly: null, monthly: null,
annual: null, annual: null,
}); });
const [billingPeriod, setBillingPeriod] = React.useState("monthly"); const [billingPeriod, setBillingPeriod] = React.useState("monthly");
const [isLoadingPlans, setIsLoadingPlans] = React.useState(true);
const [processingPriceId, setProcessingPriceId] = React.useState(null); const [processingPriceId, setProcessingPriceId] = React.useState(null);
const [errorMessage, setErrorMessage] = React.useState(null); const [errorMessage, setErrorMessage] = React.useState(null);
const initialPackHandledRef = React.useRef(false); 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(() => { React.useEffect(() => {
initialPackHandledRef.current = false; initialPackHandledRef.current = false;
}, [initialSubscriptionPack]); }, [initialSubscriptionPack]);
const fetchPlans = React.useCallback(async () => { React.useEffect(() => {
setIsLoadingPlans(true); const shouldApplyPack =
setErrorMessage(null); Boolean(initialSubscriptionPack) && !initialPackHandledRef.current;
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"),
};
setPlans(normalizedPlans); let matchedPeriod = null;
let matchedPriceId = null;
let matchedPeriod = null; if (shouldApplyPack && normalizedPlans) {
let matchedPriceId = null; const desired = initialSubscriptionPack.trim().toLowerCase();
const shouldApplyPack = for (const [periodKey, mapping] of Object.entries(
Boolean(initialSubscriptionPack) && !initialPackHandledRef.current; PACK_PRICE_ID_BY_PERIOD,
)) {
if (shouldApplyPack) { const candidateId = mapping[desired];
const desired = initialSubscriptionPack.trim().toLowerCase(); if (!candidateId) continue;
for (const [periodKey, mapping] of Object.entries( const exists = (normalizedPlans[periodKey] || []).some(
PACK_PRICE_ID_BY_PERIOD, (plan) => plan?.priceId === candidateId,
)) { );
const candidateId = mapping[desired]; if (exists) {
if (!candidateId) continue; matchedPeriod = periodKey;
const exists = (normalizedPlans[periodKey] || []).some( matchedPriceId = candidateId;
(plan) => plan?.priceId === candidateId, break;
);
if (exists) {
matchedPeriod = periodKey;
matchedPriceId = candidateId;
break;
}
} }
} }
}
setSelectedPriceIds((current) => { setSelectedPriceIds((current) => {
const next = { ...current }; const next = { ...current };
PLAN_SEGMENTS.forEach(({ key }) => { PLAN_SEGMENTS.forEach(({ key }) => {
const periodPlans = normalizedPlans[key] || []; const periodPlans = normalizedPlans[key] || [];
if (!periodPlans.length) { if (!periodPlans.length) {
next[key] = null; next[key] = null;
return; 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;
} }
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) { if (matchedPeriod && matchedPriceId) {
setBillingPeriod(matchedPeriod); next[matchedPeriod] = matchedPriceId;
initialPackHandledRef.current = true;
} else if (shouldApplyPack) {
initialPackHandledRef.current = true;
} }
} catch (error) {
console.error("[Payments] fetchPlans error", error); return next;
setErrorMessage( });
error?.message || "Impossible de récupérer les abonnements Stripe.",
); if (matchedPeriod && matchedPriceId) {
} finally { setBillingPeriod(matchedPeriod);
setIsLoadingPlans(false); initialPackHandledRef.current = true;
} else if (shouldApplyPack && !isCatalogLoading) {
initialPackHandledRef.current = true;
} }
}, [initialSubscriptionPack]); }, [
normalizedPlans,
initialSubscriptionPack,
isCatalogLoading,
]);
React.useEffect(() => { const currentPlans = normalizedPlans[billingPeriod] || [];
fetchPlans();
}, [fetchPlans]);
const currentPlans = plans[billingPeriod] || [];
const selectedPriceId = selectedPriceIds[billingPeriod]; const selectedPriceId = selectedPriceIds[billingPeriod];
const handleSelect = React.useCallback( const handleSelect = React.useCallback(
@@ -433,33 +420,7 @@ export default function Payments() {
setProcessingPriceId(selectedPriceId); setProcessingPriceId(selectedPriceId);
setErrorMessage(null); setErrorMessage(null);
try { try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable( await createSubscriptionCheckout(selectedPriceId);
"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);
}
} catch (error) { } catch (error) {
console.error("[Payments] checkout error", error); console.error("[Payments] checkout error", error);
setErrorMessage( setErrorMessage(
@@ -469,7 +430,7 @@ export default function Payments() {
} finally { } finally {
setProcessingPriceId(null); setProcessingPriceId(null);
} }
}, [isWeb, selectedPriceId]); }, [selectedPriceId, createSubscriptionCheckout]);
React.useEffect(() => { React.useEffect(() => {
setSelectedPriceIds((current) => { setSelectedPriceIds((current) => {
@@ -523,8 +484,8 @@ export default function Payments() {
</Text> </Text>
</View> </View>
{errorMessage ? ( {combinedErrorMessage ? (
<Text style={styles.errorText}>{errorMessage}</Text> <Text style={styles.errorText}>{combinedErrorMessage}</Text>
) : null} ) : null}
{isLoadingPlans && !currentPlans.length ? ( {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 React from "react";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { ai, background } from "../../assets"; import { background } from "../../assets";
import { gutters } from "../../styles"; import { gutters } from "../../styles";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
@@ -12,7 +12,6 @@ import { Routes } from "../../navigation";
const Production = () => { const Production = () => {
return ( return (
<Page headerType="NONE" backgroundImg={background.productionBG}> <Page headerType="NONE" backgroundImg={background.productionBG}>
<Image source={ai.bena} style={styles.img} resizeMode="contain" />
<MusicLandHeader onPressBack={goBack} progress={25} /> <MusicLandHeader onPressBack={goBack} progress={25} />
<View <View
style={{ style={{
@@ -43,13 +42,3 @@ const Production = () => {
}; };
export default 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 { useUserData } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles"; import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { formatDate, toDate } from "../../utils/dateFormatting";
const FUNCTIONS_REGION = "europe-west1"; 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) => { const capitalize = (value) => {
if (typeof value !== "string" || !value) { if (typeof value !== "string" || !value) {
return null; return null;
+4 -10
View File
@@ -1,9 +1,8 @@
import { useRoute } from "@react-navigation/native"; import { useRoute } from "@react-navigation/native";
import { Image } from "expo-image";
import React, { useCallback, useMemo, useState } from "react"; import React, { useCallback, useMemo, useState } from "react";
import { Linking, Text, View } from "react-native"; import { Linking, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; 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 BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
@@ -58,10 +57,10 @@ const PublishYoutube = () => {
: "Publier sur YouTube"; : "Publier sur YouTube";
const statusMessage = useMemo(() => { const statusMessage = useMemo(() => {
if (!playbackReady) { 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) { 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") { if (youtubeError || youtubeStatus === "FAILED") {
return "La dernière tentative de publication a échoué. Tu peux réessayer ci-dessous."; return "La dernière tentative de publication a échoué. Tu peux réessayer ci-dessous.";
@@ -69,7 +68,7 @@ const PublishYoutube = () => {
if (hasYoutubePublication) { if (hasYoutubePublication) {
return "Ta vidéo est publiée sur la chaîne YouTube MusicLand. Tu peux la partager dès maintenant."; 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, hasYoutubePublication,
isPublishing, isPublishing,
@@ -175,11 +174,6 @@ const PublishYoutube = () => {
}} }}
> >
<View style={{ alignItems: "center", gap: 18 }}> <View style={{ alignItems: "center", gap: 18 }}>
<Image
source={ai.bena}
style={{ width: 220, height: 320 }}
contentFit="contain"
/>
<Text <Text
style={{ style={{
fontSize: 22, fontSize: 22,
+2 -13
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { Image, StyleSheet, View } from "react-native"; import { View } from "react-native";
import { ai, background } from "../../assets"; import { background } from "../../assets";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo"; import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
@@ -21,7 +21,6 @@ const Compose = () => {
}, []); }, []);
return ( return (
<Page backgroundImg={background.studioBG2} headerType="NONE"> <Page backgroundImg={background.studioBG2} headerType="NONE">
<Image source={ai.malik} style={styles.img} resizeMode="contain" />
<MusicLandHeader <MusicLandHeader
onPressBack={goBack} onPressBack={goBack}
progress={9} progress={9}
@@ -53,13 +52,3 @@ const Compose = () => {
}; };
export default 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 React, { useEffect, useState } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native"; import { Image, Platform, Pressable, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; 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 AppAlert from "../../components/Alert";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import ProgressBar from "../../components/ProgressBar"; import ProgressBar from "../../components/ProgressBar";
@@ -194,22 +194,6 @@ const CreatingSong = ({ active, config }) => {
height: "50%", 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 <View
style={{ style={{
flex: 1, flex: 1,
+6 -2
View File
@@ -338,7 +338,7 @@ const SongReady = () => {
<View style={{ flex: 1, marginTop: 16 }}> <View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader <CreateLyricsHeader
title="Ta chanson est prête !" title="Ta chanson est prête !"
subTitle="Quen penses-tu ?" subTitle="Choisis ton morceau"
containerStyle={{ containerStyle={{
marginBottom: responsiveHeight(2), marginBottom: responsiveHeight(2),
}} }}
@@ -432,7 +432,11 @@ const SongReady = () => {
try { try {
const player = idx === 0 ? player0 : player1; const player = idx === 0 ? player0 : player1;
if (player && wasPlayingBeforeSeek.current[idx]) { if (player && wasPlayingBeforeSeek.current[idx]) {
await player.play?.(); if (player.resume) {
await player.resume?.();
} else {
await player.play?.();
}
setIsPlaying((p) => ({ ...p, [idx]: true })); setIsPlaying((p) => ({ ...p, [idx]: true }));
} }
wasPlayingBeforeSeek.current[idx] = false; wasPlayingBeforeSeek.current[idx] = false;
+2 -2
View File
@@ -286,7 +286,7 @@ const Lyrics = ({ navigation }) => {
await setIsLoading(false); await setIsLoading(false);
alert( alert(
"Céline", "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", text: "Retour à l'accueil",
@@ -300,7 +300,7 @@ const Lyrics = ({ navigation }) => {
}), }),
}, },
{ {
text: "Continuer avec Malik", text: "Continuer vers le Studio",
onPress: () => { onPress: () => {
const targetRoute = beatmakerStage?.route || Routes.Compose; const targetRoute = beatmakerStage?.route || Routes.Compose;
navigate(targetRoute, beatmakerStage?.params); navigate(targetRoute, beatmakerStage?.params);
+1 -1
View File
@@ -8,7 +8,7 @@ const SongTo = ({ audience, setAudience }) => {
<View style={{ flex: 1, gap: 10, marginTop: 16 }}> <View style={{ flex: 1, gap: 10, marginTop: 16 }}>
<CreateLyricsHeader title="À qui sadresse ta chanson ?" /> <CreateLyricsHeader title="À qui sadresse ta chanson ?" />
<CustomInput <CustomInput
placeholder="Ecrire mon contexte" placeholder="Écris à qui s'adresse ta chanson"
height={283} height={283}
value={audience} value={audience}
setValue={setAudience} 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." subTitle="Dis-nous en un peu plus pour quon puisse mieux taider."
/> />
<CustomInput <CustomInput
placeholder="Ecrire mon contexte" placeholder="Écris à qui s'adresse ta chanson"
height={283} height={283}
value={context} value={context}
setValue={setContext} setValue={setContext}
+1 -13
View File
@@ -1,8 +1,7 @@
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import React, { useCallback, useEffect, useMemo, useState } from "react"; 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 useMinuit from "react-native-minuit/src/hooks/useMinuit";
import { ai } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton"; import BorderGradientButton from "../../components/BorderGradientButton";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo"; import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
@@ -277,17 +276,6 @@ export const ChooseCoverType = () => {
backgroundColor={"#2A2E33"} backgroundColor={"#2A2E33"}
headerType="NONE" headerType="NONE"
> >
<Image
source={ai.bena}
style={{
width: "100%",
height: "70%",
position: "absolute",
bottom: -40,
right: -30,
}}
resizeMode="contain"
/>
<MusicLandHeader onPressBack={goBack} progress={9} /> <MusicLandHeader onPressBack={goBack} progress={9} />
<View <View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }} style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
+3 -3
View File
@@ -252,8 +252,8 @@ const PouchReady = () => {
const playbackStage = getStageAction("director", projectForStage); const playbackStage = getStageAction("director", projectForStage);
await setLoading(false); await setLoading(false);
alert( alert(
"Malik", "Playback prêt",
"Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec John pour produire ton playback.", "Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec Theo pour produire ton playback.",
[ [
{ {
text: "Retour à l'accueil", text: "Retour à l'accueil",
@@ -261,7 +261,7 @@ const PouchReady = () => {
onPress: () => navigate(Routes.Home), onPress: () => navigate(Routes.Home),
}, },
{ {
text: "Continuer avec John", text: "Continuer avec Theo",
onPress: () => { onPress: () => {
const targetRoute = playbackStage?.route || Routes.Playback; const targetRoute = playbackStage?.route || Routes.Playback;
const params = playbackStage?.params || { const params = playbackStage?.params || {
+3 -3
View File
@@ -117,8 +117,8 @@ const ValidateCover = () => {
const playbackStage = getStageAction("director", projectForStage); const playbackStage = getStageAction("director", projectForStage);
await setIsLoading(false); await setIsLoading(false);
alert( alert(
"Malik", "Playback prêt",
"Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec John pour produire ton playback.", "Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec Theo pour produire ton playback.",
[ [
{ {
text: "Retour à l'accueil", text: "Retour à l'accueil",
@@ -126,7 +126,7 @@ const ValidateCover = () => {
onPress: () => navigate(Routes.Home), onPress: () => navigate(Routes.Home),
}, },
{ {
text: "Continuer avec John", text: "Continuer avec Theo",
onPress: () => { onPress: () => {
const targetRoute = playbackStage?.route || Routes.Playback; const targetRoute = playbackStage?.route || Routes.Playback;
const params = const params =
+44
View File
@@ -0,0 +1,44 @@
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 instanceof 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();
}
};
export { toDate, formatDate };