Merge branch 'main' of gitlab.com:agenceminuit/musicland
This commit is contained in:
+46
-12
@@ -31,16 +31,26 @@ import {
|
||||
getStageAction,
|
||||
} from "../../utils/projectStages";
|
||||
|
||||
const isProjectEmpty = (project) => {
|
||||
if (!project) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(
|
||||
typeof project?.title === "string" && project.title.trim().length > 0
|
||||
);
|
||||
};
|
||||
|
||||
const Home = ({ navigation, route }) => {
|
||||
const isFocused = useIsFocused();
|
||||
const {
|
||||
userProjects = [],
|
||||
resetSelectedProject,
|
||||
selectProject,
|
||||
selectedProject,
|
||||
selectedProjectId,
|
||||
currentUserData,
|
||||
currentUID,
|
||||
createNewProject,
|
||||
} = useUser();
|
||||
const { setTooltip } = useMinuit();
|
||||
|
||||
@@ -310,18 +320,42 @@ const Home = ({ navigation, route }) => {
|
||||
[]
|
||||
);
|
||||
|
||||
const handleStartNew = useCallback(() => {
|
||||
resetSelectedProject();
|
||||
setActiveStageIndex(0);
|
||||
if (songwriterAction?.route) {
|
||||
console.log(
|
||||
"navigating to",
|
||||
songwriterAction.route,
|
||||
songwriterAction.params
|
||||
);
|
||||
navigate(songwriterAction.route, songwriterAction.params);
|
||||
const handleStartNew = useCallback(async () => {
|
||||
const targetRoute = songwriterAction?.route || Routes.WritingLyrics;
|
||||
const targetParams = songwriterAction?.params;
|
||||
|
||||
try {
|
||||
const emptyProject =
|
||||
projects.find((project) => isProjectEmpty(project)) || null;
|
||||
|
||||
if (emptyProject?.id) {
|
||||
selectProject(emptyProject.id);
|
||||
setActiveStageIndex(findFirstUnlockedStageIndex(emptyProject));
|
||||
navigate(targetRoute, targetParams);
|
||||
return;
|
||||
}
|
||||
|
||||
const newProjectId = await createNewProject({ hasLyrics: false });
|
||||
if (!newProjectId) {
|
||||
return;
|
||||
}
|
||||
setActiveStageIndex(0);
|
||||
navigate(targetRoute, targetParams);
|
||||
} catch (error) {
|
||||
console.warn("Home: unable to start new project", error);
|
||||
setTooltip?.({
|
||||
type: "error",
|
||||
text: "Impossible de démarrer un nouveau projet",
|
||||
});
|
||||
}
|
||||
}, [resetSelectedProject, songwriterAction]);
|
||||
}, [
|
||||
createNewProject,
|
||||
projects,
|
||||
selectProject,
|
||||
setActiveStageIndex,
|
||||
setTooltip,
|
||||
songwriterAction,
|
||||
]);
|
||||
|
||||
const continueDisabled = !hasActiveProject || stageLocked || !stageRoute;
|
||||
const startDisabled = !songwriterAction?.route;
|
||||
|
||||
+56
-370
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
Linking,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
@@ -13,7 +12,7 @@ import { BlurView } from "expo-blur";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import GradientButton from "../components/GradientButton";
|
||||
import Page from "../layouts/Page";
|
||||
import { background, icons } from "../assets";
|
||||
import { background } from "../assets";
|
||||
import { Palette, gutters } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import { isWeb } from "../hooks/useLayoutType";
|
||||
@@ -133,50 +132,6 @@ function SubscriptionCard({ plan, selected, onSelect }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CoinPackCard({ pack, selected, onSelect }) {
|
||||
const handleSelect = React.useCallback(() => {
|
||||
if (typeof onSelect === "function" && pack?.productId) {
|
||||
onSelect(pack.productId);
|
||||
}
|
||||
}, [onSelect, pack?.productId]);
|
||||
|
||||
const formattedPrice = formatCurrency(pack?.unitAmount, pack?.currency);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={handleSelect}
|
||||
style={({ pressed }) => [
|
||||
styles.cardWrapper,
|
||||
selected && styles.cardWrapperSelected,
|
||||
pressed && styles.cardWrapperPressed,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected }}
|
||||
>
|
||||
<BlurView intensity={18} tint="dark" style={styles.cardBlur}>
|
||||
<View style={styles.coinCardBody}>
|
||||
<View style={styles.coinCardHeader}>
|
||||
<Image source={icons.coin} style={styles.coinIcon} />
|
||||
<Text style={styles.coinAmount}>{pack?.coinAmount} pièces</Text>
|
||||
</View>
|
||||
|
||||
{pack?.name ? (
|
||||
<Text style={styles.coinPackName}>{pack.name}</Text>
|
||||
) : null}
|
||||
|
||||
{pack?.description ? (
|
||||
<Text style={styles.coinPackDescription}>{pack.description}</Text>
|
||||
) : null}
|
||||
|
||||
{formattedPrice ? (
|
||||
<Text style={styles.coinPrice}>{formattedPrice}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const PRICE_PRIORITY_BY_PERIOD = {
|
||||
monthly: [
|
||||
"price_1SPgitCzf2o5bDRdbnhLFx6f", // Starter
|
||||
@@ -261,13 +216,6 @@ const PLAN_SEGMENTS = [
|
||||
{ key: "annual", label: "Annuel" },
|
||||
];
|
||||
|
||||
const COIN_PACK_MODE_KEY = "packs";
|
||||
|
||||
const MODE_SEGMENTS = [
|
||||
{ key: "subscriptions", label: "Abonnements" },
|
||||
{ key: COIN_PACK_MODE_KEY, label: "Packs de pièces" },
|
||||
];
|
||||
|
||||
const HERO_IMAGE_WIDTH = isWeb ? 1280 : 520;
|
||||
const HERO_IMAGE_HEIGHT = isWeb ? 760 : 360;
|
||||
|
||||
@@ -278,13 +226,10 @@ export default function Payments() {
|
||||
return typeof rawPack === "string" ? rawPack.toLowerCase() : null;
|
||||
}, [route?.params?.pack]);
|
||||
|
||||
const initialMode =
|
||||
initialPack === COIN_PACK_MODE_KEY ? COIN_PACK_MODE_KEY : "subscriptions";
|
||||
const initialSubscriptionPack =
|
||||
initialPack && initialPack !== COIN_PACK_MODE_KEY ? initialPack : null;
|
||||
initialPack && initialPack !== "packs" ? initialPack : null;
|
||||
|
||||
const backgroundImage = background.bgTrans;
|
||||
const [mode, setMode] = React.useState(initialMode);
|
||||
const [plans, setPlans] = React.useState({ monthly: [], annual: [] });
|
||||
const [selectedPriceIds, setSelectedPriceIds] = React.useState({
|
||||
monthly: null,
|
||||
@@ -295,21 +240,11 @@ export default function Payments() {
|
||||
const [processingPriceId, setProcessingPriceId] = React.useState(null);
|
||||
const [errorMessage, setErrorMessage] = React.useState(null);
|
||||
const initialPackHandledRef = React.useRef(false);
|
||||
const [coinPacks, setCoinPacks] = React.useState([]);
|
||||
const [selectedCoinPackId, setSelectedCoinPackId] = React.useState(null);
|
||||
const [isLoadingCoinPacks, setIsLoadingCoinPacks] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
initialPackHandledRef.current = false;
|
||||
}, [initialSubscriptionPack]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (initialPack === COIN_PACK_MODE_KEY) {
|
||||
setMode(COIN_PACK_MODE_KEY);
|
||||
}
|
||||
}, [initialPack]);
|
||||
|
||||
|
||||
const fetchPlans = React.useCallback(async () => {
|
||||
setIsLoadingPlans(true);
|
||||
setErrorMessage(null);
|
||||
@@ -404,55 +339,12 @@ export default function Payments() {
|
||||
}
|
||||
}, [initialSubscriptionPack]);
|
||||
|
||||
const fetchCoinPacks = React.useCallback(async () => {
|
||||
setIsLoadingCoinPacks(true);
|
||||
setErrorMessage(null);
|
||||
try {
|
||||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
|
||||
"subscription-listCoinPacks",
|
||||
);
|
||||
const { data } = await callable();
|
||||
const fetchedPacks = Array.isArray(data?.packs) ? data.packs : [];
|
||||
setCoinPacks(fetchedPacks);
|
||||
setSelectedCoinPackId((current) => {
|
||||
if (current) {
|
||||
const exists = fetchedPacks.some(
|
||||
(pack) => pack.productId === current,
|
||||
);
|
||||
if (exists) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
return fetchedPacks[0]?.productId || null;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Payments] fetchCoinPacks error", error);
|
||||
setErrorMessage(
|
||||
error?.message || "Impossible de récupérer les packs de pièces.",
|
||||
);
|
||||
} finally {
|
||||
setIsLoadingCoinPacks(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchPlans();
|
||||
}, [fetchPlans]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mode !== COIN_PACK_MODE_KEY) {
|
||||
return;
|
||||
}
|
||||
if (coinPacks.length === 0 && !isLoadingCoinPacks) {
|
||||
fetchCoinPacks();
|
||||
}
|
||||
}, [mode, coinPacks.length, isLoadingCoinPacks, fetchCoinPacks]);
|
||||
|
||||
const isCoinMode = mode === COIN_PACK_MODE_KEY;
|
||||
const currentPlans = isCoinMode ? [] : plans[billingPeriod] || [];
|
||||
const selectedPriceId = isCoinMode
|
||||
? null
|
||||
: selectedPriceIds[billingPeriod];
|
||||
const currentPlans = plans[billingPeriod] || [];
|
||||
const selectedPriceId = selectedPriceIds[billingPeriod];
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(priceId) => {
|
||||
@@ -465,59 +357,7 @@ export default function Payments() {
|
||||
[billingPeriod],
|
||||
);
|
||||
|
||||
const handleCoinPackSelect = React.useCallback((productId) => {
|
||||
setSelectedCoinPackId(productId);
|
||||
setProcessingPriceId(null);
|
||||
}, []);
|
||||
|
||||
const handleCheckout = React.useCallback(async () => {
|
||||
if (isCoinMode) {
|
||||
if (!selectedCoinPackId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessingPriceId(selectedCoinPackId);
|
||||
setErrorMessage(null);
|
||||
try {
|
||||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
|
||||
"subscription-createCoinPackCheckoutSession",
|
||||
);
|
||||
const { data } = await callable({
|
||||
productId: selectedCoinPackId,
|
||||
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) {
|
||||
console.error("[Payments] checkout error", error);
|
||||
setErrorMessage(
|
||||
error?.message ||
|
||||
"Une erreur est survenue lors de la création de la session Stripe.",
|
||||
);
|
||||
} finally {
|
||||
setProcessingPriceId(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPriceId) {
|
||||
return;
|
||||
}
|
||||
@@ -561,17 +401,9 @@ export default function Payments() {
|
||||
} finally {
|
||||
setProcessingPriceId(null);
|
||||
}
|
||||
}, [
|
||||
isCoinMode,
|
||||
isWeb,
|
||||
selectedCoinPackId,
|
||||
selectedPriceId,
|
||||
]);
|
||||
}, [isWeb, selectedPriceId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isCoinMode) {
|
||||
return;
|
||||
}
|
||||
setSelectedPriceIds((current) => {
|
||||
if (current[billingPeriod]) {
|
||||
return current;
|
||||
@@ -585,44 +417,24 @@ export default function Payments() {
|
||||
[billingPeriod]: fallback,
|
||||
};
|
||||
});
|
||||
}, [billingPeriod, currentPlans, isCoinMode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isCoinMode) {
|
||||
return;
|
||||
}
|
||||
setProcessingPriceId(null);
|
||||
}, [billingPeriod, isCoinMode]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mode === COIN_PACK_MODE_KEY) {
|
||||
if (!selectedCoinPackId && coinPacks.length) {
|
||||
setSelectedCoinPackId(coinPacks[0].productId);
|
||||
}
|
||||
}
|
||||
}, [mode, coinPacks, selectedCoinPackId]);
|
||||
}, [billingPeriod, currentPlans]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setProcessingPriceId(null);
|
||||
}, [mode]);
|
||||
}, [billingPeriod]);
|
||||
|
||||
const isProcessing = Boolean(processingPriceId);
|
||||
const isActionDisabled = isCoinMode
|
||||
? !selectedCoinPackId || isProcessing || isLoadingCoinPacks
|
||||
: !selectedPriceId || isProcessing || isLoadingPlans;
|
||||
const isActionDisabled =
|
||||
!selectedPriceId || isProcessing || isLoadingPlans;
|
||||
const actionButtonTitle = isProcessing
|
||||
? "Redirection..."
|
||||
: isCoinMode
|
||||
? "Acheter ce pack"
|
||||
: "Choisir cet abonnement";
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<Page
|
||||
headerType="NAVIGATE"
|
||||
title={
|
||||
mode === COIN_PACK_MODE_KEY ? "Packs de pièces" : "Abonnements"
|
||||
}
|
||||
title="Abonnements"
|
||||
scrollEnabled={false}
|
||||
width={isWeb ? 960 : undefined}
|
||||
containerStyle={styles.page}
|
||||
@@ -636,24 +448,46 @@ export default function Payments() {
|
||||
/>
|
||||
|
||||
<View style={styles.content}>
|
||||
<View style={styles.modeSegmentedControl}>
|
||||
{MODE_SEGMENTS.map(({ key, label }) => {
|
||||
const isActive = mode === key;
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>
|
||||
Choisissez l’abonnement qui vous correspond
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{errorMessage ? (
|
||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||
) : null}
|
||||
|
||||
{isLoadingPlans && !currentPlans.length ? (
|
||||
<View style={styles.loaderContainer}>
|
||||
<ActivityIndicator color={Palette.white} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{!isLoadingPlans && currentPlans.length === 0 ? (
|
||||
<Text style={styles.emptyState}>
|
||||
Aucun abonnement Stripe disponible pour le moment.
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<View style={styles.segmentedControl}>
|
||||
{PLAN_SEGMENTS.map(({ key, label }) => {
|
||||
const isActive = billingPeriod === key;
|
||||
return (
|
||||
<Pressable
|
||||
key={key}
|
||||
onPress={() => setMode(key)}
|
||||
onPress={() => setBillingPeriod(key)}
|
||||
style={[
|
||||
styles.modeSegmentButton,
|
||||
isActive && styles.modeSegmentButtonActive,
|
||||
styles.segmentButton,
|
||||
isActive && styles.segmentButtonActive,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: isActive }}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.modeSegmentLabel,
|
||||
isActive && styles.modeSegmentLabelActive,
|
||||
styles.segmentLabel,
|
||||
isActive && styles.segmentLabelActive,
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
@@ -663,109 +497,23 @@ export default function Payments() {
|
||||
})}
|
||||
</View>
|
||||
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>
|
||||
{isCoinMode
|
||||
? "Recharge tes pièces MusicLand"
|
||||
: "Choisissez l’abonnement qui vous correspond"}
|
||||
</Text>
|
||||
<View style={[styles.packs, isWeb && styles.packsWeb]}>
|
||||
{currentPlans.map((plan) => (
|
||||
<SubscriptionCard
|
||||
key={plan.priceId}
|
||||
plan={plan}
|
||||
selected={selectedPriceId === plan.priceId}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{errorMessage ? (
|
||||
<Text style={styles.errorText}>{errorMessage}</Text>
|
||||
{isLoadingPlans && currentPlans.length > 0 ? (
|
||||
<View style={styles.inlineLoader}>
|
||||
<ActivityIndicator color={Palette.white} size="small" />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{isCoinMode ? (
|
||||
<>
|
||||
{isLoadingCoinPacks && !coinPacks.length ? (
|
||||
<View style={styles.loaderContainer}>
|
||||
<ActivityIndicator color={Palette.white} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{!isLoadingCoinPacks && coinPacks.length === 0 ? (
|
||||
<Text style={styles.emptyState}>
|
||||
Aucun pack de pièces disponible pour le moment.
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<View style={[styles.packs, isWeb && styles.packsWeb]}>
|
||||
{coinPacks.map((pack) => (
|
||||
<CoinPackCard
|
||||
key={pack.productId}
|
||||
pack={pack}
|
||||
selected={selectedCoinPackId === pack.productId}
|
||||
onSelect={handleCoinPackSelect}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{isLoadingCoinPacks && coinPacks.length > 0 ? (
|
||||
<View style={styles.inlineLoader}>
|
||||
<ActivityIndicator color={Palette.white} size="small" />
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{isLoadingPlans && !currentPlans.length ? (
|
||||
<View style={styles.loaderContainer}>
|
||||
<ActivityIndicator color={Palette.white} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{!isLoadingPlans && currentPlans.length === 0 ? (
|
||||
<Text style={styles.emptyState}>
|
||||
Aucun abonnement Stripe disponible pour le moment.
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<View style={styles.segmentedControl}>
|
||||
{PLAN_SEGMENTS.map(({ key, label }) => {
|
||||
const isActive = billingPeriod === key;
|
||||
return (
|
||||
<Pressable
|
||||
key={key}
|
||||
onPress={() => setBillingPeriod(key)}
|
||||
style={[
|
||||
styles.segmentButton,
|
||||
isActive && styles.segmentButtonActive,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: isActive }}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.segmentLabel,
|
||||
isActive && styles.segmentLabelActive,
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
<View style={[styles.packs, isWeb && styles.packsWeb]}>
|
||||
{currentPlans.map((plan) => (
|
||||
<SubscriptionCard
|
||||
key={plan.priceId}
|
||||
plan={plan}
|
||||
selected={selectedPriceId === plan.priceId}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{isLoadingPlans && currentPlans.length > 0 ? (
|
||||
<View style={styles.inlineLoader}>
|
||||
<ActivityIndicator color={Palette.white} size="small" />
|
||||
</View>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
|
||||
<View style={styles.actions}>
|
||||
<GradientButton
|
||||
title={actionButtonTitle}
|
||||
@@ -776,9 +524,9 @@ export default function Payments() {
|
||||
</View>
|
||||
|
||||
<Text style={styles.disclaimer}>
|
||||
{isCoinMode
|
||||
? "Les pièces sont créditées immédiatement après le paiement. Pense à conserver ton reçu Stripe pour référence."
|
||||
: "Résiliable en un clic à tout moment. Les prix sont indiqués TTC. Contacte-nous pour des besoins spécifiques (facturation annuelle, volume, offres éducation)."}
|
||||
Résiliable en un clic à tout moment. Les prix sont indiqués TTC.
|
||||
Contacte-nous pour des besoins spécifiques (facturation annuelle,
|
||||
volume, offres éducation).
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@@ -878,30 +626,6 @@ const styles = StyleSheet.create({
|
||||
marginTop: 12,
|
||||
marginBottom: 8,
|
||||
},
|
||||
modeSegmentedControl: {
|
||||
flexDirection: "row",
|
||||
alignSelf: "center",
|
||||
padding: 4,
|
||||
borderRadius: 999,
|
||||
backgroundColor: "rgba(255, 255, 255, 0.12)",
|
||||
marginBottom: 16,
|
||||
},
|
||||
modeSegmentButton: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 18,
|
||||
borderRadius: 999,
|
||||
},
|
||||
modeSegmentButtonActive: {
|
||||
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
||||
},
|
||||
modeSegmentLabel: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 14,
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
},
|
||||
modeSegmentLabelActive: {
|
||||
color: Palette.white,
|
||||
},
|
||||
segmentButton: {
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 18,
|
||||
@@ -961,44 +685,6 @@ const styles = StyleSheet.create({
|
||||
cardContent: {
|
||||
gap: 16,
|
||||
},
|
||||
coinCardHeader: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 10,
|
||||
},
|
||||
coinCardBody: {
|
||||
gap: 16,
|
||||
alignItems: "center",
|
||||
},
|
||||
coinIcon: {
|
||||
width: 26,
|
||||
height: 26,
|
||||
resizeMode: "contain",
|
||||
},
|
||||
coinAmount: {
|
||||
fontFamily: FONT_FAMILY.InterBold,
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
textAlign: "center",
|
||||
},
|
||||
coinPackName: {
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
textAlign: "center",
|
||||
},
|
||||
coinPackDescription: {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 14,
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
textAlign: "center",
|
||||
},
|
||||
coinPrice: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 18,
|
||||
color: Palette.white,
|
||||
textAlign: "center",
|
||||
},
|
||||
cardHeader: {
|
||||
gap: 6,
|
||||
},
|
||||
|
||||
@@ -1,29 +1,42 @@
|
||||
import React, { useMemo, useRef, useState } from "react";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import { Dimensions, Modal, Text, View } from "react-native";
|
||||
import SwiperFlatList from "react-native-swiper-flatlist";
|
||||
import { background, icons } from "../../assets";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import firebase from "../../config/firebase";
|
||||
import AppAlert from "../../components/Alert";
|
||||
import firebase, { usersRef } from "../../config/firebase";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { gutters } from "../../styles";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { normalizeStructureType } from "../../utils/songStructure";
|
||||
import { openCoinPackModal } from "../../utils/coinPackModal";
|
||||
import ChooseGenre from "./ChooseGenre";
|
||||
import ChooseInstruments from "./ChooseInstruments";
|
||||
import ChooseRhythm from "./ChooseRhythm";
|
||||
import CustomizeVoice from "./CustomizeVoice";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
|
||||
const { width } = Dimensions.get("window");
|
||||
const MUSIC_GENERATION_COIN_COST = 8;
|
||||
const CONFIRM_MODAL_MAX_WIDTH = 540;
|
||||
|
||||
const ComposeSong = () => {
|
||||
const scrollRef = useRef(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [progress, setProgress] = useState(18);
|
||||
const [containerLayout, setContainerLayout] = useState(null);
|
||||
const { selectedProjectId, selectedProject, updateProjectData } = useUser();
|
||||
const {
|
||||
selectedProjectId,
|
||||
selectedProject,
|
||||
updateProjectData,
|
||||
currentUserData,
|
||||
currentUID,
|
||||
} = useUser();
|
||||
|
||||
// Selections state
|
||||
const [genres, setGenres] = useState([]);
|
||||
@@ -32,6 +45,45 @@ const ComposeSong = () => {
|
||||
const [instruments, setInstruments] = useState([]);
|
||||
const [rhythm, setRhythm] = useState(null);
|
||||
|
||||
const [isConfirmVisible, setIsConfirmVisible] = useState(false);
|
||||
const [isProcessingConfirmation, setIsProcessingConfirmation] =
|
||||
useState(false);
|
||||
|
||||
const coinBalance = useMemo(() => {
|
||||
const data = currentUserData || {};
|
||||
const candidates = [
|
||||
data?.coins,
|
||||
data?.coinBalance,
|
||||
data?.coin,
|
||||
data?.wallet?.coins,
|
||||
data?.wallet?.coinBalance,
|
||||
];
|
||||
|
||||
for (const value of candidates) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}, [currentUserData]);
|
||||
|
||||
const formattedCoinBalance = useMemo(() => {
|
||||
try {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(coinBalance);
|
||||
} catch (_error) {
|
||||
return `${coinBalance}`;
|
||||
}
|
||||
}, [coinBalance]);
|
||||
|
||||
const isStepValid = useMemo(() => {
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
@@ -76,34 +128,85 @@ const ComposeSong = () => {
|
||||
};
|
||||
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId]);
|
||||
|
||||
const onPressNext = async () => {
|
||||
if (selectedIndex === 3) {
|
||||
try {
|
||||
// Persist config on the selected selectedProject so GeneratingSong can pick it up
|
||||
if (selectedProjectId) {
|
||||
await updateProjectData({
|
||||
musicConfig: {
|
||||
title: musicConfig?.title || "",
|
||||
lyrics: Array.isArray(musicConfig?.lyrics)
|
||||
? musicConfig.lyrics
|
||||
: [],
|
||||
genres: Array.isArray(musicConfig?.genres)
|
||||
? musicConfig.genres
|
||||
: [],
|
||||
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
|
||||
instruments: Array.isArray(musicConfig?.instruments)
|
||||
? musicConfig.instruments
|
||||
: [],
|
||||
tempo: musicConfig?.tempo || "",
|
||||
},
|
||||
musicStatus: null,
|
||||
sunoTaskId: firebase.firestore.FieldValue.delete(),
|
||||
musicUrls: firebase.firestore.FieldValue.delete(),
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
// Also pass the config to the screen to avoid any race condition
|
||||
const persistMusicConfig = async () => {
|
||||
try {
|
||||
if (!selectedProjectId) return;
|
||||
await updateProjectData({
|
||||
musicConfig: {
|
||||
title: musicConfig?.title || "",
|
||||
lyrics: Array.isArray(musicConfig?.lyrics) ? musicConfig.lyrics : [],
|
||||
genres: Array.isArray(musicConfig?.genres) ? musicConfig.genres : [],
|
||||
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
|
||||
instruments: Array.isArray(musicConfig?.instruments)
|
||||
? musicConfig.instruments
|
||||
: [],
|
||||
tempo: musicConfig?.tempo || "",
|
||||
},
|
||||
musicStatus: null,
|
||||
sunoTaskId: firebase.firestore.FieldValue.delete(),
|
||||
musicUrls: firebase.firestore.FieldValue.delete(),
|
||||
});
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const spendCoinsForGeneration = async () => {
|
||||
if (!currentUID) {
|
||||
throw new Error("Utilisateur introuvable. Merci de réessayer.");
|
||||
}
|
||||
|
||||
const decrement = firebase.firestore.FieldValue.increment(
|
||||
-MUSIC_GENERATION_COIN_COST,
|
||||
);
|
||||
const timestamp = firebase.firestore.FieldValue.serverTimestamp();
|
||||
|
||||
await usersRef.doc(currentUID).set(
|
||||
{
|
||||
coins: decrement,
|
||||
coinBalance: decrement,
|
||||
wallet: {
|
||||
coins: decrement,
|
||||
coinBalance: decrement,
|
||||
},
|
||||
lastCoinSpendAt: timestamp,
|
||||
lastCoinSpendAmount: MUSIC_GENERATION_COIN_COST,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
};
|
||||
|
||||
const handleConfirmGeneration = async () => {
|
||||
if (isProcessingConfirmation) return;
|
||||
setIsProcessingConfirmation(true);
|
||||
try {
|
||||
const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0;
|
||||
if (availableCoins < MUSIC_GENERATION_COIN_COST) {
|
||||
setIsConfirmVisible(false);
|
||||
AppAlert(
|
||||
"Crédits insuffisants",
|
||||
"Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer.",
|
||||
);
|
||||
openCoinPackModal();
|
||||
return;
|
||||
}
|
||||
|
||||
await spendCoinsForGeneration();
|
||||
await persistMusicConfig();
|
||||
setIsConfirmVisible(false);
|
||||
navigate(Routes.GeneratingSong, { config: musicConfig });
|
||||
} catch (error) {
|
||||
setIsConfirmVisible(false);
|
||||
const message =
|
||||
error?.message ||
|
||||
"Une erreur est survenue lors du lancement de la génération.";
|
||||
AppAlert("Impossible de lancer la génération", message);
|
||||
} finally {
|
||||
setIsProcessingConfirmation(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onPressNext = () => {
|
||||
if (selectedIndex === 3) {
|
||||
setIsConfirmVisible(true);
|
||||
return;
|
||||
}
|
||||
setSelectedIndex(selectedIndex + 1);
|
||||
@@ -193,6 +296,94 @@ const ComposeSong = () => {
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent
|
||||
visible={isConfirmVisible}
|
||||
onRequestClose={() => {
|
||||
if (isProcessingConfirmation) return;
|
||||
setIsConfirmVisible(false);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: gutters,
|
||||
paddingVertical: gutters * 1.5,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||
}}
|
||||
>
|
||||
<CreateLyricsHeader
|
||||
containerStyle={{
|
||||
width: "100%",
|
||||
maxWidth: CONFIRM_MODAL_MAX_WIDTH,
|
||||
alignSelf: "center",
|
||||
paddingVertical: 24,
|
||||
paddingHorizontal: 24,
|
||||
gap: 24,
|
||||
}}
|
||||
>
|
||||
<View style={{ gap: 30, alignItems: "center" }}>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 15,
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Générer la musique ?
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Cette action coûte {MUSIC_GENERATION_COIN_COST} pièces.
|
||||
{"\n"}Souhaites-tu les utiliser pour lancer la génération ?
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Solde disponible : {formattedCoinBalance} pièces
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
|
||||
<GradientButton
|
||||
title="Confirmer"
|
||||
onPress={handleConfirmGeneration}
|
||||
disabled={isProcessingConfirmation}
|
||||
/>
|
||||
<BorderGradientButton
|
||||
title="Annuler"
|
||||
onPress={() => {
|
||||
if (isProcessingConfirmation) return;
|
||||
setIsConfirmVisible(false);
|
||||
}}
|
||||
disabled={isProcessingConfirmation}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
</Modal>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,23 +5,30 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Dimensions, FlatList, View } from "react-native";
|
||||
import { Dimensions, FlatList, Modal, Text, View } from "react-native";
|
||||
import { background } from "../../assets";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import firebase from "../../config/firebase";
|
||||
import AppAlert from "../../components/Alert";
|
||||
import firebase, { usersRef } from "../../config/firebase";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { gutters } from "../../styles";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { normalizeStructureType } from "../../utils/songStructure";
|
||||
import { openCoinPackModal } from "../../utils/coinPackModal";
|
||||
import ChooseGenre from "./ChooseGenre";
|
||||
import ChooseInstruments from "./ChooseInstruments";
|
||||
import ChooseRhythm from "./ChooseRhythm";
|
||||
import CustomizeVoice from "./CustomizeVoice";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
|
||||
const { width: windowWidth } = Dimensions.get("window");
|
||||
const MUSIC_GENERATION_COIN_COST = 8;
|
||||
const CONFIRM_MODAL_MAX_WIDTH = 540;
|
||||
|
||||
const ComposeSong = () => {
|
||||
const scrollRef = useRef(null);
|
||||
@@ -29,12 +36,56 @@ const ComposeSong = () => {
|
||||
const [progress, setProgress] = useState(18);
|
||||
const [parentLayout, setParentLayout] = useState(null);
|
||||
const containerWidth = parentLayout?.width || windowWidth || 1;
|
||||
const { selectedProjectId, selectedProject, updateProjectData } = useUser();
|
||||
const {
|
||||
selectedProjectId,
|
||||
selectedProject,
|
||||
updateProjectData,
|
||||
currentUserData,
|
||||
currentUID,
|
||||
} = useUser();
|
||||
|
||||
const [genres, setGenres] = useState([]);
|
||||
const [voice, setVoice] = useState({});
|
||||
const [instruments, setInstruments] = useState([]);
|
||||
const [rhythm, setRhythm] = useState(null);
|
||||
const [isConfirmVisible, setIsConfirmVisible] = useState(false);
|
||||
const [isProcessingConfirmation, setIsProcessingConfirmation] =
|
||||
useState(false);
|
||||
|
||||
const coinBalance = useMemo(() => {
|
||||
const data = currentUserData || {};
|
||||
const candidates = [
|
||||
data?.coins,
|
||||
data?.coinBalance,
|
||||
data?.coin,
|
||||
data?.wallet?.coins,
|
||||
data?.wallet?.coinBalance,
|
||||
];
|
||||
|
||||
for (const value of candidates) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}, [currentUserData]);
|
||||
|
||||
const formattedCoinBalance = useMemo(() => {
|
||||
try {
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(coinBalance);
|
||||
} catch (_error) {
|
||||
return `${coinBalance}`;
|
||||
}
|
||||
}, [coinBalance]);
|
||||
|
||||
const isStepValid = useMemo(() => {
|
||||
switch (selectedIndex) {
|
||||
@@ -132,32 +183,91 @@ const ComposeSong = () => {
|
||||
[containerWidth]
|
||||
);
|
||||
|
||||
const onPressNext = async () => {
|
||||
if (selectedIndex === steps.length - 1) {
|
||||
try {
|
||||
if (selectedProjectId) {
|
||||
await updateProjectData({
|
||||
musicConfig: {
|
||||
title: musicConfig?.title || "",
|
||||
lyrics: Array.isArray(musicConfig?.lyrics)
|
||||
? musicConfig.lyrics
|
||||
: [],
|
||||
genres: Array.isArray(musicConfig?.genres)
|
||||
? musicConfig.genres
|
||||
: [],
|
||||
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
|
||||
instruments: Array.isArray(musicConfig?.instruments)
|
||||
? musicConfig.instruments
|
||||
: [],
|
||||
tempo: musicConfig?.tempo || "",
|
||||
},
|
||||
musicStatus: null,
|
||||
sunoTaskId: firebase.firestore.FieldValue.delete(),
|
||||
musicUrls: firebase.firestore.FieldValue.delete(),
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
const persistMusicConfig = useCallback(async () => {
|
||||
try {
|
||||
if (!selectedProjectId) return;
|
||||
await updateProjectData({
|
||||
musicConfig: {
|
||||
title: musicConfig?.title || "",
|
||||
lyrics: Array.isArray(musicConfig?.lyrics) ? musicConfig.lyrics : [],
|
||||
genres: Array.isArray(musicConfig?.genres) ? musicConfig.genres : [],
|
||||
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
|
||||
instruments: Array.isArray(musicConfig?.instruments)
|
||||
? musicConfig.instruments
|
||||
: [],
|
||||
tempo: musicConfig?.tempo || "",
|
||||
},
|
||||
musicStatus: null,
|
||||
sunoTaskId: firebase.firestore.FieldValue.delete(),
|
||||
musicUrls: firebase.firestore.FieldValue.delete(),
|
||||
});
|
||||
} catch (_error) {}
|
||||
}, [musicConfig, selectedProjectId, updateProjectData]);
|
||||
|
||||
const spendCoinsForGeneration = useCallback(async () => {
|
||||
if (!currentUID) {
|
||||
throw new Error("Utilisateur introuvable. Merci de réessayer.");
|
||||
}
|
||||
|
||||
const decrement = firebase.firestore.FieldValue.increment(
|
||||
-MUSIC_GENERATION_COIN_COST,
|
||||
);
|
||||
const timestamp = firebase.firestore.FieldValue.serverTimestamp();
|
||||
|
||||
await usersRef.doc(currentUID).set(
|
||||
{
|
||||
coins: decrement,
|
||||
coinBalance: decrement,
|
||||
wallet: {
|
||||
coins: decrement,
|
||||
coinBalance: decrement,
|
||||
},
|
||||
lastCoinSpendAt: timestamp,
|
||||
lastCoinSpendAmount: MUSIC_GENERATION_COIN_COST,
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
}, [currentUID]);
|
||||
|
||||
const handleConfirmGeneration = useCallback(async () => {
|
||||
if (isProcessingConfirmation) return;
|
||||
setIsProcessingConfirmation(true);
|
||||
try {
|
||||
const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0;
|
||||
if (availableCoins < MUSIC_GENERATION_COIN_COST) {
|
||||
setIsConfirmVisible(false);
|
||||
AppAlert(
|
||||
"Crédits insuffisants",
|
||||
"Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer.",
|
||||
);
|
||||
openCoinPackModal();
|
||||
return;
|
||||
}
|
||||
|
||||
await spendCoinsForGeneration();
|
||||
await persistMusicConfig();
|
||||
setIsConfirmVisible(false);
|
||||
navigate(Routes.GeneratingSong, { config: musicConfig });
|
||||
} catch (error) {
|
||||
setIsConfirmVisible(false);
|
||||
const message =
|
||||
error?.message ||
|
||||
"Une erreur est survenue lors du lancement de la génération.";
|
||||
AppAlert("Impossible de lancer la génération", message);
|
||||
} finally {
|
||||
setIsProcessingConfirmation(false);
|
||||
}
|
||||
}, [
|
||||
coinBalance,
|
||||
isProcessingConfirmation,
|
||||
musicConfig,
|
||||
persistMusicConfig,
|
||||
spendCoinsForGeneration,
|
||||
]);
|
||||
|
||||
const onPressNext = () => {
|
||||
if (selectedIndex === steps.length - 1) {
|
||||
setIsConfirmVisible(true);
|
||||
return;
|
||||
}
|
||||
setSelectedIndex((prev) => Math.min(prev + 1, steps.length - 1));
|
||||
@@ -215,6 +325,94 @@ const ComposeSong = () => {
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent
|
||||
visible={isConfirmVisible}
|
||||
onRequestClose={() => {
|
||||
if (isProcessingConfirmation) return;
|
||||
setIsConfirmVisible(false);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: gutters,
|
||||
paddingVertical: gutters * 1.5,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||
}}
|
||||
>
|
||||
<CreateLyricsHeader
|
||||
containerStyle={{
|
||||
width: "100%",
|
||||
maxWidth: CONFIRM_MODAL_MAX_WIDTH,
|
||||
alignSelf: "center",
|
||||
paddingVertical: 24,
|
||||
paddingHorizontal: 24,
|
||||
gap: 24,
|
||||
}}
|
||||
>
|
||||
<View style={{ gap: 30, alignItems: "center" }}>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 15,
|
||||
gap: 12,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 22,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Générer la musique ?
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Cette action coûte {MUSIC_GENERATION_COIN_COST} pièces.
|
||||
{"\n"}Souhaites-tu les utiliser pour lancer la génération ?
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 14,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Solde disponible : {formattedCoinBalance} pièces
|
||||
</Text>
|
||||
</View>
|
||||
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
|
||||
<GradientButton
|
||||
title="Confirmer"
|
||||
onPress={handleConfirmGeneration}
|
||||
disabled={isProcessingConfirmation}
|
||||
/>
|
||||
<BorderGradientButton
|
||||
title="Annuler"
|
||||
onPress={() => {
|
||||
if (isProcessingConfirmation) return;
|
||||
setIsConfirmVisible(false);
|
||||
}}
|
||||
disabled={isProcessingConfirmation}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
</Modal>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import { background, icons } from "../../assets";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import { background } from "../../assets";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { strings } from "../../constants/strings";
|
||||
@@ -14,16 +13,22 @@ import { useUserData } from "../../providers/UserDataProvider";
|
||||
import { Palette, gutters } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
|
||||
const CTA_BUTTON_HEIGHT = 58;
|
||||
const CTA_BUTTON_MAX_WIDTH = 520;
|
||||
|
||||
const WritingLyrics = () => {
|
||||
const { createNewProject, selectedProject, updateProjectData } =
|
||||
useUserData();
|
||||
const {
|
||||
createNewProject,
|
||||
selectedProjectId,
|
||||
updateProjectData,
|
||||
} = useUserData();
|
||||
const { setIsLoading } = useMinuit();
|
||||
|
||||
const startWriting = React.useCallback(async () => {
|
||||
try {
|
||||
await setIsLoading(true);
|
||||
if (selectedProject) {
|
||||
updateProjectData({ hasLyrics: false });
|
||||
if (selectedProjectId) {
|
||||
await updateProjectData({ hasLyrics: false });
|
||||
setTimeout(() => navigate(Routes.CreateLyricsWithAi), 500);
|
||||
return;
|
||||
}
|
||||
@@ -40,7 +45,7 @@ const WritingLyrics = () => {
|
||||
}, [
|
||||
createNewProject,
|
||||
navigate,
|
||||
selectedProject,
|
||||
selectedProjectId,
|
||||
setIsLoading,
|
||||
updateProjectData,
|
||||
]);
|
||||
@@ -51,11 +56,7 @@ const WritingLyrics = () => {
|
||||
headerType="NONE"
|
||||
>
|
||||
{/* <Image source={ai.nathalie} style={styles.img} resizeMode="contain" /> */}
|
||||
<MusicLandHeader
|
||||
onPressBack={goBack}
|
||||
progress={9}
|
||||
logo={icons.musicLandWriting}
|
||||
/>
|
||||
<MusicLandHeader onPressBack={goBack} />
|
||||
<View style={styles.contentWrapper}>
|
||||
<View style={styles.heroContainer}>
|
||||
{/* <Text style={styles.heroTitle}>
|
||||
@@ -68,18 +69,12 @@ const WritingLyrics = () => {
|
||||
<View style={styles.ctaGroup}>
|
||||
<GradientButton
|
||||
size="large"
|
||||
maxWidth={520}
|
||||
containerStyle={{ alignSelf: "center", width: "100%" }}
|
||||
height={CTA_BUTTON_HEIGHT}
|
||||
maxWidth={CTA_BUTTON_MAX_WIDTH}
|
||||
containerStyle={styles.ctaButton}
|
||||
title={strings.writing.onboarding.primaryCta}
|
||||
onPress={startWriting}
|
||||
textStyle={{ fontFamily: FONT_FAMILY.InterSemiBold }}
|
||||
/>
|
||||
<BorderGradientButton
|
||||
size="small"
|
||||
title={strings.writing.onboarding.secondaryCta}
|
||||
disabled
|
||||
containerStyle={{ alignSelf: "center", width: "100%" }}
|
||||
maxWidth={360}
|
||||
textStyle={styles.ctaButtonText}
|
||||
/>
|
||||
<Text style={styles.secondaryNote}>
|
||||
{strings.writing.onboarding.secondaryNote}
|
||||
@@ -127,6 +122,14 @@ const styles = StyleSheet.create({
|
||||
ctaGroup: {
|
||||
gap: 12,
|
||||
},
|
||||
ctaButton: {
|
||||
alignSelf: "center",
|
||||
width: "100%",
|
||||
maxWidth: CTA_BUTTON_MAX_WIDTH,
|
||||
},
|
||||
ctaButtonText: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
secondaryNote: {
|
||||
textAlign: "center",
|
||||
color: Palette.white,
|
||||
|
||||
Reference in New Issue
Block a user