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,
+ }
+}