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 ` `;
@@ -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
-
-
- )}
-
+ >
+
+
+ )}
+
+ ) : (
+ // 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)}
+ />
);
};