diff --git a/functions/config/keys.js b/functions/config/keys.js index 687aef2..dc5bd77 100644 --- a/functions/config/keys.js +++ b/functions/config/keys.js @@ -2,7 +2,7 @@ exports.GEMINI_API_KEY = "AIzaSyBoPQC5ZaMKP73TlKGpZQp1mAw8ArHiH9Y"; exports.SUNO_API_KEY = "c1636e04f606811511e19ec6e1545aa6"; // api key -exports.RESEND_API_KEY = "re_"; +exports.RESEND_API_KEY = "re_NLcHmYiz_4LoFrzvPBbShNQBgNGTQmBbu"; exports.STRIPE_SECRET_KEY = "sk_test_51SPfcjCzf2o5bDRdUFGNrQYIE271EDfS2Ucn31f98Ublttcl1EBNRoOoJX1RfXXzHp7mKRGrIlCG24biiqUZ2YMh00s9WODluu"; diff --git a/functions/src/lyrics.js b/functions/src/lyrics.js index f1f0f65..70621c8 100644 --- a/functions/src/lyrics.js +++ b/functions/src/lyrics.js @@ -7,7 +7,6 @@ const { } = require("../config/suno"); const { SUNO_API_KEY } = require("../config/keys"); const axios = require("axios"); -const admin = require("firebase-admin"); const { FieldValue } = require("firebase-admin/firestore"); const { refList } = require("../index"); @@ -41,7 +40,7 @@ const STRUCTURE_ALIASES = { "intro instrumentale longue": "long_intro", "pré-refrain": "pre_refrain_instrumental", "pre-refrain": "pre_refrain_instrumental", - "pre_refrain": "pre_refrain_instrumental", + pre_refrain: "pre_refrain_instrumental", "pre chorus": "pre_refrain_instrumental", "pre-chorus": "pre_refrain_instrumental", prechorus: "pre_refrain_instrumental", @@ -62,7 +61,9 @@ const STRUCTURE_ALIASES = { }; const normalizeStructureValue = (value) => { - const raw = String(value || "").trim().toLowerCase(); + const raw = String(value || "") + .trim() + .toLowerCase(); if (!raw) return ""; if (STRUCTURE_PROMPT_LABELS[raw]) return raw; if (STRUCTURE_ALIASES[raw]) return STRUCTURE_ALIASES[raw]; @@ -160,9 +161,7 @@ const moderationIndicatesBlock = (moderation = {}, fallbackText = "") => { const excerpts = Array.isArray(moderation.excerpts) ? moderation.excerpts : []; - const reasons = Array.isArray(moderation.reasons) - ? moderation.reasons - : []; + const reasons = Array.isArray(moderation.reasons) ? moderation.reasons : []; const keywordDetected = (value) => typeof value === "string" && TOXIC_KEYWORDS.some((rx) => rx.test(value)); @@ -182,12 +181,7 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => { style = "", audience = "", emotion = "", - structure: rawStructure = [ - "couplet", - "refrain", - "couplet", - "refrain", - ], + structure: rawStructure = ["couplet", "refrain", "couplet", "refrain"], rhymes = "", } = data; @@ -248,14 +242,14 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => { const system = ` Tu es un parolier expert en chanson française moderne et populaire. -Tu écris des chansons marquantes, accessibles, sur des thématiques d'innovation, d'esprit d'équipe et de créativité digitale. -Ton écriture doit toucher à la fois l'humain, le collectif et la passion d'entreprendre. Analyse attentivement les informations placées entre balises XML-like. Génère un titre accrocheur et mémorable en plus des paroles, tout en respectant strictement la structure demandée. Retourne uniquement un JSON conforme au schéma fourni, sans ajouter d'autres textes. `.trim(); - const structureTags = (Array.isArray(promptStructure) ? promptStructure : []) + const structureTags = ( + Array.isArray(promptStructure) ? promptStructure : [] + ) .map((part, index) => { const content = part || ""; return `
${content}
`; @@ -314,14 +308,12 @@ ${structureTags || fallbackStructureTags} lyrics: z .string() .describe( - "Paroles de la section, chaque ligne séparée " + - "par un retour à la ligne", + "Paroles de la section, chaque ligne séparée par un retour à la ligne", ), }), ) .describe( - "Paroles de la chanson sous forme de tableau de sections " + - "structurées.", + "Paroles de la chanson sous forme de tableau de sections structurées.", ), lyricsDescription: z .string() @@ -339,6 +331,74 @@ ${structureTags || fallbackStructureTags} } }); +const severityScore = (severity = "") => { + const normalized = String(severity || "").toLowerCase(); + switch (normalized) { + case "critical": + return 4; + case "high": + return 3; + case "medium": + return 2; + case "low": + return 1; + default: + return 0; + } +}; + +const softenModerationDecision = (rawResult = {}) => { + const result = { ...rawResult }; + const excerpts = Array.isArray(result.excerpts) ? result.excerpts : []; + const reasons = Array.isArray(result.reasons) ? result.reasons : []; + + const highestSeverity = excerpts.reduce( + (max, excerpt) => Math.max(max, severityScore(excerpt?.severity)), + 0, + ); + const score = + typeof result.score === "number" && !Number.isNaN(result.score) + ? Math.min(Math.max(result.score, 0), 1) + : 0; + + const narrativeHint = reasons.some((reason) => + /narrati|story|persona|fiction|metaphor|metaphore|récit|roleplay|contexte/i.test( + reason || "", + ), + ); + + const adjustments = []; + + if (result.blocked) { + const shouldUnblockLowSeverity = highestSeverity <= 1 && score < 0.65; + if (shouldUnblockLowSeverity) { + result.blocked = false; + adjustments.push("auto-unblock-low-severity"); + } + } + + if (!result.blocked && result.flagged) { + const lowSignal = highestSeverity <= 1 && score < 0.4; + const contextual = narrativeHint && highestSeverity <= 2 && score < 0.55; + if (lowSignal) { + result.flagged = false; + adjustments.push("drop-flag-low-signal"); + } else if (contextual) { + result.flagged = false; + adjustments.push("drop-flag-contextual"); + } + } + + return { + ...result, + moderationAdjustments: adjustments, + moderationCalibration: { + highestSeverity, + score, + }, + }; +}; + // Vérifie la toxicité des paroles via Gemini et retourne un résultat structuré // Nom Cloud Function: lyrics-analyseLyricsToxicity exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => { @@ -364,10 +424,11 @@ exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => { }); const parsed = requestSchema.parse(data || {}); - const result = await analyseLyrics({ + const aiResult = await analyseLyrics({ title: parsed.title || "", lyrics: parsed.lyrics, }); + const result = softenModerationDecision(aiResult); // Prépare un message lisible pour le front const highCats = Object.entries(result.categories || {}) diff --git a/src/assets/UI/leftIcon.png b/src/assets/UI/leftIcon.png deleted file mode 100644 index 38a6903..0000000 Binary files a/src/assets/UI/leftIcon.png and /dev/null differ diff --git a/src/assets/UI/rightIcon.png b/src/assets/UI/rightIcon.png deleted file mode 100644 index 4a63a90..0000000 Binary files a/src/assets/UI/rightIcon.png and /dev/null differ diff --git a/src/assets/icons/production.png b/src/assets/icons/production.png index 3bf484a..fb10929 100644 Binary files a/src/assets/icons/production.png and b/src/assets/icons/production.png differ diff --git a/src/assets/icons/studio.png b/src/assets/icons/studio.png index f1a239a..b005d19 100644 Binary files a/src/assets/icons/studio.png and b/src/assets/icons/studio.png differ diff --git a/src/assets/icons/video.png b/src/assets/icons/video.png index 828592b..5dcfd08 100644 Binary files a/src/assets/icons/video.png and b/src/assets/icons/video.png differ diff --git a/src/assets/icons/writing.png b/src/assets/icons/writing.png index 3731465..9b130da 100644 Binary files a/src/assets/icons/writing.png and b/src/assets/icons/writing.png differ diff --git a/src/assets/index.js b/src/assets/index.js index cc63c79..a2d2c5c 100644 --- a/src/assets/index.js +++ b/src/assets/index.js @@ -219,8 +219,6 @@ export const ai = { malik, bena, john, - leftIcon: require("./UI/leftIcon.png"), - rightIcon: require("./UI/rightIcon.png"), }; export const videos = { diff --git a/src/assets/mockups/loginAppDashboard.png b/src/assets/mockups/loginAppDashboard.png deleted file mode 100644 index 44bf218..0000000 Binary files a/src/assets/mockups/loginAppDashboard.png and /dev/null differ diff --git a/src/components/CreditAmount.js b/src/components/CreditAmount.js new file mode 100644 index 0000000..a69b80f --- /dev/null +++ b/src/components/CreditAmount.js @@ -0,0 +1,121 @@ +import React from "react"; +import { StyleSheet, Text, View } from "react-native"; +import CoinIcon from "./CoinIcon"; +import { FONT_FAMILY } from "../styles/Fonts"; + +const defaultFormatOptions = { + minimumFractionDigits: 0, + maximumFractionDigits: 0, +}; + +const formatAmount = (value, options = defaultFormatOptions) => { + if (typeof value !== "number" || !Number.isFinite(value)) { + return null; + } + + try { + return new Intl.NumberFormat("fr-FR", { + ...defaultFormatOptions, + ...options, + }).format(value); + } catch (_error) { + return `${value}`; + } +}; + +const extractNumericValue = (value) => { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + + if (typeof value === "string") { + const parsed = Number(value); + if (Number.isFinite(parsed)) { + return parsed; + } + } + + return null; +}; + +const CreditAmount = ({ + value, + style, + textStyle, + iconSize = 18, + iconStyle, + iconPosition = "right", + gap = 6, + showPlus = false, + formatterOptions, + accessibilityLabel, +}) => { + const numericValue = React.useMemo( + () => extractNumericValue(value), + [value], + ); + + const resolvedValue = + numericValue !== null + ? numericValue + : value ?? 0; + + const formattedValue = + numericValue !== null + ? formatAmount(resolvedValue, formatterOptions) ?? `${resolvedValue}` + : typeof resolvedValue === "string" + ? resolvedValue + : `${resolvedValue}`; + + const prefix = + numericValue !== null && showPlus && numericValue > 0 ? "+" : ""; + + const a11yLabel = + accessibilityLabel || `${prefix}${formattedValue} pièces`; + + const containerStyles = Array.isArray(style) + ? [styles.container, { gap }, ...style] + : [styles.container, { gap }, style]; + + const textStyles = Array.isArray(textStyle) + ? [styles.value, ...textStyle] + : [styles.value, textStyle]; + + const iconStyles = Array.isArray(iconStyle) + ? [styles.icon, ...iconStyle] + : [styles.icon, iconStyle]; + + return ( + + {iconPosition === "left" ? ( + + ) : null} + {`${prefix}${formattedValue}`} + {iconPosition === "right" ? ( + + ) : null} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + alignItems: "center", + }, + value: { + fontFamily: FONT_FAMILY.InterSemiBold, + fontSize: 16, + color: "#fff", + }, + icon: { + width: 18, + height: 18, + }, +}); + +export default CreditAmount; diff --git a/src/components/MusicLandHeader.js b/src/components/MusicLandHeader.js index 940eb5a..f8825c7 100644 --- a/src/components/MusicLandHeader.js +++ b/src/components/MusicLandHeader.js @@ -11,6 +11,7 @@ const MusicLandHeader = ({ showSkip = false, progress = 1, logo = null, + style, }) => { const isWeb = Platform.OS === "web"; const backButtonStyle = isWeb @@ -34,7 +35,7 @@ const MusicLandHeader = ({ }; return ( - + )} - + {logo && ( + + )} ); }; diff --git a/src/components/Slider.js b/src/components/Slider.js index 2a935dc..a99b254 100644 --- a/src/components/Slider.js +++ b/src/components/Slider.js @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { StyleSheet, Text, View } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import Animated, { @@ -24,6 +24,7 @@ export default ({ const offset = useSharedValue(0); const boxWidth = useSharedValue(INITIAL_BOX_SIZE); const [layout, setLayout] = useState(null); + const seekingRef = useRef(false); const SLIDER_WIDTH = layout?.width; const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE; @@ -31,12 +32,34 @@ export default ({ const pan = Gesture.Pan() .enabled(seekEnabled) .onBegin(() => { - if (seekEnabled && typeof onSeekStart === "function") { - // Notify JS thread that user started seeking (e.g., pause audio) + if ( + seekEnabled && + typeof onSeekStart === "function" && + !seekingRef.current + ) { + seekingRef.current = true; + runOnJS(onSeekStart)(); + } + }) + .onStart(() => { + if ( + seekEnabled && + typeof onSeekStart === "function" && + !seekingRef.current + ) { + seekingRef.current = true; runOnJS(onSeekStart)(); } }) .onChange((event) => { + if ( + seekEnabled && + typeof onSeekStart === "function" && + !seekingRef.current + ) { + seekingRef.current = true; + runOnJS(onSeekStart)(); + } offset.value = Math.abs(offset.value) <= MAX_VALUE ? offset.value + event.changeX <= 0 @@ -50,14 +73,22 @@ export default ({ boxWidth.value = newWidth; }) .onEnd(() => { - if (!seekEnabled || !onSeek || !MAX_VALUE) return; - const ratio = - MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0; - // Reanimated -> JS thread bridge - runOnJS(onSeek)(ratio); + if (seekEnabled && typeof onSeek === "function" && MAX_VALUE) { + const ratio = + MAX_VALUE > 0 + ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) + : 0; + // Reanimated -> JS thread bridge + runOnJS(onSeek)(ratio); + } + if (seekingRef.current && typeof onSeekEnd === "function") { + seekingRef.current = false; + runOnJS(onSeekEnd)(); + } }) .onFinalize(() => { - if (seekEnabled && typeof onSeekEnd === "function") { + if (seekingRef.current && typeof onSeekEnd === "function") { + seekingRef.current = false; runOnJS(onSeekEnd)(); } }); diff --git a/src/components/modal/CoinPackModal.js b/src/components/modal/CoinPackModal.js index 41d9582..18df13f 100644 --- a/src/components/modal/CoinPackModal.js +++ b/src/components/modal/CoinPackModal.js @@ -1,7 +1,6 @@ import React from "react"; import { ActivityIndicator, - Image, Linking, Modal, Pressable, @@ -11,7 +10,6 @@ import { View, } from "react-native"; import { BlurView } from "expo-blur"; -import { icons } from "../../assets"; import BorderGradientButton from "../BorderGradientButton"; import GradientButton from "../GradientButton"; import CreateLyricsHeader from "../../screens/Writing/components/CreateLyricsHeader"; @@ -19,6 +17,7 @@ import { Palette, gutters } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import { getFunctionsClient } from "../../config/firebase"; import { isWeb } from "../../hooks/useLayoutType"; +import CreditAmount from "../CreditAmount"; const WEB_MODAL_MAX_WIDTH = 1000; const FUNCTIONS_REGION = "europe-west1"; @@ -73,10 +72,12 @@ function CoinPackCard({ pack, selected, onSelect }) { > - - - {pack?.coinAmount} pièces - + {pack?.name ? {pack.name} : null} {pack?.description ? ( @@ -395,11 +396,6 @@ const styles = StyleSheet.create({ alignItems: "center", gap: 10, }, - coinIcon: { - width: 26, - height: 26, - resizeMode: "contain", - }, coinAmount: { fontFamily: FONT_FAMILY.InterBold, fontSize: 20, diff --git a/src/layouts/Page.js b/src/layouts/Page.js index fb161fc..fd484da 100644 --- a/src/layouts/Page.js +++ b/src/layouts/Page.js @@ -4,9 +4,9 @@ import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view import { SafeAreaView } from "react-native-safe-area-context"; import React from "reactn"; import { responsiveHeight } from "../actions/responsiveSizes.js"; -import { icons, subBadges } from "../assets"; +import { subBadges } from "../assets"; import BaseHeader from "../components/BaseHeader"; -import CoinIcon from "../components/CoinIcon"; +import CreditAmount from "../components/CreditAmount"; import ConnectBtn from "../components/ConnectBtn.js"; import CoinPackModal from "../components/modal/CoinPackModal"; import NavigateHeader from "../components/NavigateHeader"; @@ -65,16 +65,6 @@ export default ({ return 0; }, [currentUserData?.coins]); - const formattedCoins = React.useMemo(() => { - try { - return new Intl.NumberFormat("fr-FR", { - maximumFractionDigits: 0, - }).format(coinBalance); - } catch (_error) { - return `${coinBalance}`; - } - }, [coinBalance]); - const showCoinBadge = isWeb && !!currentUID && showCoin; const [isCoinModalVisible, setCoinModalVisible] = React.useState(false); @@ -184,7 +174,6 @@ export default ({ flex: 1, minHeight: isWeb ? "100svh" : "100%", position: "relative", - // paddingHorizontal: isWeb ? gutters : 0, backgroundColor: backgroundColor, }} > @@ -192,7 +181,6 @@ export default ({ - - - {formattedCoins} - + /> {subscriptionBadgeSource ? ( { name={Routes.Playbacks} component={Playbacks} options={{ - tabBarLabel: "Playbacks", + tabBarLabel: "Playback", headerShown: false, tabBarShowLabel: true, tabBarIcon: ({ focused }) => renderIcon(tabs.mic, focused), diff --git a/src/navigation/Routes.js b/src/navigation/Routes.js index 4d059ec..4e3db8f 100644 --- a/src/navigation/Routes.js +++ b/src/navigation/Routes.js @@ -64,7 +64,7 @@ export const Routes = { Create: "Create", HitParade: "HitParade", - Playbacks: "Playbacks", + Playbacks: "Playback", Library: "Library", AllMyPlaylist: "AllMyPlaylist", diff --git a/src/providers/UserDataProvider.js b/src/providers/UserDataProvider.js index f6a2a77..767455c 100644 --- a/src/providers/UserDataProvider.js +++ b/src/providers/UserDataProvider.js @@ -12,10 +12,12 @@ import firebase, { playlistsRef, projectsRef, usersRef, + videosRef, } from "../config/firebase"; import { getUserPreferredArtistName } from "../utils/artistName"; import { getLikeFieldPath, LIKE_TARGET } from "../utils/likes"; import { ensureAuthenticated } from "../utils/authRedirect"; +import { isWeb } from "../hooks/useLayoutType"; const SONG_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.SONG); const PLAYBACK_LIKES_FIELD = getLikeFieldPath(LIKE_TARGET.PLAYBACK); @@ -43,20 +45,18 @@ export default ({ children }) => { }); // Subscribe to user's projects (musics) - const { - data: userProjects = [], - loading: userProjectsLoading = true, - } = useDataFromRef({ - ref: currentUID - ? projectsRef - .where("userId", "==", currentUID) - .orderBy("updatedAt", "desc") - : null, - simpleRef: false, - listener: true, - condition: !!currentUID, - refreshArray: [currentUID], - }); + const { data: userProjects = [], loading: userProjectsLoading = true } = + useDataFromRef({ + ref: currentUID + ? projectsRef + .where("userId", "==", currentUID) + .orderBy("updatedAt", "desc") + : null, + simpleRef: false, + listener: true, + condition: !!currentUID, + refreshArray: [currentUID], + }); const userPlaybacks = Array.isArray(userProjects) ? userProjects.filter((project) => project.playbackUrl) @@ -136,17 +136,17 @@ export default ({ children }) => { { selectedProjectId: projectId || null, }, - { merge: true } + { merge: true }, ); console.log("update selected project"); } catch (error) { console.log( "UserDataProvider: unable to persist selectedProjectId", - error?.message || error + error?.message || error, ); } }, - [currentUID] + [currentUID], ); const selectProject = useCallback( @@ -161,12 +161,12 @@ export default ({ children }) => { persistSelectedProjectId(safeId).catch((error) => { console.log( "UserDataProvider: persist selectedProjectId failed", - error?.message || error + error?.message || error, ); }); } }, - [currentUID, persistSelectedProjectId, setSelectedProject] + [currentUID, persistSelectedProjectId, setSelectedProject], ); const resetSelectedProject = useCallback(() => { @@ -183,7 +183,7 @@ export default ({ children }) => { ...partial, updatedAt: firebase.firestore.FieldValue.serverTimestamp(), }, - options + options, ); } catch (e) { console.log("updateProjectData error", e?.message); @@ -209,7 +209,7 @@ export default ({ children }) => { try { await setIsLoading(true); const artistDisplayName = getUserPreferredArtistName( - currentUserDoc || currentUserData || {} + currentUserDoc || currentUserData || {}, ); const payload = { userId: currentUID || null, @@ -238,7 +238,7 @@ export default ({ children }) => { followedBy: arrayUnion(currentUID), lastFollowersUpdateAt: new Date(), }, - { merge: true } + { merge: true }, ); setTooltip({ type: "success", text: "Abonnement mis à jour" }); } @@ -259,7 +259,7 @@ export default ({ children }) => { followedBy: arrayRemove(currentUID), lastFollowersUpdateAt: new Date(), }, - { merge: true } + { merge: true }, ); setTooltip({ type: "success", @@ -416,6 +416,11 @@ export default ({ children }) => { userProjects, ]); + const { data: videos } = useDataFromRef({ + ref: videosRef.doc("fr"), + simpleRef: true, + }); + return ( { setSelectedProjectId: selectProject, updateProjectData, createNewProject, + + videos, }} > {children} diff --git a/src/screens/HitParade/HitParade.js b/src/screens/HitParade/HitParade.js index 565910a..6c32d98 100644 --- a/src/screens/HitParade/HitParade.js +++ b/src/screens/HitParade/HitParade.js @@ -274,7 +274,7 @@ const HitParade = () => { gap: 12, }} > - {["Chansons", "Playbacks"].map((item, index) => ( + {["Chansons", "Playback"].map((item, index) => ( { {selected === "Chansons" && (isWeb ? renderSongsWeb() : renderSongsMobile())} - {selected === "Playbacks" && + {selected === "Playback" && (isWeb ? renderPlaybacksWeb() : renderPlaybacksMobile())} {/* "Clips" pourra reprendre la même logique si tu le réactives */} diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index 1e07e73..d78319c 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -8,15 +8,14 @@ import React, { import { StyleSheet, Text, View } from "react-native"; import { Image as ExpoImage } from "expo-image"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; -import { background, cardsImg, icons, videos } from "../../assets"; +import { background, cardsImg, icons } from "../../assets"; import Alert from "../../components/Alert"; import FullscreenIntroVideo from "../../components/FullscreenIntroVideo"; import GradientButton from "../../components/GradientButton"; import MoreMenu from "../../components/MoreMenu"; import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown"; import ShareBtn from "../../components/ShareBtn/ShareBtn"; -import { projectsRef, usersRef, videosRef } from "../../config/firebase"; -import useDataFromRef from "../../hooks/useDataFromRef"; +import { projectsRef, usersRef } from "../../config/firebase"; import { isWeb } from "../../hooks/useLayoutType.js"; import Page from "../../layouts/Page"; import LandingPage from "../LandingPage"; @@ -105,6 +104,7 @@ const Home = ({ navigation, route }) => { currentUserData, currentUID, createNewProject, + videos, } = useUser(); const { setTooltip } = useMinuit(); @@ -124,9 +124,7 @@ const Home = ({ navigation, route }) => { const statusCandidates = [ pickStatus(currentUserData?.stripeSubscriptionStatus), pickStatus(currentUserData?.stripeSubscription?.status), - pickStatus( - currentUserData?.stripeSubscription?.stripeSubscriptionStatus, - ), + pickStatus(currentUserData?.stripeSubscription?.stripeSubscriptionStatus), pickStatus(currentUserData?.stripeSubscription?.metadata?.status), ].filter(Boolean); @@ -410,11 +408,7 @@ const Home = ({ navigation, route }) => { const adventureStarted = hasLocalAdventureFlag || !!currentUserData?.adventureStarted; - const { data: video } = useDataFromRef({ - ref: videosRef.doc("fr"), - simpleRef: true, - }); - const videoUrl = isWeb ? video?.landingWeb : video?.landing; + const videoUrl = isWeb ? videos?.landingWeb : videos?.landing; const homeBackgroundImage = background.bgTrans; const landingBackgroundImage = homeBackgroundImage; @@ -447,12 +441,7 @@ const Home = ({ navigation, route }) => { } navigate(action.route, action.params); }, - [ - currentProject, - ensureProjectSelected, - hasActiveProject, - handleStartNew, - ], + [currentProject, ensureProjectSelected, hasActiveProject, handleStartNew], ); const handleClubPress = useCallback(() => { @@ -467,8 +456,8 @@ const Home = ({ navigation, route }) => { scrollEnabled={false} containerStyle={styles.page} contentContainerStyle={styles.pageContent} + maxWidth={1600} width="100%" - maxWidth={null} backgroundColor="#303438" > @@ -534,6 +523,8 @@ const Home = ({ navigation, route }) => { shareBtn title="Landing Page" backgroundImg={landingBackgroundImage} + contentContainerStyle={styles.pageContent} + backgroundColor="#303438" > { const isImageOnLeft = imagePosition !== "right"; const isTextRight = textAlign === "right"; @@ -22,16 +21,41 @@ const StageCard = ({ const imageBlock = ( @@ -40,14 +64,26 @@ const StageCard = ({ const header = ( @@ -61,16 +97,42 @@ const StageCard = ({ tint="dark" intensity={30} style={[ - styles.textContainer, - isImageOnLeft ? styles.textContainerRight : styles.textContainerLeft, - isTextRight ? styles.alignEnd : styles.alignStart, + { + flexGrow: 1, + flexShrink: 1, + maxWidth: 380, + paddingVertical: 20, + paddingHorizontal: 20, + justifyContent: "center", + alignSelf: "center", + }, + isImageOnLeft + ? { + borderTopRightRadius: 20, + borderBottomRightRadius: 20, + } + : { + borderTopLeftRadius: 20, + borderBottomLeftRadius: 20, + }, + isTextRight ? { alignItems: "flex-end" } : { alignItems: "flex-start" }, ]} > {isLocked ? ( @@ -79,8 +141,13 @@ const StageCard = ({ {header} {description} @@ -105,142 +172,21 @@ const StageCard = ({ onPress={onPress} disabled={isLocked} style={({ pressed }) => [ - styles.card, - pressed && !isLocked && styles.cardPressed, + { + width: "48%", + }, + pressed && !isLocked && { opacity: 0.85 }, ]} > - {content} + + {content} + ); }; export default memo(StageCard); - -const styles = StyleSheet.create({ - card: { - width: "48%", - minWidth: 260, - flexGrow: 0, - flexShrink: 0, - borderRadius: 20, - }, - cardPressed: { - opacity: 0.85, - }, - inner: { - flexDirection: "row", - alignItems: "stretch", - }, - imageContainer: { - flexGrow: 1, - flexShrink: 1, - minHeight: 200, - alignItems: "center", - justifyContent: "center", - overflow: "hidden", - }, - imageLeft: { - borderTopLeftRadius: 20, - borderBottomLeftRadius: 20, - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - }, - imageRight: { - borderTopRightRadius: 20, - borderBottomRightRadius: 20, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - }, - image: { - width: "85%", - height: "85%", - minHeight: 200, - borderRadius: 0, - }, - imageAlignRight: { - alignSelf: "flex-end", - }, - imageAlignLeft: { - alignSelf: "flex-start", - }, - textContainer: { - flexGrow: 1, - flexShrink: 1, - minWidth: 240, - maxWidth: 320, - minHeight: 120, - maxHeight: 160, - paddingVertical: 10, - paddingHorizontal: 16, - borderRadius: 18, - justifyContent: "center", - alignSelf: "center", - }, - textContainerRight: { - borderTopRightRadius: 18, - borderBottomRightRadius: 18, - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, - marginLeft: -32, - }, - textContainerLeft: { - borderTopLeftRadius: 18, - borderBottomLeftRadius: 18, - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - marginRight: -32, - }, - alignStart: { - alignItems: "flex-start", - }, - alignEnd: { - alignItems: "flex-end", - }, - textHeader: { - width: "100%", - flexDirection: "row", - alignItems: "center", - marginBottom: 8, - }, - textHeaderAlignStart: { - justifyContent: "flex-start", - }, - textHeaderAlignEnd: { - justifyContent: "flex-end", - }, - step: { - fontFamily: FONT_FAMILY.InterSemiBold, - fontSize: 14, - color: Palette.white, - flexShrink: 1, - }, - description: { - fontFamily: FONT_FAMILY.InterRegular, - fontSize: 13, - lineHeight: 20, - color: Palette.white, - }, - textLeft: { - textAlign: "left", - }, - textRight: { - textAlign: "right", - }, - lockBadge: { - position: "absolute", - top: 10, - backgroundColor: "rgba(0, 0, 0, 0.55)", - width: 32, - height: 32, - borderRadius: 16, - alignItems: "center", - justifyContent: "center", - zIndex: 2, - }, - lockBadgeLeft: { - left: 10, - }, - lockBadgeRight: { - right: 10, - }, -}); diff --git a/src/screens/Library/AllMyList.js b/src/screens/Library/AllMyList.js index 37ce556..172d41d 100644 --- a/src/screens/Library/AllMyList.js +++ b/src/screens/Library/AllMyList.js @@ -97,9 +97,9 @@ export default function AllMyList() { liked: { title: "Aucun playback liké pour le moment", description: - "Ajoutez vos playbacks favoris à vos likes pour les retrouver instantanément.", + "Ajoutez vos playback favoris à vos likes pour les retrouver instantanément.", cta: { - label: "Voir les playbacks", + label: "Voir les playback", action: () => navigate(Routes.Playbacks), }, }, @@ -110,7 +110,7 @@ export default function AllMyList() { }, [liked, scope]); const loadingMessage = useMemo(() => { - const target = scope === "playback" ? "playbacks" : "musiques"; + const target = scope === "playback" ? "playback" : "musiques"; return liked ? `Chargement des ${target} likés…` : `Chargement des ${target}…`; diff --git a/src/screens/Library/components/BackTracks.js b/src/screens/Library/components/BackTracks.js index 58f1221..2963b77 100644 --- a/src/screens/Library/components/BackTracks.js +++ b/src/screens/Library/components/BackTracks.js @@ -37,10 +37,10 @@ const BackTracks = () => { return ( navigate(Routes.AllMyList, { - title: "Mes playbacks", + title: "Mes playback", scope: "playback", liked: false, }) diff --git a/src/screens/Library/components/LikedPlayback.js b/src/screens/Library/components/LikedPlayback.js index a884d90..627d8f9 100644 --- a/src/screens/Library/components/LikedPlayback.js +++ b/src/screens/Library/components/LikedPlayback.js @@ -31,10 +31,10 @@ const LikedPlayback = () => { ); return ( navigate(Routes.AllMyList, { - title: "Playbacks likés", + title: "Playback likés", scope: "playback", liked: true, }) @@ -84,7 +84,7 @@ const LikedPlayback = () => { navigate(Routes.Playbacks)} - title="Voir les playbacks" + title="Voir les playback" > )} diff --git a/src/screens/Library/components/ResearchHeader.js b/src/screens/Library/components/ResearchHeader.js index 10111e1..a8d2f59 100644 --- a/src/screens/Library/components/ResearchHeader.js +++ b/src/screens/Library/components/ResearchHeader.js @@ -31,7 +31,7 @@ const ResearchHeader = ({ gap: 6, }} > - {["Musiques", "Playbacks", "Profils"].map((item, index) => ( + {["Musiques", "Playback", "Profils"].map((item, index) => ( onPress(item)}> 0 || playbacksLoading)) || - selected === "Playbacks"; + selected === "Playback"; const shouldShowUsers = (!selected && (users.length > 0 || usersLoading)) || selected === "Profils"; diff --git a/src/screens/Payments.js b/src/screens/Payments.js index a69b554..8c17224 100644 --- a/src/screens/Payments.js +++ b/src/screens/Payments.js @@ -11,6 +11,7 @@ import { useRoute } from "@react-navigation/native"; import { BlurView } from "expo-blur"; import { Image as ExpoImage } from "expo-image"; import GradientButton from "../components/GradientButton"; +import CreditAmount from "../components/CreditAmount"; import Page from "../layouts/Page"; import { background, subBadges } from "../assets"; import { Palette, gutters } from "../styles"; @@ -137,9 +138,16 @@ function SubscriptionCard({ plan, selected, onSelect }) { ) : null} {coinsPerMonth !== null ? ( - - {`+${coinsPerMonth} crédits / mois`} - + + + / mois + ) : null} ) : null} @@ -782,11 +790,21 @@ const styles = StyleSheet.create({ alignItems: "baseline", gap: 8, }, + coinsPerMonthRow: { + flexDirection: "row", + alignItems: "center", + gap: 4, + }, coinsPerMonth: { fontFamily: FONT_FAMILY.InterMedium, fontSize: 13, color: Palette.primary, }, + coinsPerMonthSuffix: { + fontFamily: FONT_FAMILY.InterMedium, + fontSize: 13, + color: Palette.primary, + }, priceValue: { fontFamily: FONT_FAMILY.InterBold, fontSize: 22, diff --git a/src/screens/Playback/RecordPlayback.js b/src/screens/Playback/RecordPlayback.js index 5c5c0b1..67d0be5 100644 --- a/src/screens/Playback/RecordPlayback.js +++ b/src/screens/Playback/RecordPlayback.js @@ -7,7 +7,7 @@ import React, { useRef, useState, } from "react"; -import { Text, View } from "react-native"; +import { Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import Svg, { Circle } from "react-native-svg"; import alert from "../../components/Alert"; @@ -30,6 +30,11 @@ const log = ? (...args) => console.log(LOG_PREFIX, ...args) : () => {}; +const CAMERA_FACING_OPTIONS = [ + { label: "Avant", value: "front" }, + { label: "Arrière", value: "back" }, +]; + const RecordPlayback = ({ route }) => { const { top, bottom } = useSafeAreaInsets(); const { project } = route.params || {}; @@ -63,6 +68,7 @@ const RecordPlayback = ({ route }) => { const [isRecording, setIsRecording] = useState(false); const [showProgress, setShowProgress] = useState(false); const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 }); + const [cameraFacing, setCameraFacing] = useState("front"); // Musique const songUrl = project?.songUrl || null; @@ -612,7 +618,7 @@ const RecordPlayback = ({ route }) => { @@ -626,6 +632,14 @@ const RecordPlayback = ({ route }) => { > + + + + {/* Overlay de compte à rebours : on affiche 5→1 pour éviter l'effet visuel à 1 */} {isPreparing && countdown >= 1 && !showProgress && ( @@ -836,3 +850,49 @@ const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => { ); }; + +const CameraFacingSelector = ({ value = "front", onChange, disabled = false }) => { + return ( + + {CAMERA_FACING_OPTIONS.map((option) => { + const isActive = option.value === value; + return ( + { + if (typeof onChange === "function") onChange(option.value); + }} + disabled={isActive} + style={{ + paddingVertical: 6, + paddingHorizontal: 14, + borderRadius: 999, + backgroundColor: isActive ? Palette.white : "transparent", + }} + > + + {option.label} + + + ); + })} + + ); +}; diff --git a/src/screens/Playback/RecordedPlayback.web.js b/src/screens/Playback/RecordedPlayback.web.js index ecb212b..069c4e9 100644 --- a/src/screens/Playback/RecordedPlayback.web.js +++ b/src/screens/Playback/RecordedPlayback.web.js @@ -1,6 +1,12 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Text, View } from "react-native"; -import { background } from "../../assets"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { Image, Pressable, Text, View } from "react-native"; +import { background, icons } from "../../assets"; import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; @@ -32,14 +38,22 @@ const RecordedPlayback = ({ route }) => { const songUrl = project?.songUrl || null; console.log("video uri is : ", videoUri); // AUDIO PLAYER (expo-audio → seconds) - const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, { - id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined, - title: typeof project?.title === "string" ? project.title : "Sans titre", - artist: typeof project?.userName === "string" ? project.userName : "MusicLand", - artwork: project?.coverUrl || null, - coverUrl: project?.coverUrl || null, - metadata: { projectId: project?.id, screen: "RecordedPlayback" }, - }); + const audioPlayer = useSharedAudioPlayer( + songUrl ? { uri: songUrl } : undefined, + { + id: project?.id + ? `recorded-${project.id}` + : songUrl + ? `recorded-${songUrl}` + : undefined, + title: typeof project?.title === "string" ? project.title : "Sans titre", + artist: + typeof project?.userName === "string" ? project.userName : "MusicLand", + artwork: project?.coverUrl || null, + coverUrl: project?.coverUrl || null, + metadata: { projectId: project?.id, screen: "RecordedPlayback" }, + }, + ); // Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source const videoElRef = useRef(null); @@ -149,8 +163,11 @@ const RecordedPlayback = ({ route }) => { }; const wasPlayingRef = useRef(false); - const onSeekStart = async () => { + const isSeekingRef = useRef(false); + const onSeekStart = useCallback(async () => { try { + if (isSeekingRef.current) return; + isSeekingRef.current = true; wasPlayingRef.current = !!audioPlayer?.playing; playbackEndedRef.current = false; if (audioPlayer?.playing) await audioPlayer.pause?.(); @@ -158,80 +175,151 @@ const RecordedPlayback = ({ route }) => { videoElRef.current.pause(); } } catch {} - }; - const onSeekEnd = async () => { + }, [audioPlayer]); + const onSeekEnd = useCallback(async () => { try { + if (!isSeekingRef.current) return; + isSeekingRef.current = false; if (wasPlayingRef.current) { if (audioPlayer) await audioPlayer.play?.(); if (videoElRef.current && videoUri) videoElRef.current.play().catch(() => {}); } } catch {} - }; + }, [audioPlayer, videoUri]); + + const handleTogglePlayback = useCallback(async () => { + try { + const duration = Number(progress?.durS || 0); + const position = Number(progress?.posS || 0); + const isAtEnd = duration > 0 && duration - position < 0.35; + const isCurrentlyPlaying = !!audioPlayer?.playing; + + if (isCurrentlyPlaying) { + playbackEndedRef.current = false; + await stopPlayback(); + return; + } + + if (isAtEnd) { + if (audioPlayer) await audioPlayer.seekTo?.(0); + if (videoElRef.current && videoUri) { + videoElRef.current.currentTime = 0; + } + } + + playbackEndedRef.current = false; + if (songUrl && audioPlayer) { + await audioPlayer.play?.(); + } + if (videoElRef.current && videoUri) { + videoElRef.current.muted = true; + videoElRef.current.play().catch(() => {}); + } + } catch {} + }, [audioPlayer, songUrl, stopPlayback, progress, videoUri]); return ( - - + + - - {/* Bloc vidéo optionnel si jamais tu as un videoUri sur web */} - {videoUri ? ( - + + )} + + ) : ( + // Placeholder quand pas de vidéo sur web + + Aperçu vidéo non disponible sur le web + + + )} + + + { /> + - + { + navigate(Routes.DownloadSongs, { + action: "playback", + uri: videoUri || null, // peut être null sur web + project, + }); }} - > - { - navigate(Routes.DownloadSongs, { - action: "playback", - uri: videoUri || null, // peut être null sur web - project, - }); - }} - /> - { - try { - await stopPlayback(); - } catch {} - navigate(Routes.RecordPlayback, { project }); - }} - /> - + /> + { + try { + await stopPlayback(); + } catch {} + navigate(Routes.RecordPlayback, { project }); + }} + /> ); diff --git a/src/screens/Playbacks/Playbacks.web.js b/src/screens/Playbacks/Playbacks.web.js index 3878187..951169a 100644 --- a/src/screens/Playbacks/Playbacks.web.js +++ b/src/screens/Playbacks/Playbacks.web.js @@ -9,6 +9,7 @@ import React, { import { FlatList, Image, + Pressable, StyleSheet, View, useWindowDimensions, @@ -85,7 +86,7 @@ const Playbacks = () => { loadMore?.(); } }, - [playbacks?.length, loadMore] + [playbacks?.length, loadMore], ); const syncBackgroundVideo = useCallback(({ currentTime, isPlaying }) => { @@ -193,7 +194,7 @@ const Playbacks = () => { handleActiveIndexChange(idx); } catch (_e) {} }, - [windowHeight, handleActiveIndexChange] + [windowHeight, handleActiveIndexChange], ); const onScroll = useCallback( @@ -204,7 +205,7 @@ const Playbacks = () => { handleActiveIndexChange(idx); } catch (_e) {} }, - [windowHeight, handleActiveIndexChange] + [windowHeight, handleActiveIndexChange], ); // programmatic jump to focus item @@ -228,7 +229,7 @@ const Playbacks = () => { offset: windowHeight * index, index, }), - [windowHeight] + [windowHeight], ); const total = playbacks.length; @@ -272,6 +273,47 @@ const Playbacks = () => { return `${activeIndex + 1}/${total}`; }, [total, activeIndex, hasMore, loading]); + const scrollToPlayback = useCallback( + (direction) => { + if (!direction || !listRef.current) return; + const totalItems = playbacks.length; + if (totalItems <= 0) return; + const targetIndex = Math.max( + 0, + Math.min(activeIndex + direction, totalItems - 1), + ); + if (targetIndex === activeIndex) { + if (direction > 0 && hasMore) { + loadMore?.(); + } + return; + } + const targetOffset = targetIndex * windowHeight; + try { + listRef.current.scrollToOffset({ + offset: targetOffset, + animated: true, + }); + } catch (_e) { + try { + listRef.current.scrollToIndex({ + index: targetIndex, + animated: true, + }); + } catch (__e) {} + } + handleActiveIndexChange(targetIndex); + }, + [ + activeIndex, + handleActiveIndexChange, + hasMore, + loadMore, + playbacks.length, + windowHeight, + ], + ); + return ( {activeVideoUrl ? ( @@ -324,16 +366,23 @@ const Playbacks = () => { scrollEventThrottle={16} /> {showIndicators && ( - - + + scrollToPlayback(-1)} + hitSlop={12} + disabled={!canScrollUp} + style={styles.indicatorArrowTouch} + > + + {indicatorItems.map((item, index) => ( { {/* {indicatorLabel ? ( {indicatorLabel} ) : null} */} - + scrollToPlayback(1)} + hitSlop={12} + disabled={!canScrollDown} + style={styles.indicatorArrowTouch} + > + + )} @@ -377,11 +433,8 @@ export default Playbacks; const styles = StyleSheet.create({ screen: { flex: 1, - // overscrollBehavior: "none", - // position: "relative", }, backgroundVideoWrapper: { - // ...StyleSheet.absoluteFillObject, height: "100%", width: "100%", position: "absolute", @@ -437,6 +490,10 @@ const styles = StyleSheet.create({ tintColor: Palette.white, opacity: 0.8, }, + indicatorArrowTouch: { + paddingVertical: 6, + paddingHorizontal: 8, + }, indicatorArrowUp: { transform: [{ rotate: "180deg" }], }, diff --git a/src/screens/Production/DownloadPrices.js b/src/screens/Production/DownloadPrices.js index 3a157ce..de3953b 100644 --- a/src/screens/Production/DownloadPrices.js +++ b/src/screens/Production/DownloadPrices.js @@ -26,14 +26,14 @@ const DownloadPrices = () => { price: "6,99€", }, { - title: `3 ${action === "playback" ? "Playbacks" : "chansons"}`, + title: `3 ${action === "playback" ? "Playback" : "chansons"}`, description: `Ce ${ action === "playback" ? "Playback" : "morceau" } et les 2 prochains que tu crées`, price: "12,99€", }, { - title: `5 ${action === "playback" ? "Playbacks" : "chansons"}`, + title: `5 ${action === "playback" ? "Playback" : "chansons"}`, description: `Ce ${ action === "playback" ? "Playback" : "morceau" } et les 4 prochains que tu crées`, diff --git a/src/screens/Production/StreamSong.js b/src/screens/Production/StreamSong.js index 7cfcb6e..1851e74 100644 --- a/src/screens/Production/StreamSong.js +++ b/src/screens/Production/StreamSong.js @@ -1,59 +1,101 @@ import { View, Text, Image } from "react-native"; -import React, { useState } from "react"; +import React, { useCallback, useMemo } from "react"; import Page from "../../layouts/Page"; import MusicLandHeader from "../../components/MusicLandHeader"; +import GradientButton from "../../components/GradientButton"; import { background, img } from "../../assets"; import { goBack, navigate } from "../../navigation/NavigationService"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; import { responsiveHeight } from "react-native-responsive-dimensions"; -import Style, { size } from "../../styles/Style"; +import { size } from "../../styles/Style"; import { Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import { Routes } from "../../navigation"; import { useRoute } from "@react-navigation/native"; +import { useUser } from "../../providers/UserDataProvider"; -const STREAM_PRICES = [ - { - title: "Je diffuse ma chanson", - description: "Ce morceau uniquement", - price: "1,99€ / mois", - }, - { - title: "Je diffuse 3 chansons", - description: "Ce morceau et les 2 prochains que tu crées", - price: "3,99€ / mois", - }, - { - title: "Illimité", - price: "14,99€ / mois", - recommended: true, - }, -]; +const ACTIVE_SUBSCRIPTION_STATUSES = new Set([ + "trialing", + "active", + "past_due", + "unpaid", +]); -const PLAYBACK_STREAM_PRICES = [ - { - title: "Je créer et diffuse mon Playback", - description: "Ce morceau uniquement", - price: "6,99€ / mois", - }, - { - title: "3 Playbacks", - description: "Ce morceau et les 2 prochains que tu crées", - price: "12,99€ / mois", - }, - { - title: "Illimité", - price: "20€ / mois", - recommended: true, - }, -]; +const pickStatus = (value) => { + if (typeof value !== "string") { + return null; + } + const trimmed = value.trim(); + return trimmed ? trimmed.toLowerCase() : null; +}; + +const hasActiveSubscription = (userData) => { + if (!userData) { + return false; + } + + const statusCandidates = [ + pickStatus(userData?.stripeSubscriptionStatus), + pickStatus(userData?.stripeSubscription?.status), + pickStatus(userData?.stripeSubscription?.stripeSubscriptionStatus), + pickStatus(userData?.stripeSubscription?.metadata?.status), + ].filter(Boolean); + + if ( + statusCandidates.some((status) => + ACTIVE_SUBSCRIPTION_STATUSES.has(status), + ) + ) { + return true; + } + + const premiumUntil = userData?.premiumUntil; + if (premiumUntil) { + const asDate = + premiumUntil?.toDate?.() || + (premiumUntil?.seconds ? new Date(premiumUntil.seconds * 1000) : null); + + if (asDate && asDate > new Date()) { + return true; + } + } + + if (userData?.premium?.active) { + return true; + } + + return false; +}; const StreamSong = () => { const params = useRoute().params; const action = params?.action; - const [selectedIndex, setSelectedIndex] = useState(null); - - const DATA = action === "playback" ? PLAYBACK_STREAM_PRICES : STREAM_PRICES; + const { currentUserData = null } = useUser() || {}; + const isPlaybackFlow = action === "playback"; + const userHasActiveSubscription = useMemo( + () => hasActiveSubscription(currentUserData), + [currentUserData], + ); + const headingText = isPlaybackFlow + ? "Publier ton playback" + : "Publier ta chanson"; + const descriptionText = userHasActiveSubscription + ? `Ton abonnement est actif, tu peux publier ${ + isPlaybackFlow ? "ce playback" : "cette chanson" + } sur MusicLand et participer au concours.` + : `Pour publier ${isPlaybackFlow ? "ton playback" : "ta chanson"} sur la plateforme, tu dois rejoindre le Club MusicLand.`; + const ctaLabel = userHasActiveSubscription + ? isPlaybackFlow + ? "Publier mon playback" + : "Publier ma chanson" + : "Rejoindre le Club MusicLand"; + const handlePrimaryAction = useCallback(() => { + if (userHasActiveSubscription) { + navigate(Routes.SongRelease, { action }); + return; + } + navigate(Routes.Payments); + }, [action, userHasActiveSubscription]); return ( { textAlign: "center", }} > - Diffuser ta chanson{"\n"} - Tarifs + {headingText} { textAlign: "center", }} > - Si tu désires diffuser ta chanson ou tes chansons sur la - plateforme de MusicLand et participer au concours et être dans - le top 3 voici le tarif. + {descriptionText} - - {DATA.map((item, index) => ( - { - setSelectedIndex(index); - navigate(Routes.SongRelease, { - action, - }); - }} - > - - - {item.title} - - {item.description && ( - - {item.description} - - )} - - - - {item.price} - - - {item.recommended && ( - - - Recommandé - - - )} - - ))} - + + + {!userHasActiveSubscription && ( + + Rejoins le club pour débloquer la publication de tes créations et + tenter de grimper dans le classement. + + )} + ); diff --git a/src/screens/Profile/ManageSubscription.js b/src/screens/Profile/ManageSubscription.js index 870d2ba..8ce2545 100644 --- a/src/screens/Profile/ManageSubscription.js +++ b/src/screens/Profile/ManageSubscription.js @@ -3,6 +3,7 @@ import React, { useCallback, useMemo, useState } from "react"; import { Alert, Platform, StyleSheet, Text, View } from "react-native"; import BorderGradientButton from "../../components/BorderGradientButton"; +import CreditAmount from "../../components/CreditAmount"; import { background } from "../../assets"; import { getFunctionsClient } from "../../config/firebase"; import Page from "../../layouts/Page"; @@ -44,19 +45,6 @@ const PERIOD_LABELS = { const PAGE_BACKGROUND_COLOR = "#303438"; -const formatCoinsAmount = (value) => { - if (typeof value !== "number" || !Number.isFinite(value)) { - return null; - } - try { - return new Intl.NumberFormat("fr-FR", { - maximumFractionDigits: 0, - }).format(value); - } catch (_error) { - return `${Math.round(value)}`; - } -}; - const getStatusColors = (status) => { switch (status) { case "trialing": @@ -384,11 +372,6 @@ const ManageSubscription = ({ navigation }) => { typeof coinsPerMonth === "number" && Number.isFinite(coinsPerMonth) ? Math.round(coinsPerMonth) : null; - const coinsPerMonthLabel = - normalizedCoins !== null - ? `${formatCoinsAmount(normalizedCoins)} coins` - : null; - const nextGrantTimestamp = currentUserData?.subscriptionNextGrantAt || currentUserData?.subscriptionGrantNextAt || @@ -434,11 +417,6 @@ const ManageSubscription = ({ navigation }) => { "Ton abonnement n'est plus actif. Tu peux souscrire à nouveau à tout moment.", ); } - if (isAnnual && coinsPerMonthLabel) { - helperMessages.push( - `Tes crédits sont versés chaque mois (${coinsPerMonthLabel}).`, - ); - } const helperMessage = helperMessages.join("\n"); return { @@ -462,7 +440,6 @@ const ManageSubscription = ({ navigation }) => { currentUserData?.stripeSubscription?.subscriptionId || null, coinsPerMonth: normalizedCoins, - coinsPerMonthLabel, isAnnual, nextGrantDate: resolvedNextGrantDate, nextGrantLabel, @@ -614,9 +591,15 @@ const ManageSubscription = ({ navigation }) => { Crédits mensuels - - {subscriptionInfo.coinsPerMonthLabel || "—"} - + {typeof subscriptionInfo.coinsPerMonth === "number" ? ( + + ) : ( + + )} {subscriptionInfo.isAnnual && subscriptionInfo.nextGrantLabel ? ( @@ -659,6 +642,20 @@ const ManageSubscription = ({ navigation }) => { {subscriptionInfo.helperMessage} ) : null} + {subscriptionInfo.isAnnual && + typeof subscriptionInfo.coinsPerMonth === "number" ? ( + + + Tes pièces sont versées chaque mois ( + + + ) + + ) : null} {successMessage ? ( @@ -767,6 +764,12 @@ const styles = StyleSheet.create({ lineHeight: 18, color: Palette.grayMid, }, + helperInlineRow: { + flexDirection: "row", + alignItems: "center", + flexWrap: "wrap", + gap: 4, + }, successMessage: { fontFamily: FONT_FAMILY.InterMedium, fontSize: 14, diff --git a/src/screens/Profile/OrderHistory.js b/src/screens/Profile/OrderHistory.js index 711bf64..f563554 100644 --- a/src/screens/Profile/OrderHistory.js +++ b/src/screens/Profile/OrderHistory.js @@ -13,6 +13,7 @@ import Page from "../../layouts/Page"; import { useUser } from "../../providers/UserDataProvider"; import { gutters, Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; +import CreditAmount from "../../components/CreditAmount"; const ORDER_TYPE_LABELS = { GIFT: "Crédit offert", @@ -21,19 +22,6 @@ const ORDER_TYPE_LABELS = { SUBSCRIPTION: "Abonnement", }; -const formatCoins = (amount) => { - if (typeof amount !== "number" || !Number.isFinite(amount)) { - return "0"; - } - try { - return new Intl.NumberFormat("fr-FR", { - maximumFractionDigits: 0, - }).format(amount); - } catch (_error) { - return `${amount}`; - } -}; - const formatDateParts = (date) => { if (!date) { return { @@ -133,9 +121,10 @@ const OrderHistory = () => { ? item.amount : 0; const isPositive = amountValue > 0; - const amountLabel = `${isPositive ? "+" : ""}${formatCoins(amountValue)} ${ - Math.abs(amountValue) === 1 ? "coin" : "coins" - }`; + const amountTextStyle = [ + styles.orderAmount, + isPositive ? styles.amountPositive : styles.amountNegative, + ]; const { dateLabel } = formatDateParts(item.createdAt); @@ -182,14 +171,12 @@ const OrderHistory = () => { return ( - - {amountLabel} - + {reasonLabel} {dateLabel} diff --git a/src/screens/Profile/Profile.js b/src/screens/Profile/Profile.js index 2a269a5..c92528a 100644 --- a/src/screens/Profile/Profile.js +++ b/src/screens/Profile/Profile.js @@ -508,7 +508,7 @@ const Profile = () => { }} > {/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/} - {["Chansons", "Playbacks"].map((item, index) => ( + {["Chansons", "Playback"].map((item, index) => ( onPressMenu(item)} @@ -540,7 +540,7 @@ const Profile = () => { ))} - {selected === "Playbacks" && ( + {selected === "Playback" && ( { - const [showIntro, setShowIntro] = useState(true); + const { videos } = useUserData(); + + const [videoUrl, setVideoUrl] = useState(null); + + useEffect(() => { + setVideoUrl(isWeb ? videos.malikWeb : videos?.malik); + }, []); return ( @@ -36,8 +44,9 @@ const Compose = () => { setShowIntro(false)} + visible={!!videoUrl} + url={videoUrl} + onClose={() => setVideoUrl(null)} /> ); diff --git a/src/screens/Studio/ComposeSong.js b/src/screens/Studio/ComposeSong.js index c015a2d..56d8d64 100644 --- a/src/screens/Studio/ComposeSong.js +++ b/src/screens/Studio/ComposeSong.js @@ -6,7 +6,7 @@ import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; import AppAlert from "../../components/Alert"; -import CoinIcon from "../../components/CoinIcon"; +import CreditAmount from "../../components/CreditAmount"; import firebase, { getFunctionsClient } from "../../config/firebase"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; @@ -65,16 +65,6 @@ const ComposeSong = () => { return 0; }, [currentUserData?.coins]); - 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: @@ -357,19 +347,18 @@ const ComposeSong = () => { textAlign: "center", }} > - Cette action coûte{" "} + Cette action coûte - - {MUSIC_GENERATION_COIN_COST} - - + iconSize={18} + /> { textAlign: "center", }} > - Solde disponible : {formattedCoinBalance} + Solde disponible : - + diff --git a/src/screens/Studio/ComposeSong.web.js b/src/screens/Studio/ComposeSong.web.js index 46b9fbf..63afe80 100644 --- a/src/screens/Studio/ComposeSong.web.js +++ b/src/screens/Studio/ComposeSong.web.js @@ -11,7 +11,7 @@ import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; import AppAlert from "../../components/Alert"; -import CoinIcon from "../../components/CoinIcon"; +import CreditAmount from "../../components/CreditAmount"; import firebase, { getFunctionsClient } from "../../config/firebase"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; @@ -68,16 +68,6 @@ const ComposeSong = () => { return 0; }, [currentUserData?.coins]); - 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: @@ -386,19 +376,18 @@ const ComposeSong = () => { textAlign: "center", }} > - Cette action coûte{" "} + Cette action coûte - - {MUSIC_GENERATION_COIN_COST} - - + iconSize={18} + /> { textAlign: "center", }} > - Solde disponible : {formattedCoinBalance} + Solde disponible : - + diff --git a/src/screens/Writing/CreatingLyrics.js b/src/screens/Writing/CreatingLyrics.js index 2e17f93..10bd25f 100644 --- a/src/screens/Writing/CreatingLyrics.js +++ b/src/screens/Writing/CreatingLyrics.js @@ -1,8 +1,7 @@ import { BlurView } from "expo-blur"; import React, { useEffect, useRef, useState } from "react"; -import { Image, Platform, Text, View } from "react-native"; +import { Platform, Text, View } from "react-native"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; -import { ai } from "../../assets"; import alert from "../../components/Alert"; import GradientButton from "../../components/GradientButton"; import ProgressBar from "../../components/ProgressBar"; @@ -275,7 +274,6 @@ const CreatingLyrics = ({ active, config, selections }) => { > { - + { - const { - createNewProject, - selectedProjectId, - updateProjectData, - } = useUserData(); + const { createNewProject, selectedProjectId, updateProjectData, videos } = + useUserData(); const { setIsLoading } = useMinuit(); + const [videoUrl, setVideoUrl] = useState(null); + + useEffect(() => { + setVideoUrl(isWeb ? videos.celineWeb : videos?.celine); + }, []); + const startWriting = React.useCallback(async () => { try { await setIsLoading(true); @@ -55,17 +59,9 @@ const WritingLyrics = () => { backgroundImg={isWeb ? background.libraryBgWeb : background.writingBG} headerType="NONE" > - {/* */} - - {/* - {strings.writing.onboarding.heroTitle} - - - {strings.writing.onboarding.heroSubtitle} - */} - + { + setVideoUrl(null)} + /> ); };