feat(ui): refine club, packs, and download flows
This commit is contained in:
@@ -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 { Platform, Pressable, StyleSheet } from 'react-native'
|
||||||
import CreditAmount from './CreditAmount'
|
import CreditAmount from './CreditAmount'
|
||||||
import CoinPackModal from './modal/CoinPackModal'
|
|
||||||
import { useUser } from '../providers/UserDataProvider'
|
import { useUser } from '../providers/UserDataProvider'
|
||||||
import { Palette } from '../styles'
|
import { Palette } from '../styles'
|
||||||
import { FONT_FAMILY } from '../styles/Fonts'
|
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 MobileCoinBadge = ({ style, textStyle, iconSize = 20, iconPosition = 'left' }) => {
|
||||||
const { currentUID, currentUserData } = useUser() || {}
|
const { currentUID, currentUserData } = useUser() || {}
|
||||||
const [isModalVisible, setModalVisible] = useState(false)
|
|
||||||
|
|
||||||
const coinBalance = useMemo(() => {
|
const coinBalance = useMemo(() => {
|
||||||
const value = currentUserData?.coins
|
const value = currentUserData?.coins
|
||||||
@@ -26,15 +25,7 @@ const MobileCoinBadge = ({ style, textStyle, iconSize = 20, iconPosition = 'left
|
|||||||
}, [currentUserData?.coins])
|
}, [currentUserData?.coins])
|
||||||
|
|
||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
if (Platform.OS === 'web') {
|
navigate(Routes.Payments)
|
||||||
openCoinPackModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setModalVisible(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
setModalVisible(false)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!currentUID || Platform.OS === 'web') {
|
if (!currentUID || Platform.OS === 'web') {
|
||||||
@@ -42,18 +33,15 @@ const MobileCoinBadge = ({ style, textStyle, iconSize = 20, iconPosition = 'left
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Pressable onPress={handlePress} accessibilityRole="button" style={[styles.container, style]}>
|
||||||
<Pressable onPress={handlePress} accessibilityRole="button" style={[styles.container, style]}>
|
<CreditAmount
|
||||||
<CreditAmount
|
value={coinBalance}
|
||||||
value={coinBalance}
|
style={styles.content}
|
||||||
style={styles.content}
|
textStyle={[styles.text, textStyle]}
|
||||||
textStyle={[styles.text, textStyle]}
|
iconSize={iconSize}
|
||||||
iconSize={iconSize}
|
iconPosition={iconPosition}
|
||||||
iconPosition={iconPosition}
|
/>
|
||||||
/>
|
</Pressable>
|
||||||
</Pressable>
|
|
||||||
<CoinPackModal visible={isModalVisible} onClose={handleClose} />
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import { FONT_FAMILY } from '../../styles/Fonts'
|
|||||||
import { isWeb } from '../../hooks/useLayoutType'
|
import { isWeb } from '../../hooks/useLayoutType'
|
||||||
import CreditAmount from '../CreditAmount'
|
import CreditAmount from '../CreditAmount'
|
||||||
import { useStripe } from '../../providers/StripeProvider'
|
import { useStripe } from '../../providers/StripeProvider'
|
||||||
|
import {
|
||||||
|
COIN_PACK_DETAILS,
|
||||||
|
COIN_PACK_SECTION_TITLE,
|
||||||
|
getCoinPackKey,
|
||||||
|
} from '../../utils/coinPackDisplay'
|
||||||
|
|
||||||
const WEB_MODAL_MAX_WIDTH = 820
|
const WEB_MODAL_MAX_WIDTH = 820
|
||||||
const formatCurrency = (amount, currency = 'eur') => {
|
const formatCurrency = (amount, currency = 'eur') => {
|
||||||
@@ -41,37 +46,10 @@ const formatCurrency = (amount, currency = 'eur') => {
|
|||||||
return `${normalized.toFixed(2)} ${upperCurrency}`
|
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 FALLBACK_DISCOUNT_STEPS = [0, 12, 18, 26, 32]
|
||||||
const STATIC_DISCOUNTS = [0, 30, 50]
|
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 = []) => {
|
const computeCoinPackPricing = (packs = []) => {
|
||||||
if (!Array.isArray(packs) || !packs.length) {
|
if (!Array.isArray(packs) || !packs.length) {
|
||||||
return {}
|
return {}
|
||||||
@@ -191,6 +169,8 @@ function CoinPackCard({ pack, selected, pricingDetail, onSelect, staticDiscountP
|
|||||||
const details = packKey ? COIN_PACK_DETAILS[packKey] : null
|
const details = packKey ? COIN_PACK_DETAILS[packKey] : null
|
||||||
const displayName = details?.label || pack.name
|
const displayName = details?.label || pack.name
|
||||||
const creditsText = details?.creditsDisplay
|
const creditsText = details?.creditsDisplay
|
||||||
|
const displayPrice = details?.priceDisplay || formattedPrice
|
||||||
|
const isPopular = details?.popular === true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -235,8 +215,13 @@ function CoinPackCard({ pack, selected, pricingDetail, onSelect, staticDiscountP
|
|||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.priceBlock}>
|
<View style={styles.priceBlock}>
|
||||||
{formattedPrice ? <Text style={styles.packPrice}>{formattedPrice}</Text> : null}
|
{displayPrice ? <Text style={styles.packPrice}>{displayPrice}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
|
{isPopular ? (
|
||||||
|
<View style={styles.popularBadge}>
|
||||||
|
<Text style={styles.popularBadgeText}>Le plus populaire !</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</BlurView>
|
</BlurView>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
@@ -324,7 +309,7 @@ const CoinPackModal = ({ visible, onClose }) => {
|
|||||||
|
|
||||||
if (!coinPacks.length) {
|
if (!coinPacks.length) {
|
||||||
return (
|
return (
|
||||||
<Text style={styles.emptyState}>Aucun pack de pièces n'est disponible pour le moment.</Text>
|
<Text style={styles.emptyState}>Aucun pack de crédits n'est disponible pour le moment.</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,9 +350,9 @@ const CoinPackModal = ({ visible, onClose }) => {
|
|||||||
>
|
>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.title}>Acheter des pièces</Text>
|
<Text style={styles.title}>{COIN_PACK_SECTION_TITLE}</Text>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
Choisis un pack et finalise ton achat pour continuer.
|
Crée tes chansons et tes vidéos.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -378,8 +363,7 @@ const CoinPackModal = ({ visible, onClose }) => {
|
|||||||
<View style={styles.content}>{renderContent()}</View>
|
<View style={styles.content}>{renderContent()}</View>
|
||||||
|
|
||||||
<Text style={styles.indicativeNote}>
|
<Text style={styles.indicativeNote}>
|
||||||
Tarifs indicatifs : les prix et quantités de jetons sont amenés à évoluer. Les remises
|
Téléchargement gratuit de tes créations.
|
||||||
sont affichées vs le pack de base pour mieux valoriser les offres volumineuses.
|
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
@@ -394,7 +378,7 @@ const CoinPackModal = ({ visible, onClose }) => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={styles.disclaimer}>
|
<Text style={styles.disclaimer}>
|
||||||
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é.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</CreateLyricsHeader>
|
</CreateLyricsHeader>
|
||||||
@@ -548,9 +532,24 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
priceBlock: {
|
priceBlock: {
|
||||||
|
marginTop: 'auto',
|
||||||
gap: 2,
|
gap: 2,
|
||||||
alignItems: 'center',
|
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: {
|
packPrice: {
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
|
|||||||
+4
-4
@@ -122,7 +122,7 @@ export default ({
|
|||||||
|
|
||||||
const handleCoinPress = React.useCallback(() => {
|
const handleCoinPress = React.useCallback(() => {
|
||||||
if (!showCoinBadge) return
|
if (!showCoinBadge) return
|
||||||
setCoinModalVisible(true)
|
navigate(Routes.Payments)
|
||||||
}, [showCoinBadge])
|
}, [showCoinBadge])
|
||||||
|
|
||||||
const handleCloseCoinModal = React.useCallback(() => {
|
const handleCloseCoinModal = React.useCallback(() => {
|
||||||
@@ -144,10 +144,10 @@ export default ({
|
|||||||
}, [showCoinBadge, isCoinModalVisible])
|
}, [showCoinBadge, isCoinModalVisible])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
if (!showCoinBadge) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
const unsubscribe = subscribeCoinPackModal(() => {
|
const unsubscribe = subscribeCoinPackModal(() => {
|
||||||
if (!showCoinBadge) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setCoinModalVisible(true)
|
setCoinModalVisible(true)
|
||||||
})
|
})
|
||||||
return unsubscribe
|
return unsubscribe
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
|||||||
import GradientButton from '../../components/GradientButton'
|
import GradientButton from '../../components/GradientButton'
|
||||||
import MoreMenu from '../../components/MoreMenu'
|
import MoreMenu from '../../components/MoreMenu'
|
||||||
import ProjectDropDown from '../../components/ProjectDropDown/ProjectDropDown'
|
import ProjectDropDown from '../../components/ProjectDropDown/ProjectDropDown'
|
||||||
import CoinPackModal from '../../components/modal/CoinPackModal'
|
|
||||||
import { projectsRef, usersRef } from '../../config/firebase'
|
import { projectsRef, usersRef } from '../../config/firebase'
|
||||||
import { isWeb } from '../../hooks/useLayoutType.js'
|
import { isWeb } from '../../hooks/useLayoutType.js'
|
||||||
import Page from '../../layouts/Page'
|
import Page from '../../layouts/Page'
|
||||||
@@ -23,7 +22,6 @@ import ShareBtn from '../../components/ShareBtn/ShareBtn'
|
|||||||
import { Palette, gutters } from '../../styles'
|
import { Palette, gutters } from '../../styles'
|
||||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||||
import { getCreationStageStates, getStageAction } from '../../utils/projectStages'
|
import { getCreationStageStates, getStageAction } from '../../utils/projectStages'
|
||||||
import { openCoinPackModal } from '../../utils/coinPackModal'
|
|
||||||
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
|
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
|
||||||
import usePlayer from '../../hooks/usePlayer'
|
import usePlayer from '../../hooks/usePlayer'
|
||||||
import StageCard from './components/StageCard'
|
import StageCard from './components/StageCard'
|
||||||
@@ -407,8 +405,6 @@ const Home = ({ navigation, route }) => {
|
|||||||
[stageStatesByKey]
|
[stageStatesByKey]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [isCoinModalVisible, setCoinModalVisible] = useState(false)
|
|
||||||
|
|
||||||
const handleStagePress = useCallback(
|
const handleStagePress = useCallback(
|
||||||
(stageKey, isLocked) => {
|
(stageKey, isLocked) => {
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
@@ -434,19 +430,11 @@ const Home = ({ navigation, route }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const handleClubPress = useCallback(() => {
|
const handleClubPress = useCallback(() => {
|
||||||
navigate(Routes.Payments)
|
navigate(Routes.HitParade)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleOpenCoinModal = useCallback(() => {
|
const handleOpenCoinModal = useCallback(() => {
|
||||||
if (isWeb) {
|
navigate(Routes.Payments)
|
||||||
openCoinPackModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setCoinModalVisible(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleCloseCoinModal = useCallback(() => {
|
|
||||||
setCoinModalVisible(false)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const stageCardContainerStyle = isWeb ? styles.cardsGrid : styles.cardsStack
|
const stageCardContainerStyle = isWeb ? styles.cardsGrid : styles.cardsStack
|
||||||
@@ -652,10 +640,6 @@ const Home = ({ navigation, route }) => {
|
|||||||
</View>
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{!isWeb ? (
|
|
||||||
<CoinPackModal visible={isCoinModalVisible} onClose={handleCloseCoinModal} />
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import * as FileSystem from 'expo-file-system'
|
|||||||
import { shareAsync } from 'expo-sharing'
|
import { shareAsync } from 'expo-sharing'
|
||||||
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
||||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||||
import GradientButton from '../../components/GradientButton'
|
|
||||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||||
import AppCheckbox from '../../components/AppCheckbox'
|
import AppCheckbox from '../../components/AppCheckbox'
|
||||||
import firebase, { getFunctionsClient, projectsRef, serverTimestamp } from '../../config/firebase'
|
import firebase, { getFunctionsClient, projectsRef, serverTimestamp } from '../../config/firebase'
|
||||||
@@ -178,7 +177,6 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
const downloadLabel = canDownloadPlayback
|
const downloadLabel = canDownloadPlayback
|
||||||
? 'Télécharger le playback'
|
? 'Télécharger le playback'
|
||||||
: 'Acheter le playback pour 1,99€'
|
: 'Acheter le playback pour 1,99€'
|
||||||
const showSubscriptionCta = !hasActiveSubscription
|
|
||||||
|
|
||||||
const afterPlaybackUrl = useMemo(() => {
|
const afterPlaybackUrl = useMemo(() => {
|
||||||
if (!videos) return null
|
if (!videos) return null
|
||||||
@@ -434,55 +432,43 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
|
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
|
||||||
<CreateLyricsHeader title={'Publier ton playback'} />
|
<CreateLyricsHeader title={'Publier ton playback'} />
|
||||||
|
|
||||||
<View style={styles.coverRow}>
|
<ClubAdvantagesCard
|
||||||
{projectForDownload?.coverUrl ? (
|
style={styles.clubCardSpacing}
|
||||||
<ExpoImage
|
topContent={
|
||||||
source={{ uri: projectForDownload?.coverUrl }}
|
<View style={styles.coverRow}>
|
||||||
style={styles.coverImage}
|
{projectForDownload?.coverUrl ? (
|
||||||
contentFit="cover"
|
<ExpoImage
|
||||||
/>
|
source={{ uri: projectForDownload?.coverUrl }}
|
||||||
) : (
|
style={styles.coverImage}
|
||||||
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
contentFit="cover"
|
||||||
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
||||||
|
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.downloadColumn}>
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.downloadTile,
|
||||||
|
(isPublishing || isDownloading || isCheckoutLaunching) && { opacity: 0.6 },
|
||||||
|
]}
|
||||||
|
onPress={handleDownloadPress}
|
||||||
|
disabled={isPublishing || isDownloading || isCheckoutLaunching}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
||||||
|
<Text style={styles.downloadText}>{downloadLabel}</Text>
|
||||||
|
</Pressable>
|
||||||
|
{!canDownloadPlayback && downloadPaymentPending ? (
|
||||||
|
<Text style={styles.downloadPendingText}>
|
||||||
|
Paiement en attente de confirmation...
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
}
|
||||||
|
/>
|
||||||
<Pressable
|
|
||||||
style={[
|
|
||||||
styles.downloadTile,
|
|
||||||
(isPublishing || isDownloading || isCheckoutLaunching) && { opacity: 0.6 },
|
|
||||||
]}
|
|
||||||
onPress={handleDownloadPress}
|
|
||||||
disabled={isPublishing || isDownloading || isCheckoutLaunching}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
|
||||||
<Text style={styles.downloadText}>{downloadLabel}</Text>
|
|
||||||
</Pressable>
|
|
||||||
{!canDownloadPlayback && downloadPaymentPending ? (
|
|
||||||
<Text style={styles.downloadPendingText}>
|
|
||||||
Paiement en attente de confirmation...
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{showSubscriptionCta ? (
|
|
||||||
<View style={styles.subscriptionCta}>
|
|
||||||
<Text style={styles.subscriptionNote}>
|
|
||||||
Avec un abonnement, le téléchargement est gratuit.
|
|
||||||
</Text>
|
|
||||||
<GradientButton
|
|
||||||
title="Voir les abonnements"
|
|
||||||
onPress={() => navigate(Routes.Payments)}
|
|
||||||
containerStyle={styles.subscriptionButton}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<Text style={styles.subscriptionNote}>
|
|
||||||
Avec ton abonnement, le téléchargement est gratuit.
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ClubAdvantagesCard style={styles.clubCardSpacing} />
|
|
||||||
|
|
||||||
<View style={styles.publishConsentContainer}>
|
<View style={styles.publishConsentContainer}>
|
||||||
<AppCheckbox
|
<AppCheckbox
|
||||||
@@ -546,6 +532,10 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
},
|
},
|
||||||
|
downloadColumn: {
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
downloadPendingText: {
|
downloadPendingText: {
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
@@ -553,22 +543,6 @@ const styles = StyleSheet.create({
|
|||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
subscriptionCta: {
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 10,
|
|
||||||
marginTop: 6,
|
|
||||||
},
|
|
||||||
subscriptionNote: {
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
fontSize: 13,
|
|
||||||
color: Palette.white,
|
|
||||||
opacity: 0.8,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
subscriptionButton: {
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: 280,
|
|
||||||
},
|
|
||||||
clubCardSpacing: {
|
clubCardSpacing: {
|
||||||
marginTop: 10,
|
marginTop: 10,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import GradientButton from '../../../components/GradientButton'
|
|||||||
import CreditAmount from '../../../components/CreditAmount'
|
import CreditAmount from '../../../components/CreditAmount'
|
||||||
import { useStripe } from '../../../providers/StripeProvider'
|
import { useStripe } from '../../../providers/StripeProvider'
|
||||||
import { isWeb } from '../../../hooks/useLayoutType'
|
import { isWeb } from '../../../hooks/useLayoutType'
|
||||||
|
import {
|
||||||
|
COIN_PACK_DETAILS,
|
||||||
|
COIN_PACK_SECTION_TITLE,
|
||||||
|
getCoinPackKey,
|
||||||
|
} from '../../../utils/coinPackDisplay'
|
||||||
|
|
||||||
const formatCurrency = (amount, currency = 'eur') => {
|
const formatCurrency = (amount, currency = 'eur') => {
|
||||||
if (typeof amount !== 'number') {
|
if (typeof amount !== 'number') {
|
||||||
@@ -33,34 +38,7 @@ const formatCurrency = (amount, currency = 'eur') => {
|
|||||||
return `${normalized.toFixed(2)} ${upperCurrency}`
|
return `${normalized.toFixed(2)} ${upperCurrency}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const COIN_PACK_DETAILS = {
|
const ClubAdvantagesCard = ({ style, topContent = null }) => {
|
||||||
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 navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
const { hasActiveSubscription } = useUserData() || {}
|
const { hasActiveSubscription } = useUserData() || {}
|
||||||
const { coinPacks, createCoinPackCheckout } = useStripe()
|
const { coinPacks, createCoinPackCheckout } = useStripe()
|
||||||
@@ -91,16 +69,15 @@ const ClubAdvantagesCard = ({ style }) => {
|
|||||||
>
|
>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.titleBlock}>
|
<View style={styles.titleBlock}>
|
||||||
<Text style={{fontSize: 30, fontWeight: 'bold', color: Palette.white, marginBottom: 12}}>
|
{topContent ? <View style={styles.topContentContainer}>{topContent}</View> : null}
|
||||||
OU
|
<View style={styles.topDivider} />
|
||||||
</Text>
|
|
||||||
<Text style={styles.title}>
|
<Text style={styles.title}>
|
||||||
{'Les abonnements et les packs permettent le téléchargement de vos créations'}
|
{'Les abonnements et les packs permettent le téléchargement de vos créations'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.subtitle}>{'Avec un abonnement le téléchargement est gratuit'}</Text>
|
<Text style={styles.subtitle}>{'Avec un abonnement le téléchargement est gratuit'}</Text>
|
||||||
{coinPacks && coinPacks.length > 0 ? (
|
{coinPacks && coinPacks.length > 0 ? (
|
||||||
<View style={styles.sectionContainer}>
|
<View style={styles.sectionContainer}>
|
||||||
<Text style={styles.sectionTitle}>Recharger vos crédits</Text>
|
<Text style={styles.sectionTitle}>{COIN_PACK_SECTION_TITLE}</Text>
|
||||||
<View style={styles.creditsGrid}>
|
<View style={styles.creditsGrid}>
|
||||||
{coinPacks.map((pack) => {
|
{coinPacks.map((pack) => {
|
||||||
const rawAmount = pack.unitAmount ?? pack.amount ?? pack.price ?? 0
|
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 details = packKey ? COIN_PACK_DETAILS[packKey] : null
|
||||||
const displayName = details?.label || pack.name
|
const displayName = details?.label || pack.name
|
||||||
const creditsText = details?.creditsDisplay
|
const creditsText = details?.creditsDisplay
|
||||||
|
const displayPrice = details?.priceDisplay || price || `${rawAmount / 100}€`
|
||||||
|
const isPopular = details?.popular === true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={pack.productId} style={styles.creditCard}>
|
<View key={pack.productId} style={styles.creditCard}>
|
||||||
@@ -128,12 +107,13 @@ const ClubAdvantagesCard = ({ style }) => {
|
|||||||
<Text style={styles.packDisclaimer}>{details.disclaimer}</Text>
|
<Text style={styles.packDisclaimer}>{details.disclaimer}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
{isPopular ? (
|
||||||
|
<View style={styles.popularBadge}>
|
||||||
|
<Text style={styles.popularBadgeText}>Le plus populaire !</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title={
|
title={processingPriceId === pack.productId ? '...' : displayPrice}
|
||||||
processingPriceId === pack.productId
|
|
||||||
? '...'
|
|
||||||
: price || `${rawAmount / 100}€`
|
|
||||||
}
|
|
||||||
onPress={() => handleBuyCredits(pack)}
|
onPress={() => handleBuyCredits(pack)}
|
||||||
disabled={Boolean(processingPriceId)}
|
disabled={Boolean(processingPriceId)}
|
||||||
style={styles.creditButton}
|
style={styles.creditButton}
|
||||||
@@ -179,6 +159,17 @@ const styles = StyleSheet.create({
|
|||||||
gap: gutters * 0.3,
|
gap: gutters * 0.3,
|
||||||
alignItems: 'center',
|
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: {
|
title: {
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -196,8 +187,6 @@ const styles = StyleSheet.create({
|
|||||||
gap: 16,
|
gap: 16,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: 'rgba(255, 255, 255, 0.08)',
|
|
||||||
paddingTop: gutters,
|
paddingTop: gutters,
|
||||||
marginTop: gutters,
|
marginTop: gutters,
|
||||||
paddingBottom: gutters,
|
paddingBottom: gutters,
|
||||||
@@ -221,7 +210,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'flex-start',
|
||||||
width: isWeb ? 220 : '100%',
|
width: isWeb ? 220 : '100%',
|
||||||
minHeight: 160,
|
minHeight: 160,
|
||||||
gap: 12,
|
gap: 12,
|
||||||
@@ -257,6 +246,7 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
creditButton: {
|
creditButton: {
|
||||||
|
marginTop: 'auto',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 36,
|
height: 36,
|
||||||
minHeight: 36,
|
minHeight: 36,
|
||||||
@@ -264,6 +254,20 @@ const styles = StyleSheet.create({
|
|||||||
creditButtonText: {
|
creditButtonText: {
|
||||||
fontSize: 13,
|
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: {
|
errorText: {
|
||||||
color: Palette.red,
|
color: Palette.red,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
|||||||
+85
-192
@@ -7,7 +7,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|||||||
import GradientButton from '../components/GradientButton'
|
import GradientButton from '../components/GradientButton'
|
||||||
import CreditAmount from '../components/CreditAmount'
|
import CreditAmount from '../components/CreditAmount'
|
||||||
import Page from '../layouts/Page'
|
import Page from '../layouts/Page'
|
||||||
import { background, icons, planBadges, subBadges } from '../assets'
|
import { background, icons, planBadges } from '../assets'
|
||||||
import { Palette, gutters } from '../styles'
|
import { Palette, gutters } from '../styles'
|
||||||
import { FONT_FAMILY } from '../styles/Fonts'
|
import { FONT_FAMILY } from '../styles/Fonts'
|
||||||
import { isWeb } from '../hooks/useLayoutType'
|
import { isWeb } from '../hooks/useLayoutType'
|
||||||
@@ -15,6 +15,18 @@ import { useStripe } from '../providers/StripeProvider'
|
|||||||
import { useUserData } from '../providers/UserDataProvider'
|
import { useUserData } from '../providers/UserDataProvider'
|
||||||
import { goBack, navigate } from '../navigation/NavigationService'
|
import { goBack, navigate } from '../navigation/NavigationService'
|
||||||
import { Routes } from '../navigation/Routes'
|
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') => {
|
const formatCurrency = (amount, currency = 'eur') => {
|
||||||
if (typeof amount !== 'number') {
|
if (typeof amount !== 'number') {
|
||||||
@@ -63,21 +75,22 @@ const getIntervalLabel = (recurring) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
||||||
const planKey = getPlanKeyForBadge(plan)
|
const planKey = getSubscriptionPlanKey(plan)
|
||||||
const planName =
|
|
||||||
(planKey && PLAN_DISPLAY_NAME_BY_KEY[planKey]) ||
|
|
||||||
plan?.product?.name ||
|
|
||||||
plan?.nickname ||
|
|
||||||
plan?.priceId ||
|
|
||||||
'Abonnement'
|
|
||||||
const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency)
|
const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency)
|
||||||
const intervalLabel = getIntervalLabel(plan?.recurring)
|
const intervalLabel = getIntervalLabel(plan?.recurring)
|
||||||
const coinsPerMonth =
|
const coinsPerMonth =
|
||||||
typeof plan?.coinsPerMonth === 'number' && Number.isFinite(plan.coinsPerMonth)
|
typeof plan?.coinsPerMonth === 'number' && Number.isFinite(plan.coinsPerMonth)
|
||||||
? Math.round(plan.coinsPerMonth)
|
? Math.round(plan.coinsPerMonth)
|
||||||
: null
|
: null
|
||||||
const details = planKey ? PLAN_DETAILS[planKey] : null
|
const cardDisplay = getSubscriptionCardDisplay({
|
||||||
const isPopular = details?.popular
|
planKey,
|
||||||
|
isAnnual,
|
||||||
|
formattedPrice,
|
||||||
|
intervalLabel,
|
||||||
|
coinsPerMonth,
|
||||||
|
fallbackTitle: plan?.product?.name || plan?.nickname || plan?.priceId || 'Abonnement',
|
||||||
|
})
|
||||||
|
const isPopular = cardDisplay.popular
|
||||||
|
|
||||||
const planBadgeKey = planKey
|
const planBadgeKey = planKey
|
||||||
const getPlanBadge = (plan) => {
|
const getPlanBadge = (plan) => {
|
||||||
@@ -92,7 +105,6 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const planBadgeSource = planBadgeKey && subBadges[planBadgeKey] ? subBadges[planBadgeKey] : null
|
|
||||||
const handleSelect = React.useCallback(() => {
|
const handleSelect = React.useCallback(() => {
|
||||||
if (typeof onSelect === 'function' && plan?.priceId) {
|
if (typeof onSelect === 'function' && plan?.priceId) {
|
||||||
onSelect(plan.priceId)
|
onSelect(plan.priceId)
|
||||||
@@ -124,61 +136,41 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
|||||||
|
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<View style={styles.titleRow}>
|
<View style={styles.titleRow}>
|
||||||
<Text style={styles.planName}>{planName}</Text>
|
<Text style={styles.planName}>{cardDisplay.title}</Text>
|
||||||
{getPlanBadge(planKey)}
|
{getPlanBadge(planKey)}
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
{plan?.nickname ? <Text style={styles.cardSubtitle}>{plan.nickname}</Text> : null}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{formattedPrice || coinsPerMonth !== null ? (
|
{cardDisplay.primaryPrice || cardDisplay.creditsLabel ? (
|
||||||
<View style={styles.priceBlock}>
|
<View style={styles.priceBlock}>
|
||||||
{formattedPrice ? (
|
{cardDisplay.primaryPrice ? (
|
||||||
<View style={styles.priceRow}>
|
<Text style={styles.priceValue}>{cardDisplay.primaryPrice}</Text>
|
||||||
<Text style={styles.priceValue}>{formattedPrice}</Text>
|
|
||||||
{intervalLabel ? <Text style={styles.period}>{intervalLabel}</Text> : null}
|
|
||||||
</View>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
{cardDisplay.secondaryPrice ? (
|
||||||
{details?.creditsLabel ? (
|
<Text style={styles.secondaryPriceText}>{cardDisplay.secondaryPrice}</Text>
|
||||||
<Text style={styles.coinsPromoText}>{details.creditsLabel}</Text>
|
) : null}
|
||||||
) : coinsPerMonth !== null ? (
|
{cardDisplay.creditsLabel ? (
|
||||||
<View style={styles.coinsPerMonthRow}>
|
<Text style={styles.coinsPromoText}>{cardDisplay.creditsLabel}</Text>
|
||||||
<CreditAmount
|
|
||||||
value={coinsPerMonth}
|
|
||||||
showPlus
|
|
||||||
textStyle={styles.coinsPerMonth}
|
|
||||||
iconSize={16}
|
|
||||||
accessibilityLabel={`+${coinsPerMonth} crédits par mois`}
|
|
||||||
/>
|
|
||||||
<Text style={styles.coinsPerMonthSuffix}>/ mois</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View style={styles.cardBenefits}>
|
{Array.isArray(cardDisplay.benefitItems) && cardDisplay.benefitItems.length ? (
|
||||||
<Text style={styles.benefitsTitle}>Privilège Membre :</Text>
|
<View style={styles.cardBenefits}>
|
||||||
{details?.privileges?.map((item, index) => (
|
{cardDisplay.benefitItems.map((item, index) =>
|
||||||
<View key={index} style={styles.benefitBlock}>
|
item?.type === 'plus' ? (
|
||||||
<Text style={[styles.benefitText, item.bold && styles.benefitTextBold]}>
|
<Text key={`benefit-plus-${index}`} style={styles.benefitPlus}>
|
||||||
{item.bold ? `• ${item.text}` : item.text}
|
{item.text}
|
||||||
</Text>
|
|
||||||
{item.subItems ? (
|
|
||||||
<View style={styles.benefitSubList}>
|
|
||||||
<Text style={styles.benefitSubText}>{item.subItems.join(' - ')}</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
)) || (
|
|
||||||
<>
|
|
||||||
<Text style={styles.cardBenefitsItem}>
|
|
||||||
• Eligible au concours mensuel chanson/Vidéo
|
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.cardBenefitsItem}>• Crédits gratuits tous les mois</Text>
|
) : (
|
||||||
</>
|
<Text key={`benefit-text-${index}`} style={styles.downloadLabel}>
|
||||||
|
{item.text}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{isPopular ? (
|
{isPopular ? (
|
||||||
<View style={styles.popularBadge}>
|
<View style={styles.popularBadge}>
|
||||||
@@ -187,11 +179,6 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
|||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</BlurView>
|
</BlurView>
|
||||||
{isAnnual ? (
|
|
||||||
<View style={styles.annualBadge}>
|
|
||||||
<Text style={styles.annualBadgeText}>2 mois offert</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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 getPlanPriority = (plan, periodKey) => {
|
||||||
const priceId = typeof plan?.priceId === 'string' ? plan.priceId : plan?.id || null
|
const priceId = typeof plan?.priceId === 'string' ? plan.priceId : plan?.id || null
|
||||||
const priceOrder = PRICE_PRIORITY_BY_PERIOD[periodKey] || []
|
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 label = (plan?.product?.name || plan?.nickname || '').toString().toLowerCase()
|
||||||
|
|
||||||
const fallbackIndex = PLAN_FALLBACK_ORDER.findIndex((candidate) => {
|
const fallbackIndex = SUBSCRIPTION_PLAN_ORDER.findIndex((candidate) => {
|
||||||
const variants = PLAN_SYNONYMS[candidate] || [candidate]
|
const variants = SUBSCRIPTION_PLAN_SYNONYMS[candidate] || [candidate]
|
||||||
return variants.some((variant) => label.includes(variant))
|
return variants.some((variant) => label.includes(variant))
|
||||||
})
|
})
|
||||||
|
|
||||||
const baseOffset = priceOrder.length
|
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) => {
|
const normalizePlans = (plans = [], periodKey) => {
|
||||||
@@ -390,7 +260,7 @@ export default function Subscriptions() {
|
|||||||
createCoinPackCheckout,
|
createCoinPackCheckout,
|
||||||
} = useStripe()
|
} = useStripe()
|
||||||
const { currentUserData } = useUserData()
|
const { currentUserData } = useUserData()
|
||||||
const [selectedPeriodKey, setSelectedPeriodKey] = React.useState('annual')
|
const [selectedPeriodKey, setSelectedPeriodKey] = React.useState('monthly')
|
||||||
const [selectedPriceId, setSelectedPriceId] = React.useState(null)
|
const [selectedPriceId, setSelectedPriceId] = React.useState(null)
|
||||||
const [processingPriceId, setProcessingPriceId] = React.useState(null)
|
const [processingPriceId, setProcessingPriceId] = React.useState(null)
|
||||||
const [errorMessage, setErrorMessage] = React.useState(null)
|
const [errorMessage, setErrorMessage] = React.useState(null)
|
||||||
@@ -438,13 +308,13 @@ export default function Subscriptions() {
|
|||||||
let nextPeriodKey = selectedPeriodKey
|
let nextPeriodKey = selectedPeriodKey
|
||||||
// Only force switch if current selected period is empty and another is not
|
// Only force switch if current selected period is empty and another is not
|
||||||
if ((normalizedPlansByPeriod[selectedPeriodKey]?.length || 0) === 0) {
|
if ((normalizedPlansByPeriod[selectedPeriodKey]?.length || 0) === 0) {
|
||||||
nextPeriodKey = periodEntries[0]?.[0] || 'annual'
|
nextPeriodKey = periodEntries[0]?.[0] || 'monthly'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldApplyPack) {
|
if (shouldApplyPack) {
|
||||||
const desired = initialSubscriptionPack.trim().toLowerCase()
|
const desired = initialSubscriptionPack.trim().toLowerCase()
|
||||||
for (const [periodKey, plans] of periodEntries) {
|
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) {
|
if (candidateId) {
|
||||||
const exists = plans.some((plan) => plan?.priceId === candidateId)
|
const exists = plans.some((plan) => plan?.priceId === candidateId)
|
||||||
if (exists) {
|
if (exists) {
|
||||||
@@ -772,7 +642,7 @@ export default function Subscriptions() {
|
|||||||
|
|
||||||
{coinPacks && coinPacks.length > 0 ? (
|
{coinPacks && coinPacks.length > 0 ? (
|
||||||
<View style={styles.creditsSection}>
|
<View style={styles.creditsSection}>
|
||||||
<Text style={styles.sectionTitle}>Recharger vos crédits</Text>
|
<Text style={styles.sectionTitle}>{COIN_PACK_SECTION_TITLE}</Text>
|
||||||
<View style={styles.creditsGrid}>
|
<View style={styles.creditsGrid}>
|
||||||
{coinPacks.map((pack) => {
|
{coinPacks.map((pack) => {
|
||||||
// Attempt to find amount in commonly used Stripe fields
|
// 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 details = packKey ? COIN_PACK_DETAILS[packKey] : null
|
||||||
const displayName = details?.label || pack.name
|
const displayName = details?.label || pack.name
|
||||||
const creditsText = details?.creditsDisplay
|
const creditsText = details?.creditsDisplay
|
||||||
|
const displayPrice = details?.priceDisplay || price || `${rawAmount / 100}€`
|
||||||
|
const isPopular = details?.popular === true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={pack.productId} style={styles.creditCard}>
|
<View key={pack.productId} style={styles.creditCard}>
|
||||||
@@ -803,12 +675,13 @@ export default function Subscriptions() {
|
|||||||
<Text style={styles.packDisclaimer}>{details.disclaimer}</Text>
|
<Text style={styles.packDisclaimer}>{details.disclaimer}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
{isPopular ? (
|
||||||
|
<View style={styles.popularBadge}>
|
||||||
|
<Text style={styles.popularBadgeText}>Le plus populaire !</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title={
|
title={processingPriceId === pack.productId ? '...' : displayPrice}
|
||||||
processingPriceId === pack.productId
|
|
||||||
? '...'
|
|
||||||
: price || `${rawAmount / 100}€`
|
|
||||||
}
|
|
||||||
onPress={() => handleBuyCredits(pack)}
|
onPress={() => handleBuyCredits(pack)}
|
||||||
disabled={Boolean(processingPriceId)}
|
disabled={Boolean(processingPriceId)}
|
||||||
style={styles.creditButton}
|
style={styles.creditButton}
|
||||||
@@ -933,6 +806,19 @@ const styles = StyleSheet.create({
|
|||||||
paddingTop: 12,
|
paddingTop: 12,
|
||||||
gap: 6,
|
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: {
|
cardBenefitsItem: {
|
||||||
fontFamily: FONT_FAMILY.InterMedium,
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@@ -1008,7 +894,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'flex-start',
|
||||||
width: isWeb ? 200 : '45%',
|
width: isWeb ? 200 : '45%',
|
||||||
gap: 12,
|
gap: 12,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
@@ -1043,6 +929,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
},
|
},
|
||||||
creditButton: {
|
creditButton: {
|
||||||
|
marginTop: 'auto',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 36,
|
height: 36,
|
||||||
minHeight: 36,
|
minHeight: 36,
|
||||||
@@ -1207,11 +1094,12 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: gutters,
|
paddingHorizontal: gutters,
|
||||||
paddingVertical: isWeb ? gutters : Math.max(gutters * 0.2, 10),
|
paddingVertical: isWeb ? gutters : Math.max(gutters * 0.2, 10),
|
||||||
gap: isWeb ? 18 : 10,
|
gap: isWeb ? 18 : 10,
|
||||||
justifyContent: 'center',
|
justifyContent: 'flex-start',
|
||||||
backgroundColor: 'rgba(48, 52, 56, 0.55)',
|
backgroundColor: 'rgba(48, 52, 56, 0.55)',
|
||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
},
|
},
|
||||||
cardContent: {
|
cardContent: {
|
||||||
|
flex: 1,
|
||||||
gap: isWeb ? 16 : 4,
|
gap: isWeb ? 16 : 4,
|
||||||
},
|
},
|
||||||
cardHeader: {
|
cardHeader: {
|
||||||
@@ -1244,6 +1132,11 @@ const styles = StyleSheet.create({
|
|||||||
priceBlock: {
|
priceBlock: {
|
||||||
gap: isWeb ? 4 : 1,
|
gap: isWeb ? 4 : 1,
|
||||||
},
|
},
|
||||||
|
secondaryPriceText: {
|
||||||
|
fontFamily: FONT_FAMILY.InterBold,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Palette.primary,
|
||||||
|
},
|
||||||
priceRow: {
|
priceRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'baseline',
|
alignItems: 'baseline',
|
||||||
@@ -1289,7 +1182,7 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
popularBadge: {
|
popularBadge: {
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
marginTop: 12,
|
marginTop: 'auto',
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
|
|||||||
@@ -112,27 +112,37 @@ const SongDownload = ({ route }) => {
|
|||||||
const canDownloadDirectly = hasActiveSubscription || hasPaidDownload || hasPurchased
|
const canDownloadDirectly = hasActiveSubscription || hasPaidDownload || hasPurchased
|
||||||
const downloadLabel = canDownloadDirectly
|
const downloadLabel = canDownloadDirectly
|
||||||
? 'Télécharger mon morceau'
|
? 'Télécharger mon morceau'
|
||||||
: 'Acheter ce morceau pour 1,99€'
|
: 'Télécharger ce morceau pour 1,99€'
|
||||||
const showSubscriptionCta = !hasActiveSubscription
|
|
||||||
const shouldShowDownloadPage = adventureChoice === 'stop'
|
const shouldShowDownloadPage = adventureChoice === 'stop'
|
||||||
|
|
||||||
|
const reopenAdventureChoice = useCallback(() => {
|
||||||
|
reopenAdventureModalOnFocusRef.current = false
|
||||||
|
setShowIntro(null)
|
||||||
|
setAdventureChoice('pending')
|
||||||
|
setShowAdventureModal(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleBackPress = useCallback(() => {
|
const handleBackPress = useCallback(() => {
|
||||||
|
if (shouldShowDownloadPage) {
|
||||||
|
reopenAdventureChoice()
|
||||||
|
return true
|
||||||
|
}
|
||||||
if (backRoute) {
|
if (backRoute) {
|
||||||
navigate(backRoute)
|
navigate(backRoute)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
goBack()
|
goBack()
|
||||||
return true
|
return true
|
||||||
}, [backRoute])
|
}, [backRoute, reopenAdventureChoice, shouldShowDownloadPage])
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
if (Platform.OS !== 'android' || !backRoute) {
|
if (Platform.OS !== 'android' || (!backRoute && !shouldShowDownloadPage)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress)
|
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress)
|
||||||
return () => subscription.remove()
|
return () => subscription.remove()
|
||||||
}, [backRoute, handleBackPress])
|
}, [backRoute, handleBackPress, shouldShowDownloadPage])
|
||||||
)
|
)
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
@@ -512,8 +522,8 @@ const SongDownload = ({ route }) => {
|
|||||||
}
|
}
|
||||||
if (promptPurchaseConfirm) {
|
if (promptPurchaseConfirm) {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Acheter ce morceau',
|
'Télécharger ce morceau',
|
||||||
'Voulez-vous acheter ce morceau pour 1,99€ ?',
|
'Voulez-vous télécharger ce morceau pour 1,99€ ?',
|
||||||
[
|
[
|
||||||
{ text: 'Annuler', style: 'cancel' },
|
{ text: 'Annuler', style: 'cancel' },
|
||||||
{ text: 'Acheter', onPress: () => startSongDownloadCheckout() },
|
{ text: 'Acheter', onPress: () => startSongDownloadCheckout() },
|
||||||
@@ -569,38 +579,39 @@ const SongDownload = ({ route }) => {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<CreateLyricsHeader title="Ta pochette est validée" />
|
<CreateLyricsHeader title="Ta pochette est validée" />
|
||||||
<View style={styles.coverRow}>
|
<ClubAdvantagesCard
|
||||||
{coverUrl ? (
|
style={styles.clubCardSpacing}
|
||||||
<ExpoImage source={{ uri: coverUrl }} style={styles.coverImage} contentFit="cover" />
|
topContent={
|
||||||
) : (
|
<View style={styles.coverRow}>
|
||||||
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
{coverUrl ? (
|
||||||
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
<ExpoImage source={{ uri: coverUrl }} style={styles.coverImage} contentFit="cover" />
|
||||||
|
) : (
|
||||||
|
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
||||||
|
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.downloadColumn}>
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.downloadTile,
|
||||||
|
(isDownloading || isCheckoutLaunching) && { opacity: 0.6 },
|
||||||
|
]}
|
||||||
|
onPress={handleDownloadPress}
|
||||||
|
disabled={isDownloading || isCheckoutLaunching}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
||||||
|
<Text style={styles.downloadText}>{downloadLabel}</Text>
|
||||||
|
</Pressable>
|
||||||
|
{!canDownloadDirectly && downloadPaymentPending ? (
|
||||||
|
<Text style={styles.downloadPendingText}>
|
||||||
|
Paiement en attente de confirmation...
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
}
|
||||||
|
/>
|
||||||
<View style={styles.downloadColumn}>
|
|
||||||
<Pressable
|
|
||||||
style={[
|
|
||||||
styles.downloadTile,
|
|
||||||
(isDownloading || isCheckoutLaunching) && { opacity: 0.6 },
|
|
||||||
]}
|
|
||||||
onPress={handleDownloadPress}
|
|
||||||
disabled={isDownloading || isCheckoutLaunching}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
|
||||||
<Text style={styles.downloadText}>{downloadLabel}</Text>
|
|
||||||
</Pressable>
|
|
||||||
{!canDownloadDirectly && downloadPaymentPending ? (
|
|
||||||
<Text style={styles.downloadPendingText}>
|
|
||||||
Paiement en attente de confirmation...
|
|
||||||
</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ClubAdvantagesCard style={styles.clubCardSpacing} />
|
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Revenir à l'accueil"
|
title="Revenir à l'accueil"
|
||||||
onPress={handleGoHome}
|
onPress={handleGoHome}
|
||||||
@@ -695,22 +706,6 @@ const styles = StyleSheet.create({
|
|||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
opacity: 0.8,
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
subscriptionCta: {
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: gutters * 0.6,
|
|
||||||
marginTop: gutters * 0.4,
|
|
||||||
},
|
|
||||||
subscriptionNote: {
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
fontSize: 13,
|
|
||||||
color: Palette.white,
|
|
||||||
opacity: 0.8,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
subscriptionButton: {
|
|
||||||
width: '100%',
|
|
||||||
maxWidth: 280,
|
|
||||||
},
|
|
||||||
adventureGatePlaceholder: {
|
adventureGatePlaceholder: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -20,11 +20,20 @@ import { useStripe } from '../../../providers/StripeProvider'
|
|||||||
import CreditAmount from '../../../components/CreditAmount'
|
import CreditAmount from '../../../components/CreditAmount'
|
||||||
import GradientButton from '../../../components/GradientButton'
|
import GradientButton from '../../../components/GradientButton'
|
||||||
import AppCheckbox from '../../../components/AppCheckbox'
|
import AppCheckbox from '../../../components/AppCheckbox'
|
||||||
import { subBadges, icons, planBadges } from '../../../assets'
|
import { icons, planBadges } from '../../../assets'
|
||||||
import { isWeb } from '../../../hooks/useLayoutType'
|
import { isWeb } from '../../../hooks/useLayoutType'
|
||||||
import { navigate } from '../../../navigation/NavigationService'
|
import { navigate } from '../../../navigation/NavigationService'
|
||||||
import { Routes } from '../../../navigation'
|
import { Routes } from '../../../navigation'
|
||||||
import { musiclandShareHeading } from '../../../data'
|
import { musiclandShareHeading } from '../../../data'
|
||||||
|
import {
|
||||||
|
COIN_PACK_DETAILS,
|
||||||
|
COIN_PACK_SECTION_TITLE,
|
||||||
|
getCoinPackKey,
|
||||||
|
} from '../../../utils/coinPackDisplay'
|
||||||
|
import {
|
||||||
|
getSubscriptionCardDisplay,
|
||||||
|
getSubscriptionPlanKey,
|
||||||
|
} from '../../../utils/subscriptionCardDisplay'
|
||||||
|
|
||||||
// --- HELPER FUNCTIONS & CONSTANTS (Copied from Subscriptions.js) ---
|
// --- HELPER FUNCTIONS & CONSTANTS (Copied from Subscriptions.js) ---
|
||||||
|
|
||||||
@@ -69,122 +78,25 @@ const getIntervalLabel = (recurring) => {
|
|||||||
return `tous les ${count} ${count > 1 ? terms.plural : terms.singular}`
|
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) ---
|
// --- SUBSCRIPTION CARD COMPONENT (Copied) ---
|
||||||
|
|
||||||
function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
||||||
const planKey = getPlanKeyForBadge(plan)
|
const planKey = getSubscriptionPlanKey(plan)
|
||||||
const planName =
|
|
||||||
(planKey && PLAN_DISPLAY_NAME_BY_KEY[planKey]) ||
|
|
||||||
plan?.product?.name ||
|
|
||||||
plan?.nickname ||
|
|
||||||
plan?.priceId ||
|
|
||||||
'Abonnement'
|
|
||||||
const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency)
|
const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency)
|
||||||
const intervalLabel = getIntervalLabel(plan?.recurring)
|
const intervalLabel = getIntervalLabel(plan?.recurring)
|
||||||
const coinsPerMonth =
|
const coinsPerMonth =
|
||||||
typeof plan?.coinsPerMonth === 'number' && Number.isFinite(plan.coinsPerMonth)
|
typeof plan?.coinsPerMonth === 'number' && Number.isFinite(plan.coinsPerMonth)
|
||||||
? Math.round(plan.coinsPerMonth)
|
? Math.round(plan.coinsPerMonth)
|
||||||
: null
|
: null
|
||||||
const details = planKey ? PLAN_DETAILS[planKey] : null
|
const cardDisplay = getSubscriptionCardDisplay({
|
||||||
const isPopular = details?.popular
|
planKey,
|
||||||
|
isAnnual,
|
||||||
|
formattedPrice,
|
||||||
|
intervalLabel,
|
||||||
|
coinsPerMonth,
|
||||||
|
fallbackTitle: plan?.product?.name || plan?.nickname || plan?.priceId || 'Abonnement',
|
||||||
|
})
|
||||||
|
const isPopular = cardDisplay.popular
|
||||||
|
|
||||||
const planBadgeKey = planKey
|
const planBadgeKey = planKey
|
||||||
const planBadgeSource = planBadgeKey && planBadges[planBadgeKey] ? planBadges[planBadgeKey] : null
|
const planBadgeSource = planBadgeKey && planBadges[planBadgeKey] ? planBadges[planBadgeKey] : null
|
||||||
@@ -217,7 +129,7 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
|||||||
|
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<View style={styles.titleRow}>
|
<View style={styles.titleRow}>
|
||||||
<Text style={styles.planName}>{planName}</Text>
|
<Text style={styles.planName}>{cardDisplay.title}</Text>
|
||||||
{planBadgeSource ? (
|
{planBadgeSource ? (
|
||||||
<ExpoImage
|
<ExpoImage
|
||||||
source={planBadgeSource}
|
source={planBadgeSource}
|
||||||
@@ -226,46 +138,37 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
|||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
{plan?.nickname ? <Text style={styles.cardSubtitle}>{plan.nickname}</Text> : null}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{formattedPrice || coinsPerMonth !== null ? (
|
{cardDisplay.primaryPrice || cardDisplay.creditsLabel ? (
|
||||||
<View style={styles.priceBlock}>
|
<View style={styles.priceBlock}>
|
||||||
{formattedPrice ? (
|
{cardDisplay.primaryPrice ? (
|
||||||
<View style={styles.priceRow}>
|
<Text style={styles.priceValue}>{cardDisplay.primaryPrice}</Text>
|
||||||
<Text style={styles.priceValue}>{formattedPrice}</Text>
|
|
||||||
{intervalLabel ? <Text style={styles.period}>{intervalLabel}</Text> : null}
|
|
||||||
</View>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
{cardDisplay.secondaryPrice ? (
|
||||||
{details?.creditsLabel ? (
|
<Text style={styles.secondaryPriceText}>{cardDisplay.secondaryPrice}</Text>
|
||||||
<Text style={styles.coinsPromoText}>{details.creditsLabel}</Text>
|
) : null}
|
||||||
) : coinsPerMonth !== null ? (
|
{cardDisplay.creditsLabel ? (
|
||||||
<View style={styles.coinsPerMonthRow}>
|
<Text style={styles.coinsPromoText}>{cardDisplay.creditsLabel}</Text>
|
||||||
<CreditAmount
|
|
||||||
value={coinsPerMonth}
|
|
||||||
showPlus
|
|
||||||
textStyle={styles.coinsPerMonth}
|
|
||||||
iconSize={16}
|
|
||||||
/>
|
|
||||||
<Text style={styles.coinsPerMonthSuffix}>/ mois</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<View style={styles.cardBenefits}>
|
{Array.isArray(cardDisplay.benefitItems) && cardDisplay.benefitItems.length ? (
|
||||||
<Text style={styles.benefitsTitle}>Privilège Membre :</Text>
|
<View style={styles.cardBenefits}>
|
||||||
{details?.privileges?.map((item, index) => (
|
{cardDisplay.benefitItems.map((item, index) =>
|
||||||
<View key={index} style={styles.benefitBlock}>
|
item?.type === 'plus' ? (
|
||||||
<Text style={[styles.benefitText, item.bold && styles.benefitTextBold]}>
|
<Text key={`benefit-plus-${index}`} style={styles.benefitPlus}>
|
||||||
{item.bold ? `• ${item.text}` : item.text}
|
{item.text}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
) : (
|
||||||
)) || (
|
<Text key={`benefit-text-${index}`} style={styles.downloadLabel}>
|
||||||
<Text style={styles.cardBenefitsItem}>• Crédits gratuits tous les mois</Text>
|
{item.text}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{isPopular ? (
|
{isPopular ? (
|
||||||
<View style={styles.popularBadge}>
|
<View style={styles.popularBadge}>
|
||||||
@@ -274,11 +177,6 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
|||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</BlurView>
|
</BlurView>
|
||||||
{isAnnual ? (
|
|
||||||
<View style={styles.annualBadge}>
|
|
||||||
<Text style={styles.annualBadgeText}>2 mois offert</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -301,7 +199,7 @@ const ShareAndCreditsModal = ({
|
|||||||
isCatalogLoading,
|
isCatalogLoading,
|
||||||
} = useStripe()
|
} = useStripe()
|
||||||
|
|
||||||
const [selectedPeriodKey, setSelectedPeriodKey] = useState('annual')
|
const [selectedPeriodKey, setSelectedPeriodKey] = useState('monthly')
|
||||||
const [selectedPriceId, setSelectedPriceId] = useState(null)
|
const [selectedPriceId, setSelectedPriceId] = useState(null)
|
||||||
const [processingPriceId, setProcessingPriceId] = useState(null)
|
const [processingPriceId, setProcessingPriceId] = useState(null)
|
||||||
const [errorMessage, setErrorMessage] = useState(null)
|
const [errorMessage, setErrorMessage] = useState(null)
|
||||||
@@ -422,7 +320,7 @@ const ShareAndCreditsModal = ({
|
|||||||
{/* CREDITS SECTION */}
|
{/* CREDITS SECTION */}
|
||||||
{coinPacks && coinPacks.length > 0 ? (
|
{coinPacks && coinPacks.length > 0 ? (
|
||||||
<View style={styles.sectionContainer}>
|
<View style={styles.sectionContainer}>
|
||||||
<Text style={styles.sectionTitle}>Recharger vos crédits</Text>
|
<Text style={styles.sectionTitle}>{COIN_PACK_SECTION_TITLE}</Text>
|
||||||
<View style={styles.creditsGrid}>
|
<View style={styles.creditsGrid}>
|
||||||
{coinPacks.map((pack) => {
|
{coinPacks.map((pack) => {
|
||||||
const rawAmount = pack.unitAmount ?? pack.amount ?? pack.price ?? 0
|
const rawAmount = pack.unitAmount ?? pack.amount ?? pack.price ?? 0
|
||||||
@@ -432,6 +330,8 @@ const ShareAndCreditsModal = ({
|
|||||||
const details = packKey ? COIN_PACK_DETAILS[packKey] : null
|
const details = packKey ? COIN_PACK_DETAILS[packKey] : null
|
||||||
const displayName = details?.label || pack.name
|
const displayName = details?.label || pack.name
|
||||||
const creditsText = details?.creditsDisplay
|
const creditsText = details?.creditsDisplay
|
||||||
|
const displayPrice = details?.priceDisplay || price || `${rawAmount / 100}€`
|
||||||
|
const isPopular = details?.popular === true
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={pack.productId} style={styles.creditCard}>
|
<View key={pack.productId} style={styles.creditCard}>
|
||||||
@@ -450,12 +350,13 @@ const ShareAndCreditsModal = ({
|
|||||||
<Text style={styles.packDisclaimer}>{details.disclaimer}</Text>
|
<Text style={styles.packDisclaimer}>{details.disclaimer}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
{isPopular ? (
|
||||||
|
<View style={styles.popularBadge}>
|
||||||
|
<Text style={styles.popularBadgeText}>Le plus populaire !</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title={
|
title={processingPriceId === pack.productId ? '...' : displayPrice}
|
||||||
processingPriceId === pack.productId
|
|
||||||
? '...'
|
|
||||||
: price || `${rawAmount / 100}€`
|
|
||||||
}
|
|
||||||
onPress={() => handleBuyCredits(pack)}
|
onPress={() => handleBuyCredits(pack)}
|
||||||
disabled={Boolean(processingPriceId)}
|
disabled={Boolean(processingPriceId)}
|
||||||
style={styles.creditButton}
|
style={styles.creditButton}
|
||||||
@@ -636,7 +537,7 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'flex-start',
|
||||||
width: isWeb ? 220 : '100%',
|
width: isWeb ? 220 : '100%',
|
||||||
minHeight: 160,
|
minHeight: 160,
|
||||||
gap: 12,
|
gap: 12,
|
||||||
@@ -672,6 +573,7 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
},
|
},
|
||||||
creditButton: {
|
creditButton: {
|
||||||
|
marginTop: 'auto',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 36,
|
height: 36,
|
||||||
minHeight: 36,
|
minHeight: 36,
|
||||||
@@ -758,6 +660,10 @@ const styles = StyleSheet.create({
|
|||||||
padding: 16,
|
padding: 16,
|
||||||
gap: 16,
|
gap: 16,
|
||||||
},
|
},
|
||||||
|
cardContent: {
|
||||||
|
flex: 1,
|
||||||
|
gap: 16,
|
||||||
|
},
|
||||||
badge: {
|
badge: {
|
||||||
alignSelf: 'flex-start',
|
alignSelf: 'flex-start',
|
||||||
backgroundColor: 'rgba(112, 35, 247, 0.25)',
|
backgroundColor: 'rgba(112, 35, 247, 0.25)',
|
||||||
@@ -795,6 +701,11 @@ const styles = StyleSheet.create({
|
|||||||
priceBlock: {
|
priceBlock: {
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
secondaryPriceText: {
|
||||||
|
color: Palette.primary,
|
||||||
|
fontSize: 15,
|
||||||
|
fontFamily: FONT_FAMILY.InterBold,
|
||||||
|
},
|
||||||
priceRow: {
|
priceRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'baseline',
|
alignItems: 'baseline',
|
||||||
@@ -830,6 +741,19 @@ const styles = StyleSheet.create({
|
|||||||
cardBenefits: {
|
cardBenefits: {
|
||||||
gap: 6,
|
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: {
|
benefitsTitle: {
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -856,7 +780,7 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
marginTop: 12,
|
marginTop: 'auto',
|
||||||
},
|
},
|
||||||
popularBadgeText: {
|
popularBadgeText: {
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
|
|||||||
@@ -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
@@ -1,21 +1,25 @@
|
|||||||
const listeners = new Set()
|
const listeners = new Map()
|
||||||
|
let nextListenerId = 0
|
||||||
|
|
||||||
export const subscribeCoinPackModal = (listener) => {
|
export const subscribeCoinPackModal = (listener) => {
|
||||||
if (typeof listener !== 'function') {
|
if (typeof listener !== 'function') {
|
||||||
return () => {}
|
return () => {}
|
||||||
}
|
}
|
||||||
listeners.add(listener)
|
const listenerId = ++nextListenerId
|
||||||
|
listeners.set(listenerId, listener)
|
||||||
return () => {
|
return () => {
|
||||||
listeners.delete(listener)
|
listeners.delete(listenerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openCoinPackModal = () => {
|
export const openCoinPackModal = () => {
|
||||||
listeners.forEach((listener) => {
|
const activeListener = Array.from(listeners.values()).pop()
|
||||||
try {
|
if (!activeListener) {
|
||||||
listener()
|
return
|
||||||
} catch (error) {
|
}
|
||||||
console.error('[coinPackModal] open listener error', error)
|
try {
|
||||||
}
|
activeListener()
|
||||||
})
|
} catch (error) {
|
||||||
|
console.error('[coinPackModal] open listener error', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user