feat(ui): refine club, packs, and download flows
This commit is contained in:
@@ -112,27 +112,37 @@ const SongDownload = ({ route }) => {
|
||||
const canDownloadDirectly = hasActiveSubscription || hasPaidDownload || hasPurchased
|
||||
const downloadLabel = canDownloadDirectly
|
||||
? 'Télécharger mon morceau'
|
||||
: 'Acheter ce morceau pour 1,99€'
|
||||
const showSubscriptionCta = !hasActiveSubscription
|
||||
: 'Télécharger ce morceau pour 1,99€'
|
||||
const shouldShowDownloadPage = adventureChoice === 'stop'
|
||||
|
||||
const reopenAdventureChoice = useCallback(() => {
|
||||
reopenAdventureModalOnFocusRef.current = false
|
||||
setShowIntro(null)
|
||||
setAdventureChoice('pending')
|
||||
setShowAdventureModal(true)
|
||||
}, [])
|
||||
|
||||
const handleBackPress = useCallback(() => {
|
||||
if (shouldShowDownloadPage) {
|
||||
reopenAdventureChoice()
|
||||
return true
|
||||
}
|
||||
if (backRoute) {
|
||||
navigate(backRoute)
|
||||
return true
|
||||
}
|
||||
goBack()
|
||||
return true
|
||||
}, [backRoute])
|
||||
}, [backRoute, reopenAdventureChoice, shouldShowDownloadPage])
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
if (Platform.OS !== 'android' || !backRoute) {
|
||||
if (Platform.OS !== 'android' || (!backRoute && !shouldShowDownloadPage)) {
|
||||
return undefined
|
||||
}
|
||||
const subscription = BackHandler.addEventListener('hardwareBackPress', handleBackPress)
|
||||
return () => subscription.remove()
|
||||
}, [backRoute, handleBackPress])
|
||||
}, [backRoute, handleBackPress, shouldShowDownloadPage])
|
||||
)
|
||||
|
||||
useFocusEffect(
|
||||
@@ -512,8 +522,8 @@ const SongDownload = ({ route }) => {
|
||||
}
|
||||
if (promptPurchaseConfirm) {
|
||||
Alert.alert(
|
||||
'Acheter ce morceau',
|
||||
'Voulez-vous acheter ce morceau pour 1,99€ ?',
|
||||
'Télécharger ce morceau',
|
||||
'Voulez-vous télécharger ce morceau pour 1,99€ ?',
|
||||
[
|
||||
{ text: 'Annuler', style: 'cancel' },
|
||||
{ text: 'Acheter', onPress: () => startSongDownloadCheckout() },
|
||||
@@ -569,38 +579,39 @@ const SongDownload = ({ route }) => {
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<CreateLyricsHeader title="Ta pochette est validée" />
|
||||
<View style={styles.coverRow}>
|
||||
{coverUrl ? (
|
||||
<ExpoImage source={{ uri: coverUrl }} style={styles.coverImage} contentFit="cover" />
|
||||
) : (
|
||||
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
||||
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
||||
<ClubAdvantagesCard
|
||||
style={styles.clubCardSpacing}
|
||||
topContent={
|
||||
<View style={styles.coverRow}>
|
||||
{coverUrl ? (
|
||||
<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 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
|
||||
title="Revenir à l'accueil"
|
||||
onPress={handleGoHome}
|
||||
@@ -695,22 +706,6 @@ const styles = StyleSheet.create({
|
||||
color: Palette.white,
|
||||
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: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
@@ -20,11 +20,20 @@ import { useStripe } from '../../../providers/StripeProvider'
|
||||
import CreditAmount from '../../../components/CreditAmount'
|
||||
import GradientButton from '../../../components/GradientButton'
|
||||
import AppCheckbox from '../../../components/AppCheckbox'
|
||||
import { subBadges, icons, planBadges } from '../../../assets'
|
||||
import { icons, planBadges } from '../../../assets'
|
||||
import { isWeb } from '../../../hooks/useLayoutType'
|
||||
import { navigate } from '../../../navigation/NavigationService'
|
||||
import { Routes } from '../../../navigation'
|
||||
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) ---
|
||||
|
||||
@@ -69,122 +78,25 @@ const getIntervalLabel = (recurring) => {
|
||||
return `tous les ${count} ${count > 1 ? terms.plural : terms.singular}`
|
||||
}
|
||||
|
||||
const PLAN_SYNONYMS = {
|
||||
starter: ['starter'],
|
||||
pro: ['pro'],
|
||||
premium: ['premium'],
|
||||
}
|
||||
|
||||
const PLAN_DISPLAY_NAME_BY_KEY = {
|
||||
starter: 'Play Backer Blue',
|
||||
pro: 'Play Backer Silver',
|
||||
premium: 'Play Backer Gold',
|
||||
}
|
||||
|
||||
const PACK_PRICE_ID_BY_PERIOD = {
|
||||
monthly: {
|
||||
starter: 'price_1SPgitCzf2o5bDRdbnhLFx6f',
|
||||
pro: 'price_1SPgjCCzf2o5bDRdr08Xzp8u',
|
||||
premium: 'price_1SPgjaCzf2o5bDRdd9Xo2u26',
|
||||
},
|
||||
annual: {
|
||||
starter: 'price_1SPgkDCzf2o5bDRdNGLVNeQ3',
|
||||
pro: 'price_1SPgkXCzf2o5bDRdejBVxEBY',
|
||||
premium: 'price_1SPgkqCzf2o5bDRdIcUwTDrm',
|
||||
},
|
||||
}
|
||||
|
||||
const PRICE_ID_TO_PLAN_KEY = {}
|
||||
Object.values(PACK_PRICE_ID_BY_PERIOD).forEach((mapping) => {
|
||||
Object.entries(mapping).forEach(([planKey, priceId]) => {
|
||||
if (!priceId) return
|
||||
PRICE_ID_TO_PLAN_KEY[priceId] = planKey
|
||||
})
|
||||
})
|
||||
|
||||
const PLAN_DETAILS = {
|
||||
starter: {
|
||||
creditsLabel: '50 Crédits / Mois',
|
||||
popular: false,
|
||||
privileges: [
|
||||
{ text: 'Eligible au concours mensuel chanson/Vidéo', bold: true },
|
||||
],
|
||||
},
|
||||
pro: {
|
||||
creditsLabel: '100 crédits + 50 offerts / Mois',
|
||||
popular: true,
|
||||
privileges: [
|
||||
{ text: 'Eligible au concours mensuel chanson/Vidéo', bold: true },
|
||||
],
|
||||
},
|
||||
premium: {
|
||||
creditsLabel: '150 Crédits + 100 offerts / Mois',
|
||||
popular: false,
|
||||
privileges: [
|
||||
{ text: 'Eligible au concours mensuel chanson/Vidéo', bold: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
const COIN_PACK_DETAILS = {
|
||||
starter: {
|
||||
label: 'Pack starter',
|
||||
creditsDisplay: '50 Crédits',
|
||||
disclaimer: '*Non-Eligible au concours Chanson/Vidéo',
|
||||
},
|
||||
medium: {
|
||||
label: 'Pack Medium',
|
||||
creditsDisplay: '100 Crédits + 50 offerts',
|
||||
disclaimer: '*Non-Eligible au concours Chanson/Vidéo',
|
||||
},
|
||||
premium: {
|
||||
label: 'Pack Premium',
|
||||
creditsDisplay: '150 Crédits + 100 offerts',
|
||||
disclaimer: '*Non-Eligible au concours Chanson/Vidéo',
|
||||
},
|
||||
}
|
||||
|
||||
const getCoinPackKey = (pack) => {
|
||||
const name = (pack?.name || '').toLowerCase()
|
||||
if (name.includes('starter')) return 'starter'
|
||||
if (name.includes('medium')) return 'medium'
|
||||
if (name.includes('premium') || name.includes('prenium') || name.includes('creator'))
|
||||
return 'premium'
|
||||
return null
|
||||
}
|
||||
|
||||
const getPlanKeyForBadge = (plan) => {
|
||||
if (!plan || typeof plan !== 'object') return null
|
||||
const priceId = typeof plan?.priceId === 'string' ? plan.priceId : plan?.id || null
|
||||
if (priceId && PRICE_ID_TO_PLAN_KEY[priceId]) {
|
||||
return PRICE_ID_TO_PLAN_KEY[priceId]
|
||||
}
|
||||
const label = (plan?.product?.name || plan?.nickname || '').toString().toLowerCase()
|
||||
if (!label) return null
|
||||
const match = Object.entries(PLAN_SYNONYMS).find(([, variants]) =>
|
||||
variants.some((variant) => label.includes(variant))
|
||||
)
|
||||
return match ? match[0] : null
|
||||
}
|
||||
|
||||
// --- SUBSCRIPTION CARD COMPONENT (Copied) ---
|
||||
|
||||
function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
||||
const planKey = getPlanKeyForBadge(plan)
|
||||
const planName =
|
||||
(planKey && PLAN_DISPLAY_NAME_BY_KEY[planKey]) ||
|
||||
plan?.product?.name ||
|
||||
plan?.nickname ||
|
||||
plan?.priceId ||
|
||||
'Abonnement'
|
||||
const planKey = getSubscriptionPlanKey(plan)
|
||||
const formattedPrice = formatCurrency(plan?.unitAmount, plan?.currency)
|
||||
const intervalLabel = getIntervalLabel(plan?.recurring)
|
||||
const coinsPerMonth =
|
||||
typeof plan?.coinsPerMonth === 'number' && Number.isFinite(plan.coinsPerMonth)
|
||||
? Math.round(plan.coinsPerMonth)
|
||||
: null
|
||||
const details = planKey ? PLAN_DETAILS[planKey] : null
|
||||
const isPopular = details?.popular
|
||||
const cardDisplay = getSubscriptionCardDisplay({
|
||||
planKey,
|
||||
isAnnual,
|
||||
formattedPrice,
|
||||
intervalLabel,
|
||||
coinsPerMonth,
|
||||
fallbackTitle: plan?.product?.name || plan?.nickname || plan?.priceId || 'Abonnement',
|
||||
})
|
||||
const isPopular = cardDisplay.popular
|
||||
|
||||
const planBadgeKey = planKey
|
||||
const planBadgeSource = planBadgeKey && planBadges[planBadgeKey] ? planBadges[planBadgeKey] : null
|
||||
@@ -217,7 +129,7 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
||||
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={styles.titleRow}>
|
||||
<Text style={styles.planName}>{planName}</Text>
|
||||
<Text style={styles.planName}>{cardDisplay.title}</Text>
|
||||
{planBadgeSource ? (
|
||||
<ExpoImage
|
||||
source={planBadgeSource}
|
||||
@@ -226,46 +138,37 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{plan?.nickname ? <Text style={styles.cardSubtitle}>{plan.nickname}</Text> : null}
|
||||
</View>
|
||||
|
||||
{formattedPrice || coinsPerMonth !== null ? (
|
||||
{cardDisplay.primaryPrice || cardDisplay.creditsLabel ? (
|
||||
<View style={styles.priceBlock}>
|
||||
{formattedPrice ? (
|
||||
<View style={styles.priceRow}>
|
||||
<Text style={styles.priceValue}>{formattedPrice}</Text>
|
||||
{intervalLabel ? <Text style={styles.period}>{intervalLabel}</Text> : null}
|
||||
</View>
|
||||
{cardDisplay.primaryPrice ? (
|
||||
<Text style={styles.priceValue}>{cardDisplay.primaryPrice}</Text>
|
||||
) : null}
|
||||
|
||||
{details?.creditsLabel ? (
|
||||
<Text style={styles.coinsPromoText}>{details.creditsLabel}</Text>
|
||||
) : coinsPerMonth !== null ? (
|
||||
<View style={styles.coinsPerMonthRow}>
|
||||
<CreditAmount
|
||||
value={coinsPerMonth}
|
||||
showPlus
|
||||
textStyle={styles.coinsPerMonth}
|
||||
iconSize={16}
|
||||
/>
|
||||
<Text style={styles.coinsPerMonthSuffix}>/ mois</Text>
|
||||
</View>
|
||||
{cardDisplay.secondaryPrice ? (
|
||||
<Text style={styles.secondaryPriceText}>{cardDisplay.secondaryPrice}</Text>
|
||||
) : null}
|
||||
{cardDisplay.creditsLabel ? (
|
||||
<Text style={styles.coinsPromoText}>{cardDisplay.creditsLabel}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<View style={styles.cardBenefits}>
|
||||
<Text style={styles.benefitsTitle}>Privilège Membre :</Text>
|
||||
{details?.privileges?.map((item, index) => (
|
||||
<View key={index} style={styles.benefitBlock}>
|
||||
<Text style={[styles.benefitText, item.bold && styles.benefitTextBold]}>
|
||||
{item.bold ? `• ${item.text}` : item.text}
|
||||
</Text>
|
||||
</View>
|
||||
)) || (
|
||||
<Text style={styles.cardBenefitsItem}>• Crédits gratuits tous les mois</Text>
|
||||
{Array.isArray(cardDisplay.benefitItems) && cardDisplay.benefitItems.length ? (
|
||||
<View style={styles.cardBenefits}>
|
||||
{cardDisplay.benefitItems.map((item, index) =>
|
||||
item?.type === 'plus' ? (
|
||||
<Text key={`benefit-plus-${index}`} style={styles.benefitPlus}>
|
||||
{item.text}
|
||||
</Text>
|
||||
) : (
|
||||
<Text key={`benefit-text-${index}`} style={styles.downloadLabel}>
|
||||
{item.text}
|
||||
</Text>
|
||||
)
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{isPopular ? (
|
||||
<View style={styles.popularBadge}>
|
||||
@@ -274,11 +177,6 @@ function SubscriptionCard({ plan, selected, onSelect, isAnnual }) {
|
||||
) : null}
|
||||
</View>
|
||||
</BlurView>
|
||||
{isAnnual ? (
|
||||
<View style={styles.annualBadge}>
|
||||
<Text style={styles.annualBadgeText}>2 mois offert</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
@@ -301,7 +199,7 @@ const ShareAndCreditsModal = ({
|
||||
isCatalogLoading,
|
||||
} = useStripe()
|
||||
|
||||
const [selectedPeriodKey, setSelectedPeriodKey] = useState('annual')
|
||||
const [selectedPeriodKey, setSelectedPeriodKey] = useState('monthly')
|
||||
const [selectedPriceId, setSelectedPriceId] = useState(null)
|
||||
const [processingPriceId, setProcessingPriceId] = useState(null)
|
||||
const [errorMessage, setErrorMessage] = useState(null)
|
||||
@@ -422,7 +320,7 @@ const ShareAndCreditsModal = ({
|
||||
{/* CREDITS SECTION */}
|
||||
{coinPacks && coinPacks.length > 0 ? (
|
||||
<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}>
|
||||
{coinPacks.map((pack) => {
|
||||
const rawAmount = pack.unitAmount ?? pack.amount ?? pack.price ?? 0
|
||||
@@ -432,6 +330,8 @@ const ShareAndCreditsModal = ({
|
||||
const details = packKey ? COIN_PACK_DETAILS[packKey] : null
|
||||
const displayName = details?.label || pack.name
|
||||
const creditsText = details?.creditsDisplay
|
||||
const displayPrice = details?.priceDisplay || price || `${rawAmount / 100}€`
|
||||
const isPopular = details?.popular === true
|
||||
|
||||
return (
|
||||
<View key={pack.productId} style={styles.creditCard}>
|
||||
@@ -450,12 +350,13 @@ const ShareAndCreditsModal = ({
|
||||
<Text style={styles.packDisclaimer}>{details.disclaimer}</Text>
|
||||
) : null}
|
||||
</View>
|
||||
{isPopular ? (
|
||||
<View style={styles.popularBadge}>
|
||||
<Text style={styles.popularBadgeText}>Le plus populaire !</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<GradientButton
|
||||
title={
|
||||
processingPriceId === pack.productId
|
||||
? '...'
|
||||
: price || `${rawAmount / 100}€`
|
||||
}
|
||||
title={processingPriceId === pack.productId ? '...' : displayPrice}
|
||||
onPress={() => handleBuyCredits(pack)}
|
||||
disabled={Boolean(processingPriceId)}
|
||||
style={styles.creditButton}
|
||||
@@ -636,7 +537,7 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 16,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
justifyContent: 'flex-start',
|
||||
width: isWeb ? 220 : '100%',
|
||||
minHeight: 160,
|
||||
gap: 12,
|
||||
@@ -672,6 +573,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 4,
|
||||
},
|
||||
creditButton: {
|
||||
marginTop: 'auto',
|
||||
width: '100%',
|
||||
height: 36,
|
||||
minHeight: 36,
|
||||
@@ -758,6 +660,10 @@ const styles = StyleSheet.create({
|
||||
padding: 16,
|
||||
gap: 16,
|
||||
},
|
||||
cardContent: {
|
||||
flex: 1,
|
||||
gap: 16,
|
||||
},
|
||||
badge: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'rgba(112, 35, 247, 0.25)',
|
||||
@@ -795,6 +701,11 @@ const styles = StyleSheet.create({
|
||||
priceBlock: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
secondaryPriceText: {
|
||||
color: Palette.primary,
|
||||
fontSize: 15,
|
||||
fontFamily: FONT_FAMILY.InterBold,
|
||||
},
|
||||
priceRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'baseline',
|
||||
@@ -830,6 +741,19 @@ const styles = StyleSheet.create({
|
||||
cardBenefits: {
|
||||
gap: 6,
|
||||
},
|
||||
downloadLabel: {
|
||||
color: Palette.white,
|
||||
fontSize: 13,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: 'center',
|
||||
},
|
||||
benefitPlus: {
|
||||
color: Palette.white,
|
||||
fontSize: 22,
|
||||
fontFamily: FONT_FAMILY.InterBold,
|
||||
textAlign: 'center',
|
||||
lineHeight: 24,
|
||||
},
|
||||
benefitsTitle: {
|
||||
color: Palette.white,
|
||||
fontSize: 13,
|
||||
@@ -856,7 +780,7 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
marginTop: 12,
|
||||
marginTop: 'auto',
|
||||
},
|
||||
popularBadgeText: {
|
||||
color: Palette.white,
|
||||
|
||||
Reference in New Issue
Block a user