import React from 'react'
import { ActivityIndicator, FlatList, Image, Pressable, StyleSheet, Text, View } from 'react-native'
import { useRoute } from '@react-navigation/native'
import { BlurView } from 'expo-blur'
import { Image as ExpoImage } from 'expo-image'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import GradientButton from '../components/GradientButton'
import CreditAmount from '../components/CreditAmount'
import Page from '../layouts/Page'
import { background, icons, planBadges, subBadges } from '../assets'
import { Palette, gutters } from '../styles'
import { FONT_FAMILY } from '../styles/Fonts'
import { isWeb } from '../hooks/useLayoutType'
import { useStripe } from '../providers/StripeProvider'
import { useUserData } from '../providers/UserDataProvider'
import { goBack, navigate } from '../navigation/NavigationService'
import { Routes } from '../navigation/Routes'
const formatCurrency = (amount, currency = 'eur') => {
if (typeof amount !== 'number') {
return null
}
const normalized = amount / 100
const upperCurrency =
typeof currency === 'string' && currency.trim() ? currency.trim().toUpperCase() : 'EUR'
if (typeof Intl !== 'undefined' && Intl.NumberFormat) {
try {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: upperCurrency,
minimumFractionDigits: 2,
}).format(normalized)
} catch (_error) {
// Ignore format error and fall back to manual format.
}
}
return `${normalized.toFixed(2)} ${upperCurrency}`
}
const getIntervalLabel = (recurring) => {
if (!recurring || typeof recurring !== 'object') {
return null
}
const interval = recurring.interval || 'month'
const count = recurring.interval_count || 1
const vocabulary = {
day: { singular: 'jour', plural: 'jours' },
week: { singular: 'semaine', plural: 'semaines' },
month: { singular: 'mois', plural: 'mois' },
year: { singular: 'an', plural: 'ans' },
}
const terms = vocabulary[interval] || vocabulary.month
if (count <= 1) {
return `par ${terms.singular}`
}
return `tous les ${count} ${count > 1 ? terms.plural : terms.singular}`
}
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 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 planBadgeKey = planKey
const getPlanBadge = (plan) => {
switch (plan) {
case 'starter':
return
case 'pro':
return
case 'premium':
return
default:
return null
}
}
const planBadgeSource = planBadgeKey && subBadges[planBadgeKey] ? subBadges[planBadgeKey] : null
const handleSelect = React.useCallback(() => {
if (typeof onSelect === 'function' && plan?.priceId) {
onSelect(plan.priceId)
}
}, [onSelect, plan?.priceId])
return (
[
styles.cardWrapper,
selected && styles.cardWrapperSelected,
pressed && styles.cardWrapperPressed,
plan?.active === false && styles.cardWrapperInactive,
isPopular && styles.cardWrapperPopular,
]}
accessibilityRole="button"
accessibilityState={{ selected, disabled: plan?.active === false }}
disabled={plan?.active === false}
>
{plan?.badge ? (
{plan.badge}
) : null}
{planName}
{getPlanBadge(planKey)}
{plan?.nickname ? {plan.nickname} : null}
{formattedPrice || coinsPerMonth !== null ? (
{formattedPrice ? (
{formattedPrice}
{intervalLabel ? {intervalLabel} : null}
) : null}
{details?.creditsLabel ? (
{details.creditsLabel}
) : coinsPerMonth !== null ? (
/ mois
) : null}
) : null}
Privilège Membre :
{details?.privileges?.map((item, index) => (
{item.bold ? `• ${item.text}` : item.text}
{item.subItems ? (
{item.subItems.join(' - ')}
) : null}
)) || (
<>
• Eligible au concours mensuel chanson/Vidéo
• Crédits gratuits tous les mois
>
)}
{isPopular ? (
Le plus populaire !
) : null}
{isAnnual ? (
2 mois offert
) : null}
)
}
const PRICE_PRIORITY_BY_PERIOD = {
monthly: [
'price_1SPgitCzf2o5bDRdbnhLFx6f', // Starter
'price_1SPgjCCzf2o5bDRdr08Xzp8u', // Pro
'price_1SPgjaCzf2o5bDRdd9Xo2u26', // Premium
],
annual: [
'price_1SPgkDCzf2o5bDRdNGLVNeQ3', // Starter
'price_1SPgkXCzf2o5bDRdejBVxEBY', // Pro
'price_1SPgkqCzf2o5bDRdIcUwTDrm', // Premium
],
}
const PACK_PRICE_ID_BY_PERIOD = {
monthly: {
starter: 'price_1SPgitCzf2o5bDRdbnhLFx6f',
pro: 'price_1SPgjCCzf2o5bDRdr08Xzp8u',
premium: 'price_1SPgjaCzf2o5bDRdd9Xo2u26',
},
annual: {
starter: 'price_1SPgkDCzf2o5bDRdNGLVNeQ3',
pro: 'price_1SPgkXCzf2o5bDRdejBVxEBY',
premium: 'price_1SPgkqCzf2o5bDRdIcUwTDrm',
},
}
const PRICE_ID_TO_PLAN_KEY = {}
Object.values(PACK_PRICE_ID_BY_PERIOD).forEach((mapping) => {
Object.entries(mapping).forEach(([planKey, priceId]) => {
if (!priceId) {
return
}
PRICE_ID_TO_PLAN_KEY[priceId] = planKey
})
})
const PLAN_FALLBACK_ORDER = ['starter', 'pro', 'premium']
const PLAN_SYNONYMS = {
starter: ['starter'],
pro: ['pro'],
premium: ['premium'],
}
const PLAN_DISPLAY_NAME_BY_KEY = {
starter: 'Play Backer Blue',
pro: 'Play Backer Silver',
premium: 'Play Backer Gold',
}
const COIN_PACK_DETAILS = {
starter: {
label: 'Pack starter',
creditsDisplay: '50 Crédits',
disclaimer: '*Non-Eligible au concours Chanson/Vidéo',
},
medium: {
label: 'Pack Medium',
creditsDisplay: '100 Crédits + 50 offerts',
disclaimer: '*Non-Eligible au concours Chanson/Vidéo',
},
premium: {
label: 'Pack Premium',
creditsDisplay: '150 Crédits + 100 offerts',
disclaimer: '*Non-Eligible au concours Chanson/Vidéo',
},
}
const getCoinPackKey = (pack) => {
const name = (pack?.name || '').toLowerCase()
if (name.includes('starter')) return 'starter'
if (name.includes('medium')) return 'medium'
if (name.includes('premium') || name.includes('prenium') || name.includes('creator'))
return 'premium'
return null
}
const PLAN_DETAILS = {
starter: {
creditsLabel: '50 Crédits / Mois',
popular: false,
privileges: [
{ text: 'Eligible au concours mensuel chanson/Vidéo', bold: true },
{ text: 'Catégorie Chanson :', subItems: ['Top 1: 500 €', 'Top 2: 250 €', 'Top 3: 125 €'] },
{ text: 'Catégorie Vidéo :', subItems: ['Top 1: 1000 €', 'Top 2: 500 €', 'Top 3: 250 €'] },
],
},
pro: {
creditsLabel: '100 crédits + 50 offerts / Mois',
popular: true,
privileges: [
{ text: 'Eligible au concours mensuel chanson/Vidéo', bold: true },
{ text: 'Catégorie Chanson :', subItems: ['Top 1: 500 €', 'Top 2: 250 €', 'Top 3: 125 €'] },
{ text: 'Catégorie Vidéo :', subItems: ['Top 1: 1000 €', 'Top 2: 500 €', 'Top 3: 250 €'] },
],
},
premium: {
creditsLabel: '150 Crédits + 100 offerts / Mois',
popular: false,
privileges: [
{ text: 'Eligible au concours mensuel chanson/Vidéo', bold: true },
{ text: 'Catégorie Chanson :', subItems: ['Top 1: 500 €', 'Top 2: 250 €', 'Top 3: 125 €'] },
{ text: 'Catégorie Vidéo :', subItems: ['Top 1: 1000 €', 'Top 2: 500 €', 'Top 3: 250 €'] },
],
},
}
const getPlanKeyForBadge = (plan) => {
if (!plan || typeof plan !== 'object') {
return null
}
const priceId = typeof plan?.priceId === 'string' ? plan.priceId : plan?.id || null
if (priceId && PRICE_ID_TO_PLAN_KEY[priceId]) {
return PRICE_ID_TO_PLAN_KEY[priceId]
}
const label = (plan?.product?.name || plan?.nickname || '').toString().toLowerCase()
if (!label) {
return null
}
const match = Object.entries(PLAN_SYNONYMS).find(([, variants]) =>
variants.some((variant) => label.includes(variant))
)
return match ? match[0] : null
}
const getPlanPriority = (plan, periodKey) => {
const priceId = typeof plan?.priceId === 'string' ? plan.priceId : plan?.id || null
const priceOrder = PRICE_PRIORITY_BY_PERIOD[periodKey] || []
if (priceId) {
const priceIndex = priceOrder.indexOf(priceId)
if (priceIndex !== -1) {
return priceIndex
}
}
const label = (plan?.product?.name || plan?.nickname || '').toString().toLowerCase()
const fallbackIndex = PLAN_FALLBACK_ORDER.findIndex((candidate) => {
const variants = PLAN_SYNONYMS[candidate] || [candidate]
return variants.some((variant) => label.includes(variant))
})
const baseOffset = priceOrder.length
return fallbackIndex === -1 ? baseOffset + PLAN_FALLBACK_ORDER.length : baseOffset + fallbackIndex
}
const normalizePlans = (plans = [], periodKey) => {
if (!Array.isArray(plans)) {
return []
}
return plans
.filter((plan) => plan && typeof plan === 'object' && plan.priceId)
.map((plan) => ({
...plan,
badge: null,
}))
.sort((planA, planB) => getPlanPriority(planA, periodKey) - getPlanPriority(planB, periodKey))
}
const HERO_IMAGE_WIDTH = isWeb ? 1280 : 700
const HERO_IMAGE_HEIGHT = isWeb ? 760 : 360
const PAGE_BACKGROUND_COLOR = '#303438'
const SUBSCRIPTION_DISCLAIMER =
'Résiliable en un clic à tout moment. Les prix sont indiqués TTC. Contacte-nous pour des besoins spécifiques (facturation annuelle, volume, offres éducation).'
const CARD_MIN_HEIGHT = isWeb ? 260 : 180
export default function Subscriptions() {
const route = useRoute()
const insets = useSafeAreaInsets()
const isMobile = !isWeb
const initialPack = React.useMemo(() => {
const rawPack = route?.params?.pack
return typeof rawPack === 'string' ? rawPack.toLowerCase() : null
}, [route?.params?.pack])
const initialSubscriptionPack = initialPack && initialPack !== 'packs' ? initialPack : null
const {
subscriptions,
coinPacks,
isCatalogLoading,
catalogError,
createSubscriptionCheckout,
createCoinPackCheckout,
} = useStripe()
const { currentUserData } = useUserData()
const [selectedPeriodKey, setSelectedPeriodKey] = React.useState('annual')
const [selectedPriceId, setSelectedPriceId] = React.useState(null)
const [processingPriceId, setProcessingPriceId] = React.useState(null)
const [errorMessage, setErrorMessage] = React.useState(null)
const initialPackHandledRef = React.useRef(false)
const normalizedPlansByPeriod = React.useMemo(
() => ({
monthly: normalizePlans(subscriptions?.monthly, 'monthly'),
annual: normalizePlans(subscriptions?.annual, 'annual'),
}),
[subscriptions?.annual, subscriptions?.monthly]
)
const availablePeriods = React.useMemo(
() =>
['monthly', 'annual'].filter(
(period) => (normalizedPlansByPeriod?.[period]?.length || 0) > 0
),
[normalizedPlansByPeriod]
)
const hasAnyPlan = availablePeriods.length > 0
const isLoadingPlans = isCatalogLoading && !hasAnyPlan
const combinedErrorMessage = errorMessage || catalogError
React.useEffect(() => {
initialPackHandledRef.current = false
}, [initialSubscriptionPack])
React.useEffect(() => {
const periodEntries = Object.entries(normalizedPlansByPeriod).filter(
([, plans]) => (plans?.length || 0) > 0
)
if (!periodEntries.length) {
// Don't reset selectedPeriodKey here so it behaves more stably
setSelectedPriceId(null)
return
}
const shouldApplyPack = Boolean(initialSubscriptionPack) && !initialPackHandledRef.current
let matchedPriceId = null
let matchedPeriodKey = selectedPeriodKey
let nextPeriodKey = selectedPeriodKey
// Only force switch if current selected period is empty and another is not
if ((normalizedPlansByPeriod[selectedPeriodKey]?.length || 0) === 0) {
nextPeriodKey = periodEntries[0]?.[0] || 'annual'
}
if (shouldApplyPack) {
const desired = initialSubscriptionPack.trim().toLowerCase()
for (const [periodKey, plans] of periodEntries) {
const candidateId = PACK_PRICE_ID_BY_PERIOD?.[periodKey]?.[desired]
if (candidateId) {
const exists = plans.some((plan) => plan?.priceId === candidateId)
if (exists) {
matchedPriceId = candidateId
matchedPeriodKey = periodKey
break
}
}
const matched = plans.find((plan) => {
const label = (plan?.product?.name || plan?.nickname || '').toString().toLowerCase()
return label.includes(desired)
})
if (matched?.priceId) {
matchedPriceId = matched.priceId
matchedPeriodKey = periodKey
break
}
}
}
if (matchedPriceId) {
nextPeriodKey = matchedPeriodKey
}
const plansForPeriod = normalizedPlansByPeriod?.[nextPeriodKey] || []
const nextPriceId = (() => {
if (matchedPriceId) {
return matchedPriceId
}
const hasCurrent = plansForPeriod.some((plan) => plan.priceId === selectedPriceId)
if (hasCurrent) {
return selectedPriceId
}
return plansForPeriod?.[0]?.priceId || null
})()
if (nextPeriodKey !== selectedPeriodKey) {
setSelectedPeriodKey(nextPeriodKey)
}
if (nextPriceId !== selectedPriceId) {
setSelectedPriceId(nextPriceId)
}
if (matchedPriceId || (shouldApplyPack && !isCatalogLoading)) {
initialPackHandledRef.current = true
}
}, [
initialSubscriptionPack,
isCatalogLoading,
normalizedPlansByPeriod,
selectedPeriodKey,
selectedPriceId,
])
const currentPlans = normalizedPlansByPeriod?.[selectedPeriodKey] || []
const handleSelect = React.useCallback((priceId) => {
setSelectedPriceId(priceId)
setProcessingPriceId(null)
}, [])
const handleCheckout = React.useCallback(async () => {
if (!selectedPriceId) {
return
}
setProcessingPriceId(selectedPriceId)
setErrorMessage(null)
try {
await createSubscriptionCheckout(selectedPriceId)
} catch (error) {
console.error('[Subscriptions] checkout error', error)
setErrorMessage(
error?.message || 'Une erreur est survenue lors de la création de la session Stripe.'
)
} finally {
setProcessingPriceId(null)
}
}, [selectedPriceId, createSubscriptionCheckout])
const isProcessing = Boolean(processingPriceId)
const isActionDisabled = !selectedPriceId || isProcessing || isLoadingPlans
const actionButtonTitle = isProcessing ? 'Redirection...' : 'Choisir cet abonnement'
const currentPlanCount = currentPlans.length
const mobileActionSafePadding = React.useMemo(
() => Math.max(insets.bottom, gutters),
[insets.bottom]
)
const mobileListContentInset = React.useMemo(
() => ({
paddingBottom: mobileActionSafePadding + gutters * 3,
}),
[mobileActionSafePadding]
)
const handleBackPress = React.useCallback(() => {
goBack()
}, [])
const handlePeriodChange = React.useCallback(
(periodKey) => {
if (!periodKey || periodKey === selectedPeriodKey) {
return
}
setSelectedPeriodKey(periodKey)
setSelectedPriceId(null)
setProcessingPriceId(null)
},
[selectedPeriodKey]
)
const handleBuyCredits = React.useCallback(
async (pack) => {
const targetId = pack?.productId
if (!targetId) return
setProcessingPriceId(targetId)
setErrorMessage(null)
try {
await createCoinPackCheckout(targetId)
} catch (error) {
console.error('[Subscriptions] coin checkout error', error)
setErrorMessage(
error?.message ||
'Une erreur est survenue lors de la création de la session Stripe pour les crédits.'
)
} finally {
setProcessingPriceId(null)
}
},
[createCoinPackCheckout]
)
const renderHeaderSection = React.useCallback(() => {
return (
<>
Rejoignez le club MusicLand
Votre solde actuel :
{combinedErrorMessage ? {combinedErrorMessage} : null}
>
)
}, [combinedErrorMessage])
const renderSegmentedControl = React.useCallback(() => {
const segments = [
{ key: 'monthly', label: 'Mensuel', plans: normalizedPlansByPeriod?.monthly },
{ key: 'annual', label: 'Annuel', plans: normalizedPlansByPeriod?.annual },
]
const visibleSegments = segments.filter((segment) => (segment.plans?.length || 0) > 0)
if (visibleSegments.length <= 1) {
return null
}
return (
{visibleSegments.map((segment) => {
const isActive = selectedPeriodKey === segment.key
const showAnnualPromo = segment.key === 'annual'
return (
handlePeriodChange(segment.key)}
style={[styles.segmentButton, isActive && styles.segmentButtonActive]}
>
{segment.label}
{showAnnualPromo ? (
-16%
) : null}
)
})}
)
}, [handlePeriodChange, isMobile, normalizedPlansByPeriod, selectedPeriodKey])
const renderMobilePlanItem = React.useCallback(
({ item }) => (
),
[handleSelect, selectedPeriodKey, selectedPriceId]
)
const renderMobileEmptyComponent = React.useCallback(() => {
return (
{isLoadingPlans ? (
) : (
Aucun abonnement Stripe disponible pour le moment.
)}
)
}, [isLoadingPlans])
const renderMobileFooterComponent = React.useCallback(() => {
return (
{isLoadingPlans && currentPlanCount > 0 ? (
) : null}
{SUBSCRIPTION_DISCLAIMER}
)
}, [currentPlanCount, isLoadingPlans])
const renderMobileTopSticky = React.useCallback(() => {
return (
{renderHeaderSection()}
{renderSegmentedControl()}
)
}, [renderHeaderSection, renderSegmentedControl])
const renderMobileBottomActions = React.useCallback(() => {
return (
)
}, [actionButtonTitle, handleCheckout, isActionDisabled, mobileActionSafePadding])
const renderWebBackButton = React.useCallback(() => {
if (isMobile) return null
return (
Retour
)
}, [handleBackPress, isMobile])
return (
{isMobile ? (
plan.priceId}
renderItem={renderMobilePlanItem}
style={styles.mobileList}
contentContainerStyle={[styles.mobileListContent, mobileListContentInset]}
ListEmptyComponent={renderMobileEmptyComponent}
ListFooterComponent={renderMobileFooterComponent}
showsVerticalScrollIndicator={false}
/>
) : (
<>
{renderHeaderSection()}
{renderSegmentedControl()}
{isLoadingPlans && currentPlanCount === 0 ? (
) : null}
{!isLoadingPlans && currentPlanCount === 0 ? (
Aucun abonnement Stripe disponible pour le moment.
) : null}
{currentPlans.map((plan) => (
))}
{coinPacks && coinPacks.length > 0 ? (
Recharger vos crédits
{coinPacks.map((pack) => {
// Attempt to find amount in commonly used Stripe fields
const rawAmount = pack.unitAmount ?? pack.amount ?? pack.price ?? 0
const price = formatCurrency(rawAmount, pack.currency)
// Assuming pack structure: { id, name, unitAmount/amount, currency, metadata: { coins: 100 } }
const coins = Number(pack.metadata?.coins || 0)
const packKey = getCoinPackKey(pack)
const details = packKey ? COIN_PACK_DETAILS[packKey] : null
const displayName = details?.label || pack.name
const creditsText = details?.creditsDisplay
return (
{displayName}
{creditsText ? (
{creditsText}
) : (
)}
{details?.disclaimer ? (
{details.disclaimer}
) : null}
handleBuyCredits(pack)}
disabled={Boolean(processingPriceId)}
style={styles.creditButton}
textStyle={styles.creditButtonText}
gradientStyle={{ paddingVertical: 8, paddingHorizontal: 16 }}
/>
)
})}
) : null}
{isLoadingPlans && currentPlanCount > 0 ? (
) : null}
{SUBSCRIPTION_DISCLAIMER}
>
)}
{isMobile ? renderMobileBottomActions() : null}
)
}
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: PAGE_BACKGROUND_COLOR,
},
page: {
backgroundColor: 'transparent',
},
pageContent: {
flexGrow: 1,
paddingTop: 48,
paddingBottom: 48,
},
pageContentMobile: {
paddingTop: gutters,
paddingBottom: gutters * 2,
},
inner: {
flex: 1,
width: '100%',
maxWidth: 960,
alignSelf: 'center',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: gutters,
position: 'relative',
},
mobileInner: {
alignItems: 'stretch',
justifyContent: 'flex-start',
},
content: {
width: '100%',
gap: 32,
alignItems: 'center',
justifyContent: 'center',
position: 'relative',
zIndex: 1,
},
mobileContent: {
flex: 1,
gap: 0,
alignItems: 'stretch',
justifyContent: 'flex-start',
},
centerImage: {
width: HERO_IMAGE_WIDTH,
height: HERO_IMAGE_HEIGHT,
borderRadius: 28,
overflow: 'hidden',
position: 'absolute',
top: '50%',
left: '50%',
transform: [{ translateX: -HERO_IMAGE_WIDTH / 2 }, { translateY: -HERO_IMAGE_HEIGHT / 2 }],
pointerEvents: 'none',
},
header: {
gap: 12,
alignItems: 'center',
},
benefitsBox: {
width: '100%',
gap: 6,
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 16,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
},
benefitsTitle: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 14,
color: Palette.white,
textAlign: 'center',
},
benefitsList: {
gap: 2,
},
cardBenefits: {
marginTop: 12,
borderTopWidth: 1,
borderTopColor: 'rgba(255, 255, 255, 0.1)',
paddingTop: 12,
gap: 6,
},
cardBenefitsItem: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 12,
color: 'rgba(255, 255, 255, 0.7)',
},
benefitsTitle: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 13,
color: Palette.white,
marginBottom: 2,
},
benefitBlock: {
marginBottom: 4,
},
benefitText: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
color: 'rgba(255, 255, 255, 0.85)',
},
benefitTextBold: {
fontFamily: FONT_FAMILY.InterSemiBold,
color: Palette.white,
},
benefitSubList: {
marginTop: 2,
paddingLeft: 0,
},
benefitSubText: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 12,
color: 'rgba(255, 255, 255, 0.65)',
fontStyle: 'italic',
},
coinsPromoText: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 15,
color: Palette.primary,
},
balanceContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 20,
gap: 10,
marginTop: 8,
},
balanceLabel: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 14,
color: Palette.white,
},
creditsSection: {
width: '100%',
marginTop: 40,
gap: 16,
alignItems: 'center',
},
sectionTitle: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 20,
color: Palette.white,
},
creditsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 16,
justifyContent: 'center',
},
creditCard: {
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 16,
padding: 16,
alignItems: 'center',
justifyContent: 'space-between',
width: isWeb ? 200 : '45%',
gap: 12,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
creditName: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 14,
color: Palette.white,
textAlign: 'center',
marginBottom: 4,
},
creditInfoContainer: {
alignItems: 'center',
gap: 4,
},
creditAmountCustom: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: 16,
color: Palette.white,
textAlign: 'center',
},
packDisclaimer: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 10,
color: 'rgba(255, 255, 255, 0.6)',
fontStyle: 'italic',
textAlign: 'center',
marginTop: 4,
},
creditAmountText: {
fontSize: 20,
},
creditButton: {
width: '100%',
height: 36,
minHeight: 36,
},
creditButtonText: {
fontSize: 13,
},
webBackContainer: {
alignSelf: 'flex-start',
marginBottom: 12,
},
webBackButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
alignSelf: 'flex-start',
paddingHorizontal: 14,
paddingVertical: 7,
borderRadius: 999,
borderWidth: 1,
borderColor: Palette.ultraLightWhite,
backgroundColor: Palette.ultraLightWhite,
},
webBackIcon: {
width: 16,
height: 16,
transform: [{ rotate: '90deg' }],
},
webBackText: {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
},
title: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: isWeb ? 32 : 24,
lineHeight: isWeb ? 40 : 28,
color: Palette.white,
textAlign: 'center',
},
errorText: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 14,
color: Palette.red,
textAlign: 'center',
},
loaderContainer: {
alignItems: 'center',
},
emptyState: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 14,
color: 'rgba(255, 255, 255, 0.7)',
textAlign: 'center',
},
packs: {
width: '100%',
maxWidth: 960,
gap: gutters,
alignItems: 'center',
},
packsWeb: {
flexDirection: 'row',
gap: gutters,
alignItems: 'stretch',
justifyContent: 'center',
},
segmentedControl: {
flexDirection: 'row',
alignSelf: 'center',
justifyContent: 'center',
padding: 4,
borderRadius: 999,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
marginTop: isWeb ? 12 : 8,
marginBottom: isWeb ? 8 : 4,
},
segmentButton: {
paddingVertical: 8,
paddingHorizontal: 18,
borderRadius: 999,
position: 'relative',
},
segmentButtonActive: {
backgroundColor: 'rgba(255, 255, 255, 0.18)',
},
segmentLabel: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 14,
color: 'rgba(255, 255, 255, 0.7)',
},
segmentLabelActive: {
color: Palette.white,
},
segmentBadge: {
position: 'absolute',
top: -6,
right: -8,
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 999,
backgroundColor: Palette.red,
},
segmentBadgeText: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 11,
color: Palette.white,
},
annualBadge: {
position: 'absolute',
top: isWeb ? 12 : 10,
right: isWeb ? 12 : 10,
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 999,
backgroundColor: Palette.red + "80",
},
annualBadgeText: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 12,
color: Palette.white + "80",
},
actions: {
width: '100%',
alignItems: 'center',
},
actionButtonGradient: {
width: isWeb ? 320 : '90%',
},
cardWrapper: {
flex: 1,
width: '100%',
minWidth: 0,
minHeight: CARD_MIN_HEIGHT,
position: 'relative',
borderRadius: 24,
overflow: 'hidden',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.12)',
backgroundColor: 'rgba(12, 14, 18, 0.45)',
},
cardWrapperSelected: {
borderColor: Palette.primary,
shadowColor: '#000',
shadowOffset: { width: 0, height: 12 },
shadowOpacity: 0.35,
shadowRadius: 20,
elevation: 8,
},
cardWrapperPopular: {
borderColor: Palette.primary, // Highlight border for popular
backgroundColor: 'rgba(112, 35, 247, 0.1)', // Slight tint
},
cardWrapperPressed: {
transform: [{ scale: 0.98 }],
},
cardWrapperInactive: {
opacity: 0.6,
},
cardBlur: {
flex: 1,
paddingHorizontal: gutters,
paddingVertical: isWeb ? gutters : Math.max(gutters * 0.2, 10),
gap: isWeb ? 18 : 10,
justifyContent: 'center',
backgroundColor: 'rgba(48, 52, 56, 0.55)',
borderRadius: 24,
},
cardContent: {
gap: isWeb ? 16 : 4,
},
cardHeader: {
gap: 6,
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
width: '100%',
},
planName: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: isWeb ? 24 : 20,
color: Palette.white,
flexShrink: 1,
},
planBadgeImage: {
width: 48,
height: 48,
flexShrink: 0,
},
cardSubtitle: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 14,
lineHeight: 20,
color: 'rgba(255, 255, 255, 0.75)',
},
priceBlock: {
gap: isWeb ? 4 : 1,
},
priceRow: {
flexDirection: 'row',
alignItems: 'baseline',
gap: 8,
},
coinsPerMonthRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
},
coinsPerMonth: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 13,
color: Palette.primary,
},
coinsPerMonthSuffix: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 13,
color: Palette.primary,
},
priceValue: {
fontFamily: FONT_FAMILY.InterBold,
fontSize: isWeb ? 22 : 20,
color: Palette.white,
},
period: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 14,
color: 'rgba(255, 255, 255, 0.7)',
},
badge: {
alignSelf: 'flex-start',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 999,
backgroundColor: 'rgba(112, 35, 247, 0.25)',
},
badgeText: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 12,
color: Palette.primary,
textTransform: 'uppercase',
},
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',
},
inlineLoader: {
alignItems: 'center',
},
disclaimer: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 12,
color: 'rgba(255, 255, 255, 0.6)',
lineHeight: 18,
textAlign: 'center',
},
mobileStickyHeader: {
width: '100%',
gap: 6,
paddingBottom: gutters * 0.2,
backgroundColor: PAGE_BACKGROUND_COLOR,
alignItems: 'center',
},
segmentedControlMobile: {
alignSelf: 'center',
marginBottom: 0,
},
mobileList: {
flex: 1,
width: '100%',
},
mobileListContent: {
flexGrow: 1,
paddingTop: 0,
paddingBottom: 0,
},
mobileCard: {
marginBottom: gutters * 0.6,
},
mobileEmptyWrapper: {
paddingVertical: gutters * 2,
alignItems: 'center',
justifyContent: 'center',
},
mobileFooter: {
width: '100%',
paddingTop: gutters,
gap: gutters,
alignItems: 'center',
},
mobileActions: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
zIndex: 10,
paddingHorizontal: gutters,
paddingTop: gutters * 0.4,
backgroundColor: PAGE_BACKGROUND_COLOR,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: 'rgba(255, 255, 255, 0.12)',
shadowColor: '#000',
shadowOffset: { width: 0, height: -4 },
shadowOpacity: 0.25,
shadowRadius: 12,
elevation: 12,
},
mobileActionButtonGradient: {
width: '100%',
},
})