feat(ui): refine club, packs, and download flows

This commit is contained in:
Jacques TIROT
2026-03-11 11:54:10 +01:00
parent af8a8bafa4
commit fd299e8543
12 changed files with 565 additions and 598 deletions
+63
View File
@@ -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
}
+14 -10
View File
@@ -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)
}
}
+139
View File
@@ -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,
}
}