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
+13 -25
View File
@@ -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 (
<>
<Pressable onPress={handlePress} accessibilityRole="button" style={[styles.container, style]}>
<CreditAmount
value={coinBalance}
style={styles.content}
textStyle={[styles.text, textStyle]}
iconSize={iconSize}
iconPosition={iconPosition}
/>
</Pressable>
<CoinPackModal visible={isModalVisible} onClose={handleClose} />
</>
<Pressable onPress={handlePress} accessibilityRole="button" style={[styles.container, style]}>
<CreditAmount
value={coinBalance}
style={styles.content}
textStyle={[styles.text, textStyle]}
iconSize={iconSize}
iconPosition={iconPosition}
/>
</Pressable>
)
}
+34 -35
View File
@@ -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 (
<Pressable
@@ -235,8 +215,13 @@ function CoinPackCard({ pack, selected, pricingDetail, onSelect, staticDiscountP
) : null}
</View>
<View style={styles.priceBlock}>
{formattedPrice ? <Text style={styles.packPrice}>{formattedPrice}</Text> : null}
{displayPrice ? <Text style={styles.packPrice}>{displayPrice}</Text> : null}
</View>
{isPopular ? (
<View style={styles.popularBadge}>
<Text style={styles.popularBadgeText}>Le plus populaire !</Text>
</View>
) : null}
</BlurView>
</Pressable>
)
@@ -324,7 +309,7 @@ const CoinPackModal = ({ visible, onClose }) => {
if (!coinPacks.length) {
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.header}>
<Text style={styles.title}>Acheter des pièces</Text>
<Text style={styles.title}>{COIN_PACK_SECTION_TITLE}</Text>
<Text style={styles.subtitle}>
Choisis un pack et finalise ton achat pour continuer.
Crée tes chansons et tes vidéos.
</Text>
</View>
@@ -378,8 +363,7 @@ const CoinPackModal = ({ visible, onClose }) => {
<View style={styles.content}>{renderContent()}</View>
<Text style={styles.indicativeNote}>
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.
</Text>
<View style={styles.actions}>
@@ -394,7 +378,7 @@ const CoinPackModal = ({ visible, onClose }) => {
</View>
<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>
</View>
</CreateLyricsHeader>
@@ -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,