feat: fixes

This commit is contained in:
2025-12-10 11:52:25 +01:00
parent d92303dd8a
commit bff35bb118
7 changed files with 348 additions and 15 deletions
+155
View File
@@ -0,0 +1,155 @@
import React, { useState, useEffect } from "react";
import { Modal, View, Text, StyleSheet, ScrollView, Pressable } from "react-native";
import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import GradientButton from "../GradientButton";
import BorderGradientButton from "../BorderGradientButton";
const WebDateModal = ({ visible, onClose, onValidate, initialDate }) => {
const currentYear = new Date().getFullYear();
const [day, setDay] = useState(initialDate ? initialDate.getDate() : 1);
const [month, setMonth] = useState(initialDate ? initialDate.getMonth() : 0);
const [year, setYear] = useState(initialDate ? initialDate.getFullYear() : currentYear - 18);
useEffect(() => {
if (visible && initialDate) {
setDay(initialDate.getDate());
setMonth(initialDate.getMonth());
setYear(initialDate.getFullYear());
} else if (visible && !initialDate) {
setYear(currentYear - 18); // Default to 18 years ago
}
}, [visible, initialDate]);
const days = Array.from({ length: 31 }, (_, i) => i + 1);
const months = [
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
];
const years = Array.from({ length: 100 }, (_, i) => currentYear - i);
const renderPickerColumn = (data, selectedValue, onSelect, getLabel = (item) => item, getValue = (item, index) => item) => (
<View style={styles.column}>
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
{data.map((item, index) => {
const value = getValue(item, index);
const isSelected = selectedValue === value;
return (
<Pressable
key={index}
onPress={() => onSelect(value)}
style={[styles.item, isSelected && styles.selectedItem]}
>
<Text style={[styles.itemText, isSelected && styles.selectedItemText]}>
{getLabel(item)}
</Text>
</Pressable>
);
})}
</ScrollView>
</View>
);
const handleValidate = () => {
const newDate = new Date(year, month, day);
onValidate(newDate);
onClose();
};
return (
<Modal
visible={visible}
transparent={true}
animationType="fade"
onRequestClose={onClose}
>
<View style={styles.overlay}>
<View style={styles.modalContainer}>
<Text style={styles.title}>Date de naissance</Text>
<View style={styles.pickerContainer}>
{renderPickerColumn(days, day, setDay)}
{renderPickerColumn(months, month, setMonth, (m) => m, (_, i) => i)}
{renderPickerColumn(years, year, setYear)}
</View>
<View style={styles.buttonContainer}>
<BorderGradientButton
title="Annuler"
onPress={onClose}
containerStyle={{ flex: 1 }}
/>
<View style={{ width: 10 }} />
<GradientButton
title="Valider"
onPress={handleValidate}
containerStyle={{ flex: 1 }}
/>
</View>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.7)",
justifyContent: "center",
alignItems: "center",
},
modalContainer: {
width: 400,
backgroundColor: Palette.black,
borderRadius: 20,
padding: gutters,
borderWidth: 1,
borderColor: Palette.gray,
maxHeight: "80%",
},
title: {
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 18,
textAlign: "center",
marginBottom: 20,
},
pickerContainer: {
flexDirection: "row",
justifyContent: "space-between",
height: 200,
marginBottom: 20,
},
column: {
flex: 1,
borderWidth: 1,
borderColor: Palette.transparentPrimary,
borderRadius: 8,
marginHorizontal: 4,
backgroundColor: Palette.glass,
},
scrollContent: {
paddingVertical: 10,
},
item: {
paddingVertical: 8,
alignItems: "center",
},
selectedItem: {
backgroundColor: Palette.primary,
},
itemText: {
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 16,
},
selectedItemText: {
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-between",
},
});
export default WebDateModal;
+1 -1
View File
@@ -52,7 +52,7 @@ export const BottomTabScreen = () => {
name={Routes.HitParade} name={Routes.HitParade}
component={HitParade} component={HitParade}
options={{ options={{
tabBarLabel: "Hit Parade", tabBarLabel: "Streaming",
headerShown: false, headerShown: false,
tabBarShowLabel: true, tabBarShowLabel: true,
tabBarIcon: ({ focused }) => renderIcon(tabs.ribbon, focused), tabBarIcon: ({ focused }) => renderIcon(tabs.ribbon, focused),
+2
View File
@@ -26,6 +26,7 @@ const CreatePassword = () => {
const city = route?.params?.city || ""; const city = route?.params?.city || "";
const country = route?.params?.country || null; const country = route?.params?.country || null;
const preferredLanguage = route?.params?.preferredLanguage || null; const preferredLanguage = route?.params?.preferredLanguage || null;
const birthDate = route?.params?.birthDate || null;
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [passwordError, setPasswordError] = useState(""); const [passwordError, setPasswordError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -80,6 +81,7 @@ const CreatePassword = () => {
...(trimmedLastName ? { lastName: trimmedLastName } : {}), ...(trimmedLastName ? { lastName: trimmedLastName } : {}),
...(trimmedCity ? { city: trimmedCity } : {}), ...(trimmedCity ? { city: trimmedCity } : {}),
...(preferredLanguage ? { preferredLanguage } : {}), ...(preferredLanguage ? { preferredLanguage } : {}),
...(birthDate ? { birthDate } : {}),
...(country?.code ? { countryCode: country.code } : {}), ...(country?.code ? { countryCode: country.code } : {}),
...(country?.name ? { countryName: country.name } : {}), ...(country?.name ? { countryName: country.name } : {}),
createdAt: firebase.firestore.FieldValue.serverTimestamp(), createdAt: firebase.firestore.FieldValue.serverTimestamp(),
+30 -2
View File
@@ -1,5 +1,6 @@
import { View, Text, Image } from "react-native"; import { View, Text, Image } from "react-native";
import React, { useCallback, useEffect } from "react"; import React, { useCallback, useEffect, useState } from "react";
import AppCheckbox from "../../components/AppCheckbox";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
@@ -22,6 +23,7 @@ const StreamSong = () => {
const { hasActiveSubscription } = useUser() || {}; const { hasActiveSubscription } = useUser() || {};
const userHasActiveSubscription = hasActiveSubscription; const userHasActiveSubscription = hasActiveSubscription;
const { setTooltip } = useMinuit(); const { setTooltip } = useMinuit();
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true);
useEffect(() => { useEffect(() => {
if (action && action !== "playback") { if (action && action !== "playback") {
@@ -37,6 +39,13 @@ const StreamSong = () => {
? "Publier mon playback" ? "Publier mon playback"
: "Rejoindre le Club MusicLand"; : "Rejoindre le Club MusicLand";
const handlePrimaryAction = useCallback(() => { const handlePrimaryAction = useCallback(() => {
if (!hasAcceptedPublication) {
setTooltip({
type: "error",
text: "Confirme la diffusion sur Musicland et YouTube avant de publier",
});
return;
}
if (userHasActiveSubscription) { if (userHasActiveSubscription) {
if (action === "playback") { if (action === "playback") {
setTooltip({ setTooltip({
@@ -50,7 +59,7 @@ const StreamSong = () => {
return; return;
} }
navigate(Routes.Payments); navigate(Routes.Payments);
}, [action, setTooltip, userHasActiveSubscription]); }, [action, hasAcceptedPublication, setTooltip, userHasActiveSubscription]);
return ( return (
<Page <Page
@@ -103,6 +112,25 @@ const StreamSong = () => {
</View> </View>
</CreateLyricsHeader> </CreateLyricsHeader>
<View style={{ marginTop: 24, gap: 12 }}> <View style={{ marginTop: 24, gap: 12 }}>
<View style={{ gap: 6 }}>
<AppCheckbox
selected={hasAcceptedPublication}
onPress={() =>
setHasAcceptedPublication((prevState) => !prevState)
}
label="J'accepte la diffusion de mon contenu sur Musicland et YouTube."
/>
<Text
style={{
fontSize: 13,
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.white,
opacity: 0.8,
}}
>
Cette confirmation est requise avant toute mise en ligne.
</Text>
</View>
<GradientButton <GradientButton
title={ctaLabel} title={ctaLabel}
onPress={handlePrimaryAction} onPress={handlePrimaryAction}
+90 -9
View File
@@ -1,6 +1,8 @@
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import * as AppleAuthentication from "expo-apple-authentication"; import * as AppleAuthentication from "expo-apple-authentication";
import DateTimePicker from "@react-native-community/datetimepicker";
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useGlobal } from "reactn";
import { import {
ActivityIndicator, ActivityIndicator,
KeyboardAvoidingView, KeyboardAvoidingView,
@@ -16,6 +18,7 @@ import { background } from "../assets";
import BorderGradientButton from "../components/BorderGradientButton"; import BorderGradientButton from "../components/BorderGradientButton";
import { Input } from "../components/Input"; import { Input } from "../components/Input";
import ItemContainer from "../components/ItemContainer/ItemContainer"; import ItemContainer from "../components/ItemContainer/ItemContainer";
import WebDateModal from "../components/modal/WebDateModal";
import firebase from "../config/firebase"; import firebase from "../config/firebase";
import { isWeb } from "../hooks/useLayoutType"; import { isWeb } from "../hooks/useLayoutType";
import useSocialAuth from "../hooks/useSocialAuth"; import useSocialAuth from "../hooks/useSocialAuth";
@@ -32,6 +35,9 @@ const Register = () => {
const [firstName, setFirstName] = useState(""); const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState(""); const [lastName, setLastName] = useState("");
const [preferredLanguage, setPreferredLanguage] = useState(null); const [preferredLanguage, setPreferredLanguage] = useState(null);
const [birthDate, setBirthDate] = useState(null);
const [showDatePicker, setShowDatePicker] = useState(false);
const [, setTooltip] = useGlobal("_tooltip");
const afterSocialAuth = useCallback(async () => { const afterSocialAuth = useCallback(async () => {
const uid = firebase.auth().currentUser?.uid; const uid = firebase.auth().currentUser?.uid;
@@ -65,7 +71,43 @@ const Register = () => {
const isFormValid = const isFormValid =
email.trim().length > 0 && email.trim().length > 0 &&
firstName.trim().length > 0 && firstName.trim().length > 0 &&
lastName.trim().length > 0; lastName.trim().length > 0 &&
birthDate;
function isAdult(date) {
if (!date) return false;
const today = new Date();
let age = today.getFullYear() - date.getFullYear();
const m = today.getMonth() - date.getMonth();
if (m < 0 || (m === 0 && today.getDate() < date.getDate())) {
age--;
}
return age >= 18;
}
const handleNext = () => {
if (!isAdult(birthDate)) {
setTooltip({
text: "Vous devez avoir au moins 18 ans pour créer un compte",
type: "error",
});
return;
}
navigate(Routes.CreatePassword, {
email: email.trim(),
firstName: firstName.trim(),
lastName: lastName.trim(),
birthDate: birthDate ? birthDate.toISOString() : null,
preferredLanguage,
});
};
const onDateChange = (event, selectedDate) => {
setShowDatePicker(Platform.OS === "ios");
if (selectedDate) {
setBirthDate(selectedDate);
}
};
const renderFormContent = () => ( const renderFormContent = () => (
<View style={styles.formContent}> <View style={styles.formContent}>
@@ -101,6 +143,52 @@ const Register = () => {
value={email} value={email}
setValue={setEmail} setValue={setEmail}
/> />
{isWeb ? (
<>
<Pressable onPress={() => setShowDatePicker(true)}>
<View pointerEvents="none">
<Input
placeholder="Date de naissance"
label="Date de naissance"
isBlur
value={birthDate ? birthDate.toLocaleDateString() : ""}
editable={false}
/>
</View>
</Pressable>
<WebDateModal
visible={showDatePicker}
onClose={() => setShowDatePicker(false)}
onValidate={(date) => setBirthDate(date)}
initialDate={birthDate}
/>
</>
) : (
<>
<Pressable onPress={() => setShowDatePicker(true)}>
<View pointerEvents="none">
<Input
placeholder="Date de naissance"
label="Date de naissance"
isBlur
value={birthDate ? birthDate.toLocaleDateString() : ""}
editable={false}
/>
</View>
</Pressable>
{showDatePicker && (
<DateTimePicker
testID="dateTimePicker"
value={birthDate || new Date()}
mode="date"
is24Hour={true}
display="default"
onChange={onDateChange}
maximumDate={new Date()}
/>
)}
</>
)}
</View> </View>
<BorderGradientButton <BorderGradientButton
title="Créer mon compte" title="Créer mon compte"
@@ -108,14 +196,7 @@ const Register = () => {
width: "90%", width: "90%",
alignSelf: "center", alignSelf: "center",
}} }}
onPress={() => onPress={handleNext}
navigate(Routes.CreatePassword, {
email: email.trim(),
firstName: firstName.trim(),
lastName: lastName.trim(),
preferredLanguage,
})
}
disabled={!isFormValid || isBusy} disabled={!isFormValid || isBusy}
/> />
<View style={styles.socialWrapper}> <View style={styles.socialWrapper}>
+29 -2
View File
@@ -1,4 +1,5 @@
import { View, StyleSheet } from "react-native"; import { View, StyleSheet, Text } from "react-native";
import { MaterialIcons } from "@expo/vector-icons";
import React, { useState } from "react"; import React, { useState } from "react";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import ItemContainer from "../../components/ItemContainer/ItemContainer"; import ItemContainer from "../../components/ItemContainer/ItemContainer";
@@ -17,7 +18,33 @@ const ChooseInstruments = ({ selected = [], setSelected }) => {
<CreateLyricsHeader <CreateLyricsHeader
title="Choisis des instruments" title="Choisis des instruments"
subTitle="Tu peux choisir jusqu’à 5 instruments différents" subTitle="Tu peux choisir jusqu’à 5 instruments différents"
/> >
<View
style={{
flexDirection: "row",
gap: 6,
backgroundColor: Palette.transparentOrange,
padding: 8,
borderRadius: 8,
borderWidth: 1,
borderColor: Palette.orange,
marginTop: 4,
}}
>
<MaterialIcons name="info-outline" size={20} color={Palette.orange} />
<Text
style={{
flex: 1,
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 12,
color: Palette.white,
}}
>
Attention : Suno peut ajuster ou supprimer certains paramètres
choisis pour maintenir la cohérence de la création musicale.
</Text>
</View>
</CreateLyricsHeader>
<View <View
style={{ flex: 1 }} style={{ flex: 1 }}
onLayout={(event) => setContainerLayout(event.nativeEvent.layout)} onLayout={(event) => setContainerLayout(event.nativeEvent.layout)}
+41 -1
View File
@@ -8,9 +8,12 @@ import {
StyleSheet, StyleSheet,
Text, Text,
View, View,
Alert,
} from "react-native"; } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
import * as Sharing from "expo-sharing"; import * as Sharing from "expo-sharing";
import AppCheckbox from "../../components/AppCheckbox";
import BorderGradientButton from "../../components/BorderGradientButton"; import BorderGradientButton from "../../components/BorderGradientButton";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
@@ -38,9 +41,11 @@ const SongDownload = ({ route }) => {
} = route?.params || {}; } = route?.params || {};
const { selectedProject, updateProjectData, hasActiveSubscription } = const { selectedProject, updateProjectData, hasActiveSubscription } =
useUser(); useUser();
const { setTooltip } = useMinuit();
const { setLoading } = useGlobalLoading(); const { setLoading } = useGlobalLoading();
const [isDownloading, setIsDownloading] = useState(false); const [isDownloading, setIsDownloading] = useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false); const [showConfirmModal, setShowConfirmModal] = useState(false);
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true);
const projectForStage = useMemo( const projectForStage = useMemo(
() => routeProject || selectedProject || null, () => routeProject || selectedProject || null,
@@ -130,12 +135,26 @@ const SongDownload = ({ route }) => {
]); ]);
const handleContinue = useCallback(() => { const handleContinue = useCallback(() => {
if (!hasAcceptedPublication) {
if (setTooltip) {
setTooltip({
type: "error",
text: "Confirme la diffusion sur Musicland et YouTube avant de continuer",
});
} else {
Alert.alert(
"Confirmation requise",
"Confirme la diffusion sur Musicland et YouTube avant de continuer"
);
}
return;
}
if (hasActiveSubscription) { if (hasActiveSubscription) {
continueFlow(); continueFlow();
return; return;
} }
setShowConfirmModal(true); setShowConfirmModal(true);
}, [continueFlow, hasActiveSubscription]); }, [continueFlow, hasAcceptedPublication, hasActiveSubscription, setTooltip]);
const handleDownload = useCallback(async () => { const handleDownload = useCallback(async () => {
const downloadUrl = const downloadUrl =
@@ -401,6 +420,17 @@ const SongDownload = ({ route }) => {
<ClubAdvantagesCard style={styles.clubCardSpacing} /> <ClubAdvantagesCard style={styles.clubCardSpacing} />
<View style={styles.consentContainer}>
<AppCheckbox
selected={hasAcceptedPublication}
onPress={() => setHasAcceptedPublication((prev) => !prev)}
label="J'accepte la diffusion de mon contenu sur Musicland et YouTube."
/>
<Text style={styles.consentDescription}>
Cette confirmation est requise pour continuer.
</Text>
</View>
<BorderGradientButton <BorderGradientButton
title={ title={
hasActiveSubscription hasActiveSubscription
@@ -438,6 +468,16 @@ const styles = StyleSheet.create({
scrollContent: { scrollContent: {
paddingBottom: gutters * 2.6, paddingBottom: gutters * 2.6,
}, },
consentContainer: {
gap: 6,
marginTop: 10,
},
consentDescription: {
fontSize: 13,
fontFamily: FONT_FAMILY.InterRegular,
color: Palette.white,
opacity: 0.8,
},
coverRow: { coverRow: {
flexDirection: isWeb ? "row" : "column", flexDirection: isWeb ? "row" : "column",
alignItems: "center", alignItems: "center",