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%', }, })