diff --git a/firebase.json b/firebase.json index c9b7ca0..bea570e 100644 --- a/firebase.json +++ b/firebase.json @@ -6,7 +6,8 @@ "firebase-debug.log", "firebase-debug.*.log" ], - "predeploy": [] + "predeploy": [], + "source": "functions" }, "emulators": { "functions": { diff --git a/functions/package-lock.json b/functions/package-lock.json index 96d9f5d..0b889e5 100644 --- a/functions/package-lock.json +++ b/functions/package-lock.json @@ -7358,30 +7358,6 @@ "node": ">= 8" } }, - "node_modules/winston": { - "version": "3.18.3", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", - "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.8", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, "node_modules/winston-transport": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", @@ -7397,13 +7373,6 @@ "node": ">= 12.0.0" } }, - "node_modules/winston/node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT", - "optional": true - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/src/config/firebase.js b/src/config/firebase.js index 79f0074..197b960 100644 --- a/src/config/firebase.js +++ b/src/config/firebase.js @@ -50,12 +50,12 @@ if (!firebase?.apps?.filter(({ name_ }) => name_ === "[DEFAULT]").length) { persistence: getReactNativePersistence(AsyncStorage), }); - if (__DEV__) { - firebase.functions().useEmulator("localhost", 5001); - } + // if (__DEV__) { + // firebase.functions().useEmulator("localhost", 5001); + // configureFunctionsEmulator(defaultFunctions, "us-central1"); + // } const defaultFunctions = firebase.functions(); functionsInstances["us-central1"] = defaultFunctions; - configureFunctionsEmulator(defaultFunctions, "us-central1"); console.log("Firebase init"); } diff --git a/src/screens/HitParade/HitParade.js b/src/screens/HitParade/HitParade.js index 90cb36a..aafb3a6 100644 --- a/src/screens/HitParade/HitParade.js +++ b/src/screens/HitParade/HitParade.js @@ -60,10 +60,10 @@ const HitParade = () => { [topSongs] ); - const [playbacksCol1, playbacksCol2] = useMemo(() => { - const arr = Array.isArray(topPlaybacks) ? topPlaybacks : []; - return [arr.slice(0, 5), arr.slice(5)]; - }, [topPlaybacks]); + const playbackList = useMemo( + () => (Array.isArray(topPlaybacks) ? topPlaybacks : []), + [topPlaybacks] + ); const resolvePlaybackThumbnail = useCallback((project) => { const candidates = [ @@ -110,7 +110,7 @@ const HitParade = () => { gap: 10, paddingBottom: responsiveHeight(20), }} - data={Array.isArray(topPlaybacks) ? topPlaybacks : []} + data={playbackList} keyExtractor={(item) => item.id} renderItem={({ item, index }) => ( { ); const renderPlaybackColumns = () => ( - - - {playbacksCol1.map((item, idx) => ( - navigate(Routes.Playbacks, { projectId: item.id })} - /> - ))} - - - {playbacksCol2.length > 0 && ( - - {playbacksCol2.map((item, idx) => ( - navigate(Routes.Playbacks, { projectId: item.id })} - /> - ))} - - )} + + {playbackList.map((item, idx) => ( + navigate(Routes.Playbacks, { projectId: item.id })} + /> + ))} ); diff --git a/src/screens/LandingPage.js b/src/screens/LandingPage.js index 812ed28..d45260b 100644 --- a/src/screens/LandingPage.js +++ b/src/screens/LandingPage.js @@ -12,11 +12,10 @@ import { import { background, icons } from "../assets"; import FullscreenIntroVideo from "../components/FullscreenIntroVideo"; import GradientButton from "../components/GradientButton"; -import { LinearGradient } from "../components/LinearGradient/LinearGradient"; import { videosRef } from "../config/firebase"; import useDataFromRef from "../hooks/useDataFromRef"; import Page from "../layouts/Page"; -import { Routes } from "../navigation/Routes"; +import { Routes } from "../navigation"; import { useUser } from "../providers/UserDataProvider"; import { Palette } from "../styles"; import { FONT_FAMILY } from "../styles/Fonts"; @@ -154,7 +153,9 @@ export default function LandingPage() { title="Présentation de Musicland" onPress={() => setVideoUrl( - Platform.OS === "web" ? video?.landingWeb : video?.landing + Platform.OS === "web" + ? video?.landingWeb + : video?.landing, ) } containerStyle={styles.actionButton} diff --git a/src/screens/Production/PlaybackDownload.js b/src/screens/Production/PlaybackDownload.js index c6c49fe..cfb5435 100644 --- a/src/screens/Production/PlaybackDownload.js +++ b/src/screens/Production/PlaybackDownload.js @@ -14,6 +14,7 @@ import { background } from "../../assets"; import FullscreenIntroVideo from "../../components/FullscreenIntroVideo"; import BorderGradientButton from "../../components/BorderGradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; +import AppCheckbox from "../../components/AppCheckbox"; import firebase, { projectsRef, serverTimestamp, @@ -143,6 +144,7 @@ const PlaybackDownload = ({ route }) => { project?.playbackUrl || null, ); const [isPublishing, setIsPublishing] = useState(false); + const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true); const hasShownAfterPlaybackRef = useRef(false); const { data: video } = useDataFromRef({ @@ -284,6 +286,13 @@ const PlaybackDownload = ({ route }) => { const handlePublish = async () => { if (isPublishing) return; + if (!hasAcceptedPublication) { + setTooltip({ + type: "error", + text: "Confirme la diffusion sur Musicland et YouTube avant de publier", + }); + return; + } if (!project?.id) { if (action === "playback") { setTooltip({ @@ -429,6 +438,19 @@ const PlaybackDownload = ({ route }) => { + + + setHasAcceptedPublication((prevState) => !prevState) + } + label="J'accepte la diffusion de mon playback sur Musicland et YouTube." + /> + + Cette confirmation est requise avant de lancer la publication. + + + { @@ -438,6 +460,7 @@ const PlaybackDownload = ({ route }) => { setShowConfirmModal(true); } }} + disabled={isPublishing || !hasAcceptedPublication} containerStyle={styles.continueButton} /> @@ -491,6 +514,16 @@ const styles = StyleSheet.create({ clubCardSpacing: { marginTop: 10, }, + publishConsentContainer: { + gap: 6, + marginTop: 2, + }, + publishConsentDescription: { + fontFamily: FONT_FAMILY.InterRegular, + fontSize: 13, + color: Palette.white, + opacity: 0.8, + }, continueButton: { marginTop: 10, }, diff --git a/src/screens/Publishing/PublishYoutube.js b/src/screens/Publishing/PublishYoutube.js index 0db7e93..9038004 100644 --- a/src/screens/Publishing/PublishYoutube.js +++ b/src/screens/Publishing/PublishYoutube.js @@ -1,8 +1,9 @@ import { useRoute } from "@react-navigation/native"; import React, { useCallback, useMemo, useState } from "react"; -import { Linking, Text, View } from "react-native"; +import { Linking, StyleSheet, Text, View } from "react-native"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import { background } from "../../assets"; +import AppCheckbox from "../../components/AppCheckbox"; import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; @@ -37,6 +38,7 @@ const PublishYoutube = () => { const projectId = project?.id || routeProjectId || null; const [saving, setSaving] = useState(false); + const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true); const youtubeUrl = project?.youtubeUrl ?? null; const youtubeStatus = project?.youtubeStatus ?? null; const youtubeError = project?.youtubeError ?? null; @@ -50,7 +52,11 @@ const PublishYoutube = () => { [youtubeStatus] ); const publishDisabled = - saving || isPublishing || !playbackReady || !projectId; + saving || + isPublishing || + !playbackReady || + !projectId || + !hasAcceptedPublication; const publishButtonLabel = isPublishing ? "Publication en cours..." : hasYoutubePublication @@ -128,6 +134,13 @@ const PublishYoutube = () => { }, [playbackReady, projectId, setTooltip]); const handleConfirmPublish = useCallback(() => { + if (!hasAcceptedPublication) { + setTooltip?.({ + type: "error", + text: "Confirme la diffusion sur Musicland et YouTube avant de publier", + }); + return; + } if (publishDisabled) { return; } @@ -144,7 +157,13 @@ const PublishYoutube = () => { onPress: handleMarkPublished, }, ]); - }, [handleMarkPublished, hasYoutubePublication, publishDisabled]); + }, [ + handleMarkPublished, + hasAcceptedPublication, + hasYoutubePublication, + publishDisabled, + setTooltip, + ]); const handleResetPublication = useCallback(async () => { if (!projectId) { @@ -246,6 +265,18 @@ const PublishYoutube = () => { )} + + + setHasAcceptedPublication((prevState) => !prevState) + } + label="J'accepte la diffusion de mon contenu sur Musicland et YouTube." + /> + + Cette confirmation est requise avant toute mise en ligne. + + { ); }; +const styles = StyleSheet.create({ + consentContainer: { + gap: 6, + }, + consentDescription: { + fontSize: 13, + fontFamily: FONT_FAMILY.InterRegular, + color: Palette.white, + opacity: 0.8, + }, +}); + export default PublishYoutube; diff --git a/src/screens/Subscriptions.js b/src/screens/Subscriptions.js index 44bf884..94fbe16 100644 --- a/src/screens/Subscriptions.js +++ b/src/screens/Subscriptions.js @@ -70,9 +70,13 @@ const getIntervalLabel = (recurring) => { }; function SubscriptionCard({ plan, selected, onSelect }) { + const planKey = getPlanKeyForBadge(plan); const planName = - plan?.product?.name || plan?.nickname || plan?.priceId || "Abonnement"; - const planDescription = plan?.product?.description || ""; + (planKey && PLAN_DISPLAY_NAME_BY_KEY[planKey]) || + plan?.product?.name || + plan?.nickname || + plan?.priceId || + "Abonnement"; const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency); const intervalLabel = getIntervalLabel(plan?.recurring); const coinsPerMonth = @@ -80,21 +84,10 @@ function SubscriptionCard({ plan, selected, onSelect }) { Number.isFinite(plan.coinsPerMonth) ? Math.round(plan.coinsPerMonth) : null; - const songsPerMonth = - coinsPerMonth !== null - ? Math.floor(coinsPerMonth / CREDITS_PER_MUSIC) - : null; - const songsPerMonthLabel = - songsPerMonth === null - ? null - : songsPerMonth >= 1 - ? `≈ ${songsPerMonth} ${ - songsPerMonth > 1 ? "musiques" : "musique" - } / mois` - : "Moins d'une musique / mois"; - const planBadgeKey = getPlanKeyForBadge(plan); + const planBadgeKey = planKey; const planBadgeSource = planBadgeKey && subBadges[planBadgeKey] ? subBadges[planBadgeKey] : null; + const planFeatures = SUBSCRIPTION_FEATURES; const handleSelect = React.useCallback(() => { if (typeof onSelect === "function" && plan?.priceId) { onSelect(plan.priceId); @@ -138,14 +131,16 @@ function SubscriptionCard({ plan, selected, onSelect }) { ) : null} - {formattedPrice ? ( + {formattedPrice || coinsPerMonth !== null ? ( - - {formattedPrice} - {intervalLabel ? ( - {intervalLabel} - ) : null} - + {formattedPrice ? ( + + {formattedPrice} + {intervalLabel ? ( + {intervalLabel} + ) : null} + + ) : null} {coinsPerMonth !== null ? ( / mois ) : null} - {songsPerMonthLabel ? ( - {songsPerMonthLabel} - ) : null} ) : null} - {planDescription ? ( - {planDescription} + {planFeatures?.length ? ( + + {planFeatures.map((feature) => ( + + {feature} + + ))} + ) : null} @@ -174,11 +172,6 @@ function SubscriptionCard({ plan, selected, onSelect }) { } const PRICE_PRIORITY_BY_PERIOD = { - monthly: [ - "price_1SPgitCzf2o5bDRdbnhLFx6f", // Starter - "price_1SPgjCCzf2o5bDRdr08Xzp8u", // Pro - "price_1SPgjaCzf2o5bDRdd9Xo2u26", // Premium - ], annual: [ "price_1SPgkDCzf2o5bDRdNGLVNeQ3", // Starter "price_1SPgkXCzf2o5bDRdejBVxEBY", // Pro @@ -187,16 +180,9 @@ const PRICE_PRIORITY_BY_PERIOD = { }; const PACK_PRICE_ID_BY_PERIOD = { - monthly: { - starter: "price_1SPgitCzf2o5bDRdbnhLFx6f", - pro: "price_1SPgjCCzf2o5bDRdr08Xzp8u", - prop: "price_1SPgjCCzf2o5bDRdr08Xzp8u", - premium: "price_1SPgjaCzf2o5bDRdd9Xo2u26", - }, annual: { starter: "price_1SPgkDCzf2o5bDRdNGLVNeQ3", pro: "price_1SPgkXCzf2o5bDRdejBVxEBY", - prop: "price_1SPgkXCzf2o5bDRdejBVxEBY", premium: "price_1SPgkqCzf2o5bDRdIcUwTDrm", }, }; @@ -208,7 +194,7 @@ Object.values(PACK_PRICE_ID_BY_PERIOD).forEach((mapping) => { if (!priceId) { return; } - PRICE_ID_TO_PLAN_KEY[priceId] = planKey === "prop" ? "pro" : planKey; + PRICE_ID_TO_PLAN_KEY[priceId] = planKey; }); }); @@ -216,10 +202,22 @@ const PLAN_FALLBACK_ORDER = ["starter", "pro", "premium"]; const PLAN_SYNONYMS = { starter: ["starter"], - pro: ["pro", "prop"], + pro: ["pro"], premium: ["premium"], }; +const PLAN_DISPLAY_NAME_BY_KEY = { + starter: "Play Backer Blue", + pro: "Play Backer Silver", + premium: "Play Backer Gold", +}; + +const SUBSCRIPTION_FEATURES = [ + "*Eligible au Hit parade Vidéo", + "Privilège Membre : Récompense doublée", + "Eligible au hit parade Artiste trimestriel et annuel", +]; + const getPlanKeyForBadge = (plan) => { if (!plan || typeof plan !== "object") { return null; @@ -296,7 +294,6 @@ const PAGE_BACKGROUND_COLOR = "#303438"; const SUBSCRIPTION_DISCLAIMER = "Résiliable en un clic à tout moment. Les prix sont indiqués TTC. Contacte-nous pour des besoins spécifiques (facturation annuelle, volume, offres éducation)."; const CARD_MIN_HEIGHT = isWeb ? 320 : 240; -const CREDITS_PER_MUSIC = 8; export default function Subscriptions() { const route = useRoute(); @@ -456,8 +453,6 @@ export default function Subscriptions() { ); }, [combinedErrorMessage]); - const renderSegmentedControl = React.useCallback(() => null, []); - const renderMobilePlanItem = React.useCallback( ({ item }) => ( @@ -502,10 +497,9 @@ export default function Subscriptions() { return ( {renderHeaderSection()} - {renderSegmentedControl()} ); - }, [renderHeaderSection, renderSegmentedControl]); + }, [renderHeaderSection]); const renderMobileBottomActions = React.useCallback(() => { return ( @@ -606,8 +600,6 @@ export default function Subscriptions() { ) : null} - {renderSegmentedControl()} - {currentPlans.map((plan) => (