diff --git a/src/components/MobileCoinBadge.js b/src/components/MobileCoinBadge.js index b08989b..b17b31f 100644 --- a/src/components/MobileCoinBadge.js +++ b/src/components/MobileCoinBadge.js @@ -1,15 +1,14 @@ -import React, { useCallback, useMemo, useState } from 'react' +import React, { useCallback, useMemo } from 'react' import { Platform, Pressable, StyleSheet } from 'react-native' import CreditAmount from './CreditAmount' -import CoinPackModal from './modal/CoinPackModal' import { useUser } from '../providers/UserDataProvider' import { Palette } from '../styles' import { FONT_FAMILY } from '../styles/Fonts' -import { openCoinPackModal } from '../utils/coinPackModal' +import { navigate } from '../navigation/NavigationService' +import { Routes } from '../navigation/Routes' const MobileCoinBadge = ({ style, textStyle, iconSize = 20, iconPosition = 'left' }) => { const { currentUID, currentUserData } = useUser() || {} - const [isModalVisible, setModalVisible] = useState(false) const coinBalance = useMemo(() => { const value = currentUserData?.coins @@ -26,15 +25,7 @@ const MobileCoinBadge = ({ style, textStyle, iconSize = 20, iconPosition = 'left }, [currentUserData?.coins]) const handlePress = useCallback(() => { - if (Platform.OS === 'web') { - openCoinPackModal() - return - } - setModalVisible(true) - }, []) - - const handleClose = useCallback(() => { - setModalVisible(false) + navigate(Routes.Payments) }, []) if (!currentUID || Platform.OS === 'web') { @@ -42,18 +33,15 @@ const MobileCoinBadge = ({ style, textStyle, iconSize = 20, iconPosition = 'left } return ( - <> - - - - - + + + ) } diff --git a/src/components/modal/CoinPackModal.js b/src/components/modal/CoinPackModal.js index 3840828..63cb9d0 100644 --- a/src/components/modal/CoinPackModal.js +++ b/src/components/modal/CoinPackModal.js @@ -17,6 +17,11 @@ import { FONT_FAMILY } from '../../styles/Fonts' import { isWeb } from '../../hooks/useLayoutType' import CreditAmount from '../CreditAmount' import { useStripe } from '../../providers/StripeProvider' +import { + COIN_PACK_DETAILS, + COIN_PACK_SECTION_TITLE, + getCoinPackKey, +} from '../../utils/coinPackDisplay' const WEB_MODAL_MAX_WIDTH = 820 const formatCurrency = (amount, currency = 'eur') => { @@ -41,37 +46,10 @@ const formatCurrency = (amount, currency = 'eur') => { return `${normalized.toFixed(2)} ${upperCurrency}` } -const FALLBACK_BASE_PRICE_PER_COIN = 12 // ~0,12€ par jeton (valeur indicatif pour l'affichage) +const FALLBACK_BASE_PRICE_PER_COIN = 12 // ~0,12€ par credit (valeur indicative pour l'affichage) const FALLBACK_DISCOUNT_STEPS = [0, 12, 18, 26, 32] const STATIC_DISCOUNTS = [0, 30, 50] -const COIN_PACK_DETAILS = { - starter: { - label: 'Pack starter', - creditsDisplay: '50 Crédits', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, - medium: { - label: 'Pack Medium', - creditsDisplay: '100 Crédits + 50 offerts', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, - premium: { - label: 'Pack Premium', - creditsDisplay: '150 Crédits + 100 offerts', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, -} - -const getCoinPackKey = (pack) => { - const name = (pack?.name || '').toLowerCase() - if (name.includes('starter')) return 'starter' - if (name.includes('medium')) return 'medium' - if (name.includes('premium') || name.includes('prenium') || name.includes('creator')) - return 'premium' - return null -} - const computeCoinPackPricing = (packs = []) => { if (!Array.isArray(packs) || !packs.length) { return {} @@ -191,6 +169,8 @@ function CoinPackCard({ pack, selected, pricingDetail, onSelect, staticDiscountP const details = packKey ? COIN_PACK_DETAILS[packKey] : null const displayName = details?.label || pack.name const creditsText = details?.creditsDisplay + const displayPrice = details?.priceDisplay || formattedPrice + const isPopular = details?.popular === true return ( - {formattedPrice ? {formattedPrice} : null} + {displayPrice ? {displayPrice} : null} + {isPopular ? ( + + Le plus populaire ! + + ) : null} ) @@ -324,7 +309,7 @@ const CoinPackModal = ({ visible, onClose }) => { if (!coinPacks.length) { return ( - Aucun pack de pièces n'est disponible pour le moment. + Aucun pack de crédits n'est disponible pour le moment. ) } @@ -365,9 +350,9 @@ const CoinPackModal = ({ visible, onClose }) => { > - Acheter des pièces + {COIN_PACK_SECTION_TITLE} - Choisis un pack et finalise ton achat pour continuer. + Crée tes chansons et tes vidéos. @@ -378,8 +363,7 @@ const CoinPackModal = ({ visible, onClose }) => { {renderContent()} - Tarifs indicatifs : les prix et quantités de jetons sont amenés à évoluer. Les remises - sont affichées vs le pack de base pour mieux valoriser les offres volumineuses. + Téléchargement gratuit de tes créations. @@ -394,7 +378,7 @@ const CoinPackModal = ({ visible, onClose }) => { - Les pièces sont créditées dès que le paiement Stripe est validé. + Les crédits sont ajoutés dès que le paiement Stripe est validé. @@ -548,9 +532,24 @@ const styles = StyleSheet.create({ textAlign: 'center', }, priceBlock: { + marginTop: 'auto', gap: 2, alignItems: 'center', }, + popularBadge: { + alignSelf: 'center', + marginTop: 12, + paddingHorizontal: 12, + paddingVertical: 4, + borderRadius: 999, + backgroundColor: Palette.primary, + }, + popularBadgeText: { + fontFamily: FONT_FAMILY.InterBold, + fontSize: 11, + color: Palette.white, + textTransform: 'uppercase', + }, packPrice: { fontFamily: FONT_FAMILY.InterSemiBold, fontSize: 18, diff --git a/src/layouts/Page.js b/src/layouts/Page.js index 49d1788..d9381a8 100644 --- a/src/layouts/Page.js +++ b/src/layouts/Page.js @@ -122,7 +122,7 @@ export default ({ const handleCoinPress = React.useCallback(() => { if (!showCoinBadge) return - setCoinModalVisible(true) + navigate(Routes.Payments) }, [showCoinBadge]) const handleCloseCoinModal = React.useCallback(() => { @@ -144,10 +144,10 @@ export default ({ }, [showCoinBadge, isCoinModalVisible]) React.useEffect(() => { + if (!showCoinBadge) { + return undefined + } const unsubscribe = subscribeCoinPackModal(() => { - if (!showCoinBadge) { - return - } setCoinModalVisible(true) }) return unsubscribe diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index 10597d0..a7c8b9d 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -10,7 +10,6 @@ import FullscreenIntroVideo from '../../components/FullscreenIntroVideo' import GradientButton from '../../components/GradientButton' import MoreMenu from '../../components/MoreMenu' import ProjectDropDown from '../../components/ProjectDropDown/ProjectDropDown' -import CoinPackModal from '../../components/modal/CoinPackModal' import { projectsRef, usersRef } from '../../config/firebase' import { isWeb } from '../../hooks/useLayoutType.js' import Page from '../../layouts/Page' @@ -23,7 +22,6 @@ import ShareBtn from '../../components/ShareBtn/ShareBtn' import { Palette, gutters } from '../../styles' import { FONT_FAMILY } from '../../styles/Fonts' import { getCreationStageStates, getStageAction } from '../../utils/projectStages' -import { openCoinPackModal } from '../../utils/coinPackModal' import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails' import usePlayer from '../../hooks/usePlayer' import StageCard from './components/StageCard' @@ -407,8 +405,6 @@ const Home = ({ navigation, route }) => { [stageStatesByKey] ) - const [isCoinModalVisible, setCoinModalVisible] = useState(false) - const handleStagePress = useCallback( (stageKey, isLocked) => { if (isLocked) { @@ -434,19 +430,11 @@ const Home = ({ navigation, route }) => { ) const handleClubPress = useCallback(() => { - navigate(Routes.Payments) + navigate(Routes.HitParade) }, []) const handleOpenCoinModal = useCallback(() => { - if (isWeb) { - openCoinPackModal() - return - } - setCoinModalVisible(true) - }, []) - - const handleCloseCoinModal = useCallback(() => { - setCoinModalVisible(false) + navigate(Routes.Payments) }, []) const stageCardContainerStyle = isWeb ? styles.cardsGrid : styles.cardsStack @@ -652,10 +640,6 @@ const Home = ({ navigation, route }) => { - - {!isWeb ? ( - - ) : null} ) } diff --git a/src/screens/Production/PlaybackDownload.js b/src/screens/Production/PlaybackDownload.js index d13e843..c5d487f 100644 --- a/src/screens/Production/PlaybackDownload.js +++ b/src/screens/Production/PlaybackDownload.js @@ -6,7 +6,6 @@ import * as FileSystem from 'expo-file-system' import { shareAsync } from 'expo-sharing' import FullscreenIntroVideo from '../../components/FullscreenIntroVideo' import BorderGradientButton from '../../components/BorderGradientButton' -import GradientButton from '../../components/GradientButton' import MusicLandHeader from '../../components/MusicLandHeader' import AppCheckbox from '../../components/AppCheckbox' import firebase, { getFunctionsClient, projectsRef, serverTimestamp } from '../../config/firebase' @@ -178,7 +177,6 @@ const PlaybackDownload = ({ route }) => { const downloadLabel = canDownloadPlayback ? 'Télécharger le playback' : 'Acheter le playback pour 1,99€' - const showSubscriptionCta = !hasActiveSubscription const afterPlaybackUrl = useMemo(() => { if (!videos) return null @@ -434,55 +432,43 @@ const PlaybackDownload = ({ route }) => { - - {projectForDownload?.coverUrl ? ( - - ) : ( - - Aucune pochette + + {projectForDownload?.coverUrl ? ( + + ) : ( + + Aucune pochette + + )} + + + + + {downloadLabel} + + {!canDownloadPlayback && downloadPaymentPending ? ( + + Paiement en attente de confirmation... + + ) : null} + - )} - - - - {downloadLabel} - - {!canDownloadPlayback && downloadPaymentPending ? ( - - Paiement en attente de confirmation... - - ) : null} - - - {showSubscriptionCta ? ( - - - Avec un abonnement, le téléchargement est gratuit. - - navigate(Routes.Payments)} - containerStyle={styles.subscriptionButton} - /> - - ) : ( - - Avec ton abonnement, le téléchargement est gratuit. - - )} - - + } + /> { if (typeof amount !== 'number') { @@ -33,34 +38,7 @@ const formatCurrency = (amount, currency = 'eur') => { return `${normalized.toFixed(2)} ${upperCurrency}` } -const COIN_PACK_DETAILS = { - starter: { - label: 'Pack starter', - creditsDisplay: '50 Crédits', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, - medium: { - label: 'Pack Medium', - creditsDisplay: '100 Crédits + 50 offerts', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, - premium: { - label: 'Pack Premium', - creditsDisplay: '150 Crédits + 100 offerts', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, -} - -const getCoinPackKey = (pack) => { - const name = (pack?.name || '').toLowerCase() - if (name.includes('starter')) return 'starter' - if (name.includes('medium')) return 'medium' - if (name.includes('premium') || name.includes('prenium') || name.includes('creator')) - return 'premium' - return null -} - -const ClubAdvantagesCard = ({ style }) => { +const ClubAdvantagesCard = ({ style, topContent = null }) => { const navigation = useNavigation() const { hasActiveSubscription } = useUserData() || {} const { coinPacks, createCoinPackCheckout } = useStripe() @@ -91,16 +69,15 @@ const ClubAdvantagesCard = ({ style }) => { > - - OU - + {topContent ? {topContent} : null} + {'Les abonnements et les packs permettent le téléchargement de vos créations'} {'Avec un abonnement le téléchargement est gratuit'} {coinPacks && coinPacks.length > 0 ? ( - Recharger vos crédits + {COIN_PACK_SECTION_TITLE} {coinPacks.map((pack) => { const rawAmount = pack.unitAmount ?? pack.amount ?? pack.price ?? 0 @@ -110,6 +87,8 @@ const ClubAdvantagesCard = ({ style }) => { const details = packKey ? COIN_PACK_DETAILS[packKey] : null const displayName = details?.label || pack.name const creditsText = details?.creditsDisplay + const displayPrice = details?.priceDisplay || price || `${rawAmount / 100}€` + const isPopular = details?.popular === true return ( @@ -128,12 +107,13 @@ const ClubAdvantagesCard = ({ style }) => { {details.disclaimer} ) : null} + {isPopular ? ( + + Le plus populaire ! + + ) : null} handleBuyCredits(pack)} disabled={Boolean(processingPriceId)} style={styles.creditButton} @@ -179,6 +159,17 @@ const styles = StyleSheet.create({ gap: gutters * 0.3, alignItems: 'center', }, + topContentContainer: { + width: '100%', + alignItems: 'center', + marginBottom: gutters * 0.4, + }, + topDivider: { + width: '100%', + height: 1, + backgroundColor: 'rgba(255, 255, 255, 0.08)', + marginBottom: gutters * 0.8, + }, title: { fontFamily: FONT_FAMILY.InterSemiBold, fontSize: 18, @@ -196,8 +187,6 @@ const styles = StyleSheet.create({ gap: 16, width: '100%', alignItems: 'center', - borderTopWidth: 1, - borderTopColor: 'rgba(255, 255, 255, 0.08)', paddingTop: gutters, marginTop: gutters, paddingBottom: gutters, @@ -221,7 +210,7 @@ const styles = StyleSheet.create({ borderRadius: 16, padding: 16, alignItems: 'center', - justifyContent: 'space-between', + justifyContent: 'flex-start', width: isWeb ? 220 : '100%', minHeight: 160, gap: 12, @@ -257,6 +246,7 @@ const styles = StyleSheet.create({ marginTop: 4, }, creditButton: { + marginTop: 'auto', width: '100%', height: 36, minHeight: 36, @@ -264,6 +254,20 @@ const styles = StyleSheet.create({ creditButtonText: { fontSize: 13, }, + popularBadge: { + alignSelf: 'center', + marginTop: 12, + paddingHorizontal: 12, + paddingVertical: 4, + borderRadius: 999, + backgroundColor: Palette.primary, + }, + popularBadgeText: { + fontFamily: FONT_FAMILY.InterBold, + fontSize: 11, + color: Palette.white, + textTransform: 'uppercase', + }, errorText: { color: Palette.red, textAlign: 'center', diff --git a/src/screens/Subscriptions.js b/src/screens/Subscriptions.js index 0e84598..8ce636b 100644 --- a/src/screens/Subscriptions.js +++ b/src/screens/Subscriptions.js @@ -7,7 +7,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context' import GradientButton from '../components/GradientButton' import CreditAmount from '../components/CreditAmount' import Page from '../layouts/Page' -import { background, icons, planBadges, subBadges } from '../assets' +import { background, icons, planBadges } from '../assets' import { Palette, gutters } from '../styles' import { FONT_FAMILY } from '../styles/Fonts' import { isWeb } from '../hooks/useLayoutType' @@ -15,6 +15,18 @@ import { useStripe } from '../providers/StripeProvider' import { useUserData } from '../providers/UserDataProvider' import { goBack, navigate } from '../navigation/NavigationService' import { Routes } from '../navigation/Routes' +import { + COIN_PACK_DETAILS, + COIN_PACK_SECTION_TITLE, + getCoinPackKey, +} from '../utils/coinPackDisplay' +import { + getSubscriptionCardDisplay, + getSubscriptionPlanKey, + SUBSCRIPTION_PLAN_ORDER, + SUBSCRIPTION_PLAN_SYNONYMS, + SUBSCRIPTION_PRICE_ID_BY_PERIOD, +} from '../utils/subscriptionCardDisplay' const formatCurrency = (amount, currency = 'eur') => { if (typeof amount !== 'number') { @@ -63,21 +75,22 @@ const getIntervalLabel = (recurring) => { } function SubscriptionCard({ plan, selected, onSelect, isAnnual }) { - const planKey = getPlanKeyForBadge(plan) - const planName = - (planKey && PLAN_DISPLAY_NAME_BY_KEY[planKey]) || - plan?.product?.name || - plan?.nickname || - plan?.priceId || - 'Abonnement' + const planKey = getSubscriptionPlanKey(plan) const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency) const intervalLabel = getIntervalLabel(plan?.recurring) const coinsPerMonth = typeof plan?.coinsPerMonth === 'number' && Number.isFinite(plan.coinsPerMonth) ? Math.round(plan.coinsPerMonth) : null - const details = planKey ? PLAN_DETAILS[planKey] : null - const isPopular = details?.popular + const cardDisplay = getSubscriptionCardDisplay({ + planKey, + isAnnual, + formattedPrice, + intervalLabel, + coinsPerMonth, + fallbackTitle: plan?.product?.name || plan?.nickname || plan?.priceId || 'Abonnement', + }) + const isPopular = cardDisplay.popular const planBadgeKey = planKey const getPlanBadge = (plan) => { @@ -92,7 +105,6 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) { return null } } - const planBadgeSource = planBadgeKey && subBadges[planBadgeKey] ? subBadges[planBadgeKey] : null const handleSelect = React.useCallback(() => { if (typeof onSelect === 'function' && plan?.priceId) { onSelect(plan.priceId) @@ -124,61 +136,41 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) { - {planName} + {cardDisplay.title} {getPlanBadge(planKey)} - {plan?.nickname ? {plan.nickname} : null} - {formattedPrice || coinsPerMonth !== null ? ( + {cardDisplay.primaryPrice || cardDisplay.creditsLabel ? ( - {formattedPrice ? ( - - {formattedPrice} - {intervalLabel ? {intervalLabel} : null} - + {cardDisplay.primaryPrice ? ( + {cardDisplay.primaryPrice} ) : null} - - {details?.creditsLabel ? ( - {details.creditsLabel} - ) : coinsPerMonth !== null ? ( - - - / mois - + {cardDisplay.secondaryPrice ? ( + {cardDisplay.secondaryPrice} + ) : null} + {cardDisplay.creditsLabel ? ( + {cardDisplay.creditsLabel} ) : null} ) : null} - - Privilège Membre : - {details?.privileges?.map((item, index) => ( - - - {item.bold ? `• ${item.text}` : item.text} - - {item.subItems ? ( - - {item.subItems.join(' - ')} - - ) : null} - - )) || ( - <> - - • Eligible au concours mensuel chanson/Vidéo + {Array.isArray(cardDisplay.benefitItems) && cardDisplay.benefitItems.length ? ( + + {cardDisplay.benefitItems.map((item, index) => + item?.type === 'plus' ? ( + + {item.text} - • Crédits gratuits tous les mois - + ) : ( + + {item.text} + + ) )} - + + ) : null} {isPopular ? ( @@ -187,11 +179,6 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) { ) : null} - {isAnnual ? ( - - 2 mois offert - - ) : null} ) } @@ -209,125 +196,6 @@ const PRICE_PRIORITY_BY_PERIOD = { ], } -const PACK_PRICE_ID_BY_PERIOD = { - monthly: { - starter: 'price_1SPgitCzf2o5bDRdbnhLFx6f', - pro: 'price_1SPgjCCzf2o5bDRdr08Xzp8u', - premium: 'price_1SPgjaCzf2o5bDRdd9Xo2u26', - }, - annual: { - starter: 'price_1SPgkDCzf2o5bDRdNGLVNeQ3', - pro: 'price_1SPgkXCzf2o5bDRdejBVxEBY', - premium: 'price_1SPgkqCzf2o5bDRdIcUwTDrm', - }, -} - -const PRICE_ID_TO_PLAN_KEY = {} - -Object.values(PACK_PRICE_ID_BY_PERIOD).forEach((mapping) => { - Object.entries(mapping).forEach(([planKey, priceId]) => { - if (!priceId) { - return - } - PRICE_ID_TO_PLAN_KEY[priceId] = planKey - }) -}) - -const PLAN_FALLBACK_ORDER = ['starter', 'pro', 'premium'] - -const PLAN_SYNONYMS = { - starter: ['starter'], - pro: ['pro'], - premium: ['premium'], -} - -const PLAN_DISPLAY_NAME_BY_KEY = { - starter: 'Play Backer Blue', - pro: 'Play Backer Silver', - premium: 'Play Backer Gold', -} - -const COIN_PACK_DETAILS = { - starter: { - label: 'Pack starter', - creditsDisplay: '50 Crédits', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, - medium: { - label: 'Pack Medium', - creditsDisplay: '100 Crédits + 50 offerts', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, - premium: { - label: 'Pack Premium', - creditsDisplay: '150 Crédits + 100 offerts', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, -} - -const getCoinPackKey = (pack) => { - const name = (pack?.name || '').toLowerCase() - if (name.includes('starter')) return 'starter' - if (name.includes('medium')) return 'medium' - if (name.includes('premium') || name.includes('prenium') || name.includes('creator')) - return 'premium' - return null -} - -const PLAN_DETAILS = { - starter: { - creditsLabel: '50 Crédits / Mois', - popular: false, - privileges: [ - { text: 'Eligible au concours mensuel chanson/Vidéo', bold: true }, - { text: 'Catégorie Chanson :', subItems: ['Top 1: 500 €', 'Top 2: 250 €', 'Top 3: 125 €'] }, - { text: 'Catégorie Vidéo :', subItems: ['Top 1: 1000 €', 'Top 2: 500 €', 'Top 3: 250 €'] }, - ], - }, - pro: { - creditsLabel: '100 crédits + 50 offerts / Mois', - popular: true, - privileges: [ - { text: 'Eligible au concours mensuel chanson/Vidéo', bold: true }, - { text: 'Catégorie Chanson :', subItems: ['Top 1: 500 €', 'Top 2: 250 €', 'Top 3: 125 €'] }, - { text: 'Catégorie Vidéo :', subItems: ['Top 1: 1000 €', 'Top 2: 500 €', 'Top 3: 250 €'] }, - ], - }, - premium: { - creditsLabel: '150 Crédits + 100 offerts / Mois', - popular: false, - privileges: [ - { text: 'Eligible au concours mensuel chanson/Vidéo', bold: true }, - { text: 'Catégorie Chanson :', subItems: ['Top 1: 500 €', 'Top 2: 250 €', 'Top 3: 125 €'] }, - { text: 'Catégorie Vidéo :', subItems: ['Top 1: 1000 €', 'Top 2: 500 €', 'Top 3: 250 €'] }, - ], - }, -} - -const getPlanKeyForBadge = (plan) => { - if (!plan || typeof plan !== 'object') { - return null - } - - const priceId = typeof plan?.priceId === 'string' ? plan.priceId : plan?.id || null - - if (priceId && PRICE_ID_TO_PLAN_KEY[priceId]) { - return PRICE_ID_TO_PLAN_KEY[priceId] - } - - const label = (plan?.product?.name || plan?.nickname || '').toString().toLowerCase() - - if (!label) { - return null - } - - const match = Object.entries(PLAN_SYNONYMS).find(([, variants]) => - variants.some((variant) => label.includes(variant)) - ) - - return match ? match[0] : null -} - const getPlanPriority = (plan, periodKey) => { const priceId = typeof plan?.priceId === 'string' ? plan.priceId : plan?.id || null const priceOrder = PRICE_PRIORITY_BY_PERIOD[periodKey] || [] @@ -341,13 +209,15 @@ const getPlanPriority = (plan, periodKey) => { const label = (plan?.product?.name || plan?.nickname || '').toString().toLowerCase() - const fallbackIndex = PLAN_FALLBACK_ORDER.findIndex((candidate) => { - const variants = PLAN_SYNONYMS[candidate] || [candidate] + const fallbackIndex = SUBSCRIPTION_PLAN_ORDER.findIndex((candidate) => { + const variants = SUBSCRIPTION_PLAN_SYNONYMS[candidate] || [candidate] return variants.some((variant) => label.includes(variant)) }) const baseOffset = priceOrder.length - return fallbackIndex === -1 ? baseOffset + PLAN_FALLBACK_ORDER.length : baseOffset + fallbackIndex + return fallbackIndex === -1 + ? baseOffset + SUBSCRIPTION_PLAN_ORDER.length + : baseOffset + fallbackIndex } const normalizePlans = (plans = [], periodKey) => { @@ -390,7 +260,7 @@ export default function Subscriptions() { createCoinPackCheckout, } = useStripe() const { currentUserData } = useUserData() - const [selectedPeriodKey, setSelectedPeriodKey] = React.useState('annual') + const [selectedPeriodKey, setSelectedPeriodKey] = React.useState('monthly') const [selectedPriceId, setSelectedPriceId] = React.useState(null) const [processingPriceId, setProcessingPriceId] = React.useState(null) const [errorMessage, setErrorMessage] = React.useState(null) @@ -438,13 +308,13 @@ export default function Subscriptions() { let nextPeriodKey = selectedPeriodKey // Only force switch if current selected period is empty and another is not if ((normalizedPlansByPeriod[selectedPeriodKey]?.length || 0) === 0) { - nextPeriodKey = periodEntries[0]?.[0] || 'annual' + nextPeriodKey = periodEntries[0]?.[0] || 'monthly' } if (shouldApplyPack) { const desired = initialSubscriptionPack.trim().toLowerCase() for (const [periodKey, plans] of periodEntries) { - const candidateId = PACK_PRICE_ID_BY_PERIOD?.[periodKey]?.[desired] + const candidateId = SUBSCRIPTION_PRICE_ID_BY_PERIOD?.[periodKey]?.[desired] if (candidateId) { const exists = plans.some((plan) => plan?.priceId === candidateId) if (exists) { @@ -772,7 +642,7 @@ export default function Subscriptions() { {coinPacks && coinPacks.length > 0 ? ( - Recharger vos crédits + {COIN_PACK_SECTION_TITLE} {coinPacks.map((pack) => { // Attempt to find amount in commonly used Stripe fields @@ -785,6 +655,8 @@ export default function Subscriptions() { const details = packKey ? COIN_PACK_DETAILS[packKey] : null const displayName = details?.label || pack.name const creditsText = details?.creditsDisplay + const displayPrice = details?.priceDisplay || price || `${rawAmount / 100}€` + const isPopular = details?.popular === true return ( @@ -803,12 +675,13 @@ export default function Subscriptions() { {details.disclaimer} ) : null} + {isPopular ? ( + + Le plus populaire ! + + ) : null} handleBuyCredits(pack)} disabled={Boolean(processingPriceId)} style={styles.creditButton} @@ -933,6 +806,19 @@ const styles = StyleSheet.create({ paddingTop: 12, gap: 6, }, + downloadLabel: { + fontFamily: FONT_FAMILY.InterMedium, + fontSize: 14, + color: Palette.white, + textAlign: 'center', + }, + benefitPlus: { + fontFamily: FONT_FAMILY.InterBold, + fontSize: 22, + color: Palette.white, + textAlign: 'center', + lineHeight: 24, + }, cardBenefitsItem: { fontFamily: FONT_FAMILY.InterMedium, fontSize: 12, @@ -1008,7 +894,7 @@ const styles = StyleSheet.create({ borderRadius: 16, padding: 16, alignItems: 'center', - justifyContent: 'space-between', + justifyContent: 'flex-start', width: isWeb ? 200 : '45%', gap: 12, borderWidth: 1, @@ -1043,6 +929,7 @@ const styles = StyleSheet.create({ fontSize: 20, }, creditButton: { + marginTop: 'auto', width: '100%', height: 36, minHeight: 36, @@ -1207,11 +1094,12 @@ const styles = StyleSheet.create({ paddingHorizontal: gutters, paddingVertical: isWeb ? gutters : Math.max(gutters * 0.2, 10), gap: isWeb ? 18 : 10, - justifyContent: 'center', + justifyContent: 'flex-start', backgroundColor: 'rgba(48, 52, 56, 0.55)', borderRadius: 24, }, cardContent: { + flex: 1, gap: isWeb ? 16 : 4, }, cardHeader: { @@ -1244,6 +1132,11 @@ const styles = StyleSheet.create({ priceBlock: { gap: isWeb ? 4 : 1, }, + secondaryPriceText: { + fontFamily: FONT_FAMILY.InterBold, + fontSize: 16, + color: Palette.primary, + }, priceRow: { flexDirection: 'row', alignItems: 'baseline', @@ -1289,7 +1182,7 @@ const styles = StyleSheet.create({ }, popularBadge: { alignSelf: 'center', - marginTop: 12, + marginTop: 'auto', paddingHorizontal: 12, paddingVertical: 4, borderRadius: 999, diff --git a/src/screens/cover/SongDownload.js b/src/screens/cover/SongDownload.js index 59d2944..5f807b3 100644 --- a/src/screens/cover/SongDownload.js +++ b/src/screens/cover/SongDownload.js @@ -112,27 +112,37 @@ const SongDownload = ({ route }) => { const canDownloadDirectly = hasActiveSubscription || hasPaidDownload || hasPurchased const downloadLabel = canDownloadDirectly ? 'Télécharger mon morceau' - : 'Acheter ce morceau pour 1,99€' - const showSubscriptionCta = !hasActiveSubscription + : 'Télécharger ce morceau pour 1,99€' const shouldShowDownloadPage = adventureChoice === 'stop' + const reopenAdventureChoice = useCallback(() => { + reopenAdventureModalOnFocusRef.current = false + setShowIntro(null) + setAdventureChoice('pending') + setShowAdventureModal(true) + }, []) + const handleBackPress = useCallback(() => { + if (shouldShowDownloadPage) { + reopenAdventureChoice() + return true + } if (backRoute) { navigate(backRoute) return true } goBack() return true - }, [backRoute]) + }, [backRoute, reopenAdventureChoice, shouldShowDownloadPage]) useFocusEffect( useCallback(() => { - if (Platform.OS !== 'android' || !backRoute) { + if (Platform.OS !== 'android' || (!backRoute && !shouldShowDownloadPage)) { return undefined } const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress) return () => subscription.remove() - }, [backRoute, handleBackPress]) + }, [backRoute, handleBackPress, shouldShowDownloadPage]) ) useFocusEffect( @@ -512,8 +522,8 @@ const SongDownload = ({ route }) => { } if (promptPurchaseConfirm) { Alert.alert( - 'Acheter ce morceau', - 'Voulez-vous acheter ce morceau pour 1,99€ ?', + 'Télécharger ce morceau', + 'Voulez-vous télécharger ce morceau pour 1,99€ ?', [ { text: 'Annuler', style: 'cancel' }, { text: 'Acheter', onPress: () => startSongDownloadCheckout() }, @@ -569,38 +579,39 @@ const SongDownload = ({ route }) => { showsVerticalScrollIndicator={false} > - - {coverUrl ? ( - - ) : ( - - Aucune pochette + + {coverUrl ? ( + + ) : ( + + Aucune pochette + + )} + + + + + {downloadLabel} + + {!canDownloadDirectly && downloadPaymentPending ? ( + + Paiement en attente de confirmation... + + ) : null} + - )} - - - - - {downloadLabel} - - {!canDownloadDirectly && downloadPaymentPending ? ( - - Paiement en attente de confirmation... - - ) : null} - - - - - - + } + /> { return `tous les ${count} ${count > 1 ? terms.plural : terms.singular}` } -const PLAN_SYNONYMS = { - starter: ['starter'], - pro: ['pro'], - premium: ['premium'], -} - -const PLAN_DISPLAY_NAME_BY_KEY = { - starter: 'Play Backer Blue', - pro: 'Play Backer Silver', - premium: 'Play Backer Gold', -} - -const PACK_PRICE_ID_BY_PERIOD = { - monthly: { - starter: 'price_1SPgitCzf2o5bDRdbnhLFx6f', - pro: 'price_1SPgjCCzf2o5bDRdr08Xzp8u', - premium: 'price_1SPgjaCzf2o5bDRdd9Xo2u26', - }, - annual: { - starter: 'price_1SPgkDCzf2o5bDRdNGLVNeQ3', - pro: 'price_1SPgkXCzf2o5bDRdejBVxEBY', - premium: 'price_1SPgkqCzf2o5bDRdIcUwTDrm', - }, -} - -const PRICE_ID_TO_PLAN_KEY = {} -Object.values(PACK_PRICE_ID_BY_PERIOD).forEach((mapping) => { - Object.entries(mapping).forEach(([planKey, priceId]) => { - if (!priceId) return - PRICE_ID_TO_PLAN_KEY[priceId] = planKey - }) -}) - -const PLAN_DETAILS = { - starter: { - creditsLabel: '50 Crédits / Mois', - popular: false, - privileges: [ - { text: 'Eligible au concours mensuel chanson/Vidéo', bold: true }, - ], - }, - pro: { - creditsLabel: '100 crédits + 50 offerts / Mois', - popular: true, - privileges: [ - { text: 'Eligible au concours mensuel chanson/Vidéo', bold: true }, - ], - }, - premium: { - creditsLabel: '150 Crédits + 100 offerts / Mois', - popular: false, - privileges: [ - { text: 'Eligible au concours mensuel chanson/Vidéo', bold: true }, - ], - }, -} - -const COIN_PACK_DETAILS = { - starter: { - label: 'Pack starter', - creditsDisplay: '50 Crédits', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, - medium: { - label: 'Pack Medium', - creditsDisplay: '100 Crédits + 50 offerts', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, - premium: { - label: 'Pack Premium', - creditsDisplay: '150 Crédits + 100 offerts', - disclaimer: '*Non-Eligible au concours Chanson/Vidéo', - }, -} - -const getCoinPackKey = (pack) => { - const name = (pack?.name || '').toLowerCase() - if (name.includes('starter')) return 'starter' - if (name.includes('medium')) return 'medium' - if (name.includes('premium') || name.includes('prenium') || name.includes('creator')) - return 'premium' - return null -} - -const getPlanKeyForBadge = (plan) => { - if (!plan || typeof plan !== 'object') return null - const priceId = typeof plan?.priceId === 'string' ? plan.priceId : plan?.id || null - if (priceId && PRICE_ID_TO_PLAN_KEY[priceId]) { - return PRICE_ID_TO_PLAN_KEY[priceId] - } - const label = (plan?.product?.name || plan?.nickname || '').toString().toLowerCase() - if (!label) return null - const match = Object.entries(PLAN_SYNONYMS).find(([, variants]) => - variants.some((variant) => label.includes(variant)) - ) - return match ? match[0] : null -} - // --- SUBSCRIPTION CARD COMPONENT (Copied) --- function SubscriptionCard({ plan, selected, onSelect, isAnnual }) { - const planKey = getPlanKeyForBadge(plan) - const planName = - (planKey && PLAN_DISPLAY_NAME_BY_KEY[planKey]) || - plan?.product?.name || - plan?.nickname || - plan?.priceId || - 'Abonnement' + const planKey = getSubscriptionPlanKey(plan) const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency) const intervalLabel = getIntervalLabel(plan?.recurring) const coinsPerMonth = typeof plan?.coinsPerMonth === 'number' && Number.isFinite(plan.coinsPerMonth) ? Math.round(plan.coinsPerMonth) : null - const details = planKey ? PLAN_DETAILS[planKey] : null - const isPopular = details?.popular + const cardDisplay = getSubscriptionCardDisplay({ + planKey, + isAnnual, + formattedPrice, + intervalLabel, + coinsPerMonth, + fallbackTitle: plan?.product?.name || plan?.nickname || plan?.priceId || 'Abonnement', + }) + const isPopular = cardDisplay.popular const planBadgeKey = planKey const planBadgeSource = planBadgeKey && planBadges[planBadgeKey] ? planBadges[planBadgeKey] : null @@ -217,7 +129,7 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) { - {planName} + {cardDisplay.title} {planBadgeSource ? ( ) : null} - {plan?.nickname ? {plan.nickname} : null} - {formattedPrice || coinsPerMonth !== null ? ( + {cardDisplay.primaryPrice || cardDisplay.creditsLabel ? ( - {formattedPrice ? ( - - {formattedPrice} - {intervalLabel ? {intervalLabel} : null} - + {cardDisplay.primaryPrice ? ( + {cardDisplay.primaryPrice} ) : null} - - {details?.creditsLabel ? ( - {details.creditsLabel} - ) : coinsPerMonth !== null ? ( - - - / mois - + {cardDisplay.secondaryPrice ? ( + {cardDisplay.secondaryPrice} + ) : null} + {cardDisplay.creditsLabel ? ( + {cardDisplay.creditsLabel} ) : null} ) : null} - - Privilège Membre : - {details?.privileges?.map((item, index) => ( - - - {item.bold ? `• ${item.text}` : item.text} - - - )) || ( - • Crédits gratuits tous les mois + {Array.isArray(cardDisplay.benefitItems) && cardDisplay.benefitItems.length ? ( + + {cardDisplay.benefitItems.map((item, index) => + item?.type === 'plus' ? ( + + {item.text} + + ) : ( + + {item.text} + + ) )} - + + ) : null} {isPopular ? ( @@ -274,11 +177,6 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) { ) : null} - {isAnnual ? ( - - 2 mois offert - - ) : null} ) } @@ -301,7 +199,7 @@ const ShareAndCreditsModal = ({ isCatalogLoading, } = useStripe() - const [selectedPeriodKey, setSelectedPeriodKey] = useState('annual') + const [selectedPeriodKey, setSelectedPeriodKey] = useState('monthly') const [selectedPriceId, setSelectedPriceId] = useState(null) const [processingPriceId, setProcessingPriceId] = useState(null) const [errorMessage, setErrorMessage] = useState(null) @@ -422,7 +320,7 @@ const ShareAndCreditsModal = ({ {/* CREDITS SECTION */} {coinPacks && coinPacks.length > 0 ? ( - Recharger vos crédits + {COIN_PACK_SECTION_TITLE} {coinPacks.map((pack) => { const rawAmount = pack.unitAmount ?? pack.amount ?? pack.price ?? 0 @@ -432,6 +330,8 @@ const ShareAndCreditsModal = ({ const details = packKey ? COIN_PACK_DETAILS[packKey] : null const displayName = details?.label || pack.name const creditsText = details?.creditsDisplay + const displayPrice = details?.priceDisplay || price || `${rawAmount / 100}€` + const isPopular = details?.popular === true return ( @@ -450,12 +350,13 @@ const ShareAndCreditsModal = ({ {details.disclaimer} ) : null} + {isPopular ? ( + + Le plus populaire ! + + ) : null} handleBuyCredits(pack)} disabled={Boolean(processingPriceId)} style={styles.creditButton} @@ -636,7 +537,7 @@ const styles = StyleSheet.create({ borderRadius: 16, padding: 16, alignItems: 'center', - justifyContent: 'space-between', + justifyContent: 'flex-start', width: isWeb ? 220 : '100%', minHeight: 160, gap: 12, @@ -672,6 +573,7 @@ const styles = StyleSheet.create({ marginTop: 4, }, creditButton: { + marginTop: 'auto', width: '100%', height: 36, minHeight: 36, @@ -758,6 +660,10 @@ const styles = StyleSheet.create({ padding: 16, gap: 16, }, + cardContent: { + flex: 1, + gap: 16, + }, badge: { alignSelf: 'flex-start', backgroundColor: 'rgba(112, 35, 247, 0.25)', @@ -795,6 +701,11 @@ const styles = StyleSheet.create({ priceBlock: { marginBottom: 8, }, + secondaryPriceText: { + color: Palette.primary, + fontSize: 15, + fontFamily: FONT_FAMILY.InterBold, + }, priceRow: { flexDirection: 'row', alignItems: 'baseline', @@ -830,6 +741,19 @@ const styles = StyleSheet.create({ cardBenefits: { gap: 6, }, + downloadLabel: { + color: Palette.white, + fontSize: 13, + fontFamily: FONT_FAMILY.InterMedium, + textAlign: 'center', + }, + benefitPlus: { + color: Palette.white, + fontSize: 22, + fontFamily: FONT_FAMILY.InterBold, + textAlign: 'center', + lineHeight: 24, + }, benefitsTitle: { color: Palette.white, fontSize: 13, @@ -856,7 +780,7 @@ const styles = StyleSheet.create({ paddingHorizontal: 12, paddingVertical: 4, borderRadius: 12, - marginTop: 12, + marginTop: 'auto', }, popularBadgeText: { color: Palette.white, diff --git a/src/utils/coinPackDisplay.js b/src/utils/coinPackDisplay.js new file mode 100644 index 0000000..5a9a53d --- /dev/null +++ b/src/utils/coinPackDisplay.js @@ -0,0 +1,63 @@ +export const COIN_PACK_SECTION_TITLE = 'Achat de crédits' + +export const COIN_PACK_DETAILS = { + starter: { + label: 'Amateur', + creditsDisplay: '30 crédits', + priceDisplay: '4,99€', + disclaimer: 'Téléchargement gratuit de tes créations', + }, + pro: { + label: 'Pro', + creditsDisplay: '100 crédits', + priceDisplay: '9,99€', + popular: true, + disclaimer: 'Téléchargement gratuit de tes créations', + }, + premium: { + label: 'Engagé', + creditsDisplay: '150 crédits', + priceDisplay: '14,99€', + disclaimer: 'Téléchargement gratuit de tes créations', + }, +} + +export const getCoinPackKey = (pack) => { + const directKey = (pack?.coinPackKey || '').toString().trim().toLowerCase() + const name = (pack?.name || '').toString().trim().toLowerCase() + + if (directKey === 'starter') { + return 'starter' + } + + if (directKey === 'pro' || directKey === 'medium') { + return 'pro' + } + + if (directKey === 'premium' || directKey === 'creator') { + return 'premium' + } + + if (!name) { + return null + } + + if (name.includes('starter') || name.includes('amateur')) { + return 'starter' + } + + if (name.includes('medium') || name.includes('pro')) { + return 'pro' + } + + if ( + name.includes('premium') || + name.includes('prenium') || + name.includes('creator') || + name.includes('engage') + ) { + return 'premium' + } + + return null +} diff --git a/src/utils/coinPackModal.js b/src/utils/coinPackModal.js index 5fe8a73..737954c 100644 --- a/src/utils/coinPackModal.js +++ b/src/utils/coinPackModal.js @@ -1,21 +1,25 @@ -const listeners = new Set() +const listeners = new Map() +let nextListenerId = 0 export const subscribeCoinPackModal = (listener) => { if (typeof listener !== 'function') { return () => {} } - listeners.add(listener) + const listenerId = ++nextListenerId + listeners.set(listenerId, listener) return () => { - listeners.delete(listener) + listeners.delete(listenerId) } } export const openCoinPackModal = () => { - listeners.forEach((listener) => { - try { - listener() - } catch (error) { - console.error('[coinPackModal] open listener error', error) - } - }) + const activeListener = Array.from(listeners.values()).pop() + if (!activeListener) { + return + } + try { + activeListener() + } catch (error) { + console.error('[coinPackModal] open listener error', error) + } } diff --git a/src/utils/subscriptionCardDisplay.js b/src/utils/subscriptionCardDisplay.js new file mode 100644 index 0000000..0ad49fd --- /dev/null +++ b/src/utils/subscriptionCardDisplay.js @@ -0,0 +1,139 @@ +export const SUBSCRIPTION_PRICE_ID_BY_PERIOD = { + monthly: { + starter: 'price_1SPgitCzf2o5bDRdbnhLFx6f', + pro: 'price_1SPgjCCzf2o5bDRdr08Xzp8u', + premium: 'price_1SPgjaCzf2o5bDRdd9Xo2u26', + }, + annual: { + starter: 'price_1SPgkDCzf2o5bDRdNGLVNeQ3', + pro: 'price_1SPgkXCzf2o5bDRdejBVxEBY', + premium: 'price_1SPgkqCzf2o5bDRdIcUwTDrm', + }, +} + +export const SUBSCRIPTION_PRICE_ID_TO_PLAN_KEY = Object.values( + SUBSCRIPTION_PRICE_ID_BY_PERIOD +).reduce((acc, mapping) => { + Object.entries(mapping).forEach(([planKey, priceId]) => { + if (priceId) { + acc[priceId] = planKey + } + }) + return acc +}, {}) + +export const SUBSCRIPTION_PLAN_SYNONYMS = { + starter: ['starter', 'blue'], + pro: ['pro', 'silver'], + premium: ['premium', 'gold'], +} + +export const SUBSCRIPTION_PLAN_ORDER = ['starter', 'pro', 'premium'] + +export const SUBSCRIPTION_PLAN_DISPLAY_NAME_BY_KEY = { + starter: 'Pack Play Backer Blue', + pro: 'Pack Play Backer Silver', + premium: 'Pack Play Backer Gold', +} + +export const SUBSCRIPTION_PLAN_DETAILS = { + starter: { + monthlyPriceDisplay: '4,99 € / Mois', + annualPriceDisplay: '49,99 € / An (2 mois offerts)', + monthlyCreditsLabel: '50 Crédits / Mois', + annualCreditsLabel: 'Annuel: 600 crédits', + playlistLabel: 'Accès à la création de Playlist personnalisées', + downloadLabel: 'Téléchargement gratuit de tes créations', + fullAccessLabel: 'Accès au téléchargement de toutes les créations de Musicland', + popular: false, + }, + pro: { + monthlyPriceDisplay: '9,99 € / Mois', + annualPriceDisplay: '99,99 € / An (2 mois offerts)', + monthlyCreditsLabel: '110 crédits/Mois', + annualCreditsLabel: 'Annuel: 1320 crédits', + playlistLabel: 'Accès à la création de Playlist personnalisées', + downloadLabel: 'Téléchargement gratuit de tes créations', + fullAccessLabel: 'Accès au téléchargement de toutes les créations de Musicland', + popular: true, + }, + premium: { + monthlyPriceDisplay: '14,99 € / Mois', + annualPriceDisplay: '149,99 € / An (2 mois offerts)', + monthlyCreditsLabel: '160 Crédits/mois', + annualCreditsLabel: 'Annuel: 1920 Crédits', + playlistLabel: 'Accès à la création de Playlist personnalisées', + downloadLabel: 'Téléchargement gratuit de tes créations', + fullAccessLabel: 'Accès au téléchargement de toutes les créations de Musicland', + popular: false, + }, +} + +const formatFallbackPrimaryPrice = (formattedPrice, intervalLabel) => { + if (!formattedPrice) { + return null + } + + if (!intervalLabel) { + return formattedPrice + } + + return `${formattedPrice} ${intervalLabel}` +} + +export const getSubscriptionPlanKey = (plan) => { + if (!plan || typeof plan !== 'object') { + return null + } + + const priceId = typeof plan?.priceId === 'string' ? plan.priceId : plan?.id || null + if (priceId && SUBSCRIPTION_PRICE_ID_TO_PLAN_KEY[priceId]) { + return SUBSCRIPTION_PRICE_ID_TO_PLAN_KEY[priceId] + } + + const label = (plan?.product?.name || plan?.nickname || '').toString().toLowerCase() + if (!label) { + return null + } + + const match = Object.entries(SUBSCRIPTION_PLAN_SYNONYMS).find(([, variants]) => + variants.some((variant) => label.includes(variant)) + ) + + return match ? match[0] : null +} + +export const getSubscriptionCardDisplay = ({ + planKey, + isAnnual = false, + formattedPrice = null, + intervalLabel = null, + coinsPerMonth = null, + fallbackTitle = 'Abonnement', +}) => { + const details = planKey ? SUBSCRIPTION_PLAN_DETAILS[planKey] : null + + return { + title: + (planKey && SUBSCRIPTION_PLAN_DISPLAY_NAME_BY_KEY[planKey]) || fallbackTitle, + primaryPrice: + (details && (isAnnual ? details.annualPriceDisplay : details.monthlyPriceDisplay)) || + formatFallbackPrimaryPrice(formattedPrice, intervalLabel), + secondaryPrice: details && !isAnnual ? details.annualPriceDisplay : null, + creditsLabel: + (details && (isAnnual ? details.annualCreditsLabel : details.monthlyCreditsLabel)) || + (typeof coinsPerMonth === 'number' ? `${Math.round(coinsPerMonth)} crédits/mois` : null), + downloadLabel: details?.downloadLabel || null, + benefitItems: isAnnual + ? [ + details?.playlistLabel ? { type: 'text', text: details.playlistLabel } : null, + details?.downloadLabel ? { type: 'text', text: details.downloadLabel } : null, + { type: 'plus', text: '+' }, + details?.fullAccessLabel ? { type: 'text', text: details.fullAccessLabel } : null, + ].filter(Boolean) + : details?.downloadLabel + ? [{ type: 'text', text: details.downloadLabel }] + : [], + popular: details?.popular === true, + } +}