fix empty project creation and use credit to generate

This commit is contained in:
Thomas Demirdjian
2025-11-04 15:59:49 +01:00
parent 7d93be845b
commit 4a45924ee0
10 changed files with 1096 additions and 487 deletions
+227 -29
View File
@@ -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>
);
};