From fc6c5da6dbab23a4e66e3b3c316447544645a4f1 Mon Sep 17 00:00:00 2001 From: Leon Date: Fri, 16 Jan 2026 15:58:22 +0100 Subject: [PATCH] feat: share and credits modal --- src/screens/cover/SongDownload.js | 56 +- src/screens/cover/ValidateCover.js | 3 +- .../cover/components/AdventureChoiceModal.js | 59 ++ .../cover/components/ShareAndCreditsModal.js | 897 ++++++++++++++++++ 4 files changed, 1010 insertions(+), 5 deletions(-) create mode 100644 src/screens/cover/components/AdventureChoiceModal.js create mode 100644 src/screens/cover/components/ShareAndCreditsModal.js diff --git a/src/screens/cover/SongDownload.js b/src/screens/cover/SongDownload.js index 6133c4e..21f9c7c 100644 --- a/src/screens/cover/SongDownload.js +++ b/src/screens/cover/SongDownload.js @@ -32,6 +32,9 @@ import { isWeb } from '../../hooks/useLayoutType' import { getArtistDisplayName } from '../../utils/artistName' import { toDate } from '../../utils/dateFormatting' import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal' +import FullscreenIntroVideo from '../../components/FullscreenIntroVideo' +import AdventureChoiceModal from './components/AdventureChoiceModal' +import ShareAndCreditsModal from './components/ShareAndCreditsModal' const SongDownload = ({ route }) => { const { @@ -45,6 +48,11 @@ const SongDownload = ({ route }) => { const [isDownloading, setIsDownloading] = useState(false) const [showConfirmModal, setShowConfirmModal] = useState(false) const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true) + const { videos } = useUser() + const part1Url = isWeb ? videos?.benaiPart1 : videos?.benaiPart1 + const [showIntro, setShowIntro] = useState(null) + const [showAdventureModal, setShowAdventureModal] = useState(false) + const [showShareModal, setShowShareModal] = useState(false) const projectForStage = useMemo( () => routeProject || selectedProject || null, @@ -145,11 +153,30 @@ const SongDownload = ({ route }) => { return } if (hasActiveSubscription) { - continueFlow() + if (part1Url) { + setShowIntro(part1Url) + } else { + continueFlow() + } return } setShowConfirmModal(true) - }, [continueFlow, hasAcceptedPublication, hasActiveSubscription, setTooltip]) + }, [continueFlow, hasAcceptedPublication, hasActiveSubscription, part1Url, setTooltip]) + + const handleCloseIntro = useCallback(() => { + setShowIntro(null) + setShowAdventureModal(true) + }, []) + + const handleContinueAdventure = useCallback(() => { + setShowAdventureModal(false) + continueFlow() + }, [continueFlow]) + + const handleStopAdventure = useCallback(() => { + setShowAdventureModal(false) + setShowShareModal(true) + }, []) const handleDownload = useCallback(async () => { const downloadUrl = @@ -410,7 +437,30 @@ const SongDownload = ({ route }) => { isVisible={showConfirmModal} setIsVisible={setShowConfirmModal} onJoinClub={() => navigate(Routes.Payments)} - onContinue={continueFlow} + onContinue={() => { + if (part1Url) { + setShowIntro(part1Url) + } else { + continueFlow() + } + }} + /> + + + { + setShowShareModal(false) + navigate(Routes.DownloadPrices, { action: 'song' }) + }} + project={projectForStage} + selectedOption={selectedOption} /> ) diff --git a/src/screens/cover/ValidateCover.js b/src/screens/cover/ValidateCover.js index a62269a..75fde56 100644 --- a/src/screens/cover/ValidateCover.js +++ b/src/screens/cover/ValidateCover.js @@ -13,7 +13,6 @@ import { goBack, navigate } from '../../navigation/NavigationService' import { useUser, useUserData } from '../../providers/UserDataProvider' import { gutters, Palette, Style } from '../../styles' import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader' - const ValidateCover = () => { const { selectedProject, updateProjectData } = useUserData() const { videos } = useUser() @@ -102,7 +101,7 @@ const ValidateCover = () => { selectedOption, coverOptions, }) - }, [selectedProject, selectedOption, coverOptions]) + }, [coverOptions, selectedOption, selectedProject]) return ( diff --git a/src/screens/cover/components/AdventureChoiceModal.js b/src/screens/cover/components/AdventureChoiceModal.js new file mode 100644 index 0000000..9b59da6 --- /dev/null +++ b/src/screens/cover/components/AdventureChoiceModal.js @@ -0,0 +1,59 @@ +import React from 'react' +import { StyleSheet, Text, View } from 'react-native' +import { gutters, Palette } from '../../../styles' +import { FONT_FAMILY } from '../../../styles/Fonts' +import BorderGradientButton from '../../../components/BorderGradientButton' +import GradientButton from '../../../components/GradientButton' +import Overlay from '../../../components/Overlay' + +const AdventureChoiceModal = ({ isVisible, setIsVisible, onContinue, onStop }) => { + return ( + + + Quelle est la suite ? + + + + + + + ) +} + +export default AdventureChoiceModal + +const styles = StyleSheet.create({ + modalCard: { + width: '90%', + maxWidth: 420, + alignSelf: 'center', + padding: gutters * 1.4, + borderRadius: 20, + backgroundColor: 'rgba(37, 36, 56, 0.96)', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.12)', + gap: gutters * 0.8, + }, + modalTitle: { + fontFamily: FONT_FAMILY.InterSemiBold, + fontSize: 18, + color: Palette.white, + textAlign: 'center', + marginBottom: 8, + }, + modalActions: { + gap: gutters * 0.6, + marginTop: gutters * 0.6, + }, + modalAction: { + width: '100%', + }, +}) diff --git a/src/screens/cover/components/ShareAndCreditsModal.js b/src/screens/cover/components/ShareAndCreditsModal.js new file mode 100644 index 0000000..d8c6d2f --- /dev/null +++ b/src/screens/cover/components/ShareAndCreditsModal.js @@ -0,0 +1,897 @@ +import React, { useMemo, useState, useCallback } from 'react' +import { + StyleSheet, + Text, + View, + Pressable, + Linking, + Alert, + ScrollView, + ActivityIndicator, +} from 'react-native' +import { MaterialCommunityIcons } from '@expo/vector-icons' +import * as Sharing from 'expo-sharing' +import { BlurView } from 'expo-blur' +import { Image as ExpoImage } from 'expo-image' +import { gutters, Palette } from '../../../styles' +import { FONT_FAMILY } from '../../../styles/Fonts' +import Overlay from '../../../components/Overlay' +import { useStripe } from '../../../providers/StripeProvider' +import CreditAmount from '../../../components/CreditAmount' +import GradientButton from '../../../components/GradientButton' +import AppCheckbox from '../../../components/AppCheckbox' +import { subBadges, icons } from '../../../assets' +import { isWeb } from '../../../hooks/useLayoutType' + +// --- HELPER FUNCTIONS & CONSTANTS (Copied from Subscriptions.js) --- + +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 + } + } + 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}` +} + +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 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 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, + ]} + disabled={plan?.active === false} + > + + + {plan?.badge ? ( + + {plan.badge} + + ) : null} + + + + {planName} + {planBadgeSource ? ( + + ) : null} + + {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} + + + )) || ( + • Crédits gratuits tous les mois + )} + + + {isPopular ? ( + + Le plus populaire ! + + ) : null} + + + {isAnnual ? ( + + 2 mois offert + + ) : null} + + ) +} + +// --- MAIN WRAPPER COMPONENT --- + +const ShareAndCreditsModal = ({ + isVisible, + setIsVisible, + project, + selectedOption, +}) => { + const trackTitle = project?.title || 'Musicland Track' + + const { + subscriptions, + coinPacks, + createSubscriptionCheckout, + createCoinPackCheckout, + isCatalogLoading, + } = useStripe() + + const [selectedPeriodKey, setSelectedPeriodKey] = useState('annual') + const [selectedPriceId, setSelectedPriceId] = useState(null) + const [processingPriceId, setProcessingPriceId] = useState(null) + const [errorMessage, setErrorMessage] = useState(null) + const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true) + + const normalizedPlansByPeriod = useMemo(() => { + // Basic normalization/filter logic similar to Subscriptions.js + const normalize = (plans) => { + if (!Array.isArray(plans)) return [] + return plans.filter((p) => p && p.priceId) + } + return { + monthly: normalize(subscriptions?.monthly), + annual: normalize(subscriptions?.annual), + } + }, [subscriptions]) + + // Set default selection + React.useEffect(() => { + const plans = normalizedPlansByPeriod[selectedPeriodKey] || [] + if (plans.length > 0 && !selectedPriceId) { + // Optional: Auto-select popular/first plan + // But for now let's leave it null to force user choice or pick first + } + }, [normalizedPlansByPeriod, selectedPeriodKey, selectedPriceId]) + + + const handleShare = async (platform) => { + const shareUrl = + project?.songUrl || + project?.playbackUrl || + selectedOption?.finalUrl || + 'https://musicland.ai' + const shareText = `Check out my new song "${trackTitle}" created with MusicLand! ${shareUrl}` + + if (platform === 'whatsapp') { + Linking.openURL(`whatsapp://send?text=${encodeURIComponent(shareText)}`).catch(() => { + Alert.alert('Erreur', 'WhatsApp is not installed') + }) + } else if (platform === 'facebook') { + Linking.openURL( + `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}` + ).catch(() => { + Alert.alert('Erreur', 'Unable to open Facebook') + }) + } else if (platform === 'instagram' || platform === 'tiktok') { + if (await Sharing.isAvailableAsync()) { + await Sharing.shareAsync(shareUrl, { dialogTitle: 'Partager ma musique' }) + } else { + Linking.openURL(shareUrl) + } + } + } + + const handleBuyCredits = async (pack) => { + const targetId = pack?.productId + if (!targetId) return + setProcessingPriceId(targetId) + setErrorMessage(null) + try { + await createCoinPackCheckout(targetId) + } catch (error) { + setErrorMessage(error?.message || 'Erreur lors de l\'achat de crédits.') + } finally { + setProcessingPriceId(null) + } + } + + const handleSubscribe = async () => { + if (!selectedPriceId) return + setProcessingPriceId(selectedPriceId) + try { + await createSubscriptionCheckout(selectedPriceId) + } catch (error) { + setErrorMessage(error?.message || 'Erreur lors de l\'abonnement.') + } finally { + setProcessingPriceId(null) + } + } + + const currentPlans = normalizedPlansByPeriod[selectedPeriodKey] || [] + + return ( + + + + setIsVisible(false)} style={styles.closeButton}> + + + + + + {/* SHARE SECTION */} + Partage ton succès ! + + Partage mon morceau + + handleShare('whatsapp')}> + + + handleShare('facebook')}> + + + handleShare('instagram')}> + + + handleShare('tiktok')}> + + + + + + {/* CREDITS SECTION */} + {coinPacks && coinPacks.length > 0 ? ( + + Recharger vos crédits + + {coinPacks.map((pack) => { + const rawAmount = pack.unitAmount ?? pack.amount ?? pack.price ?? 0 + const price = formatCurrency(rawAmount, pack.currency) + 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} + + {/* SUBSCRIPTIONS SECTION */} + + Rejoignez le club MusicLand + + {/* Period Toggle */} + + {['monthly', 'annual'].map((key) => { + const isActive = selectedPeriodKey === key + const label = key === 'monthly' ? 'Mensuel' : 'Annuel' + const showAnnualPromo = key === 'annual' + return ( + { + setSelectedPeriodKey(key) + setSelectedPriceId(null) + }} + style={[styles.segmentButton, isActive && styles.segmentButtonActive]} + > + + {label} + + {showAnnualPromo && ( + + -16% + + )} + + ) + })} + + + {/* Plans Grid */} + + {isCatalogLoading && !currentPlans.length ? ( + + ) : ( + currentPlans.map((plan) => ( + + + + )) + )} + + + + p.productId === processingPriceId) ? 'Traitement...' : 'Choisir cet abonnement'} + onPress={handleSubscribe} + disabled={!selectedPriceId || Boolean(processingPriceId)} + style={{ width: isWeb ? 320 : '100%' }} + /> + + {errorMessage ? {errorMessage} : null} + + + {/* BROADCAST SECTION */} + + + setHasAcceptedPublication((prev) => !prev)} + label="J'accepte de diffuser mon contenu sur la plateforme de streaming de musicland et sur youtube" + /> + + { + // No-op for now + console.log('Diffuser pressed', { hasAcceptedPublication }) + }} + disabled={!hasAcceptedPublication} + containerStyle={styles.broadcastButton} + /> + + + + + ) +} + +export default ShareAndCreditsModal + +const styles = StyleSheet.create({ + modalCard: { + width: isWeb ? '90%' : '95%', + maxWidth: 1000, + height: isWeb ? '90%' : '85%', + alignSelf: 'center', + borderRadius: 24, + backgroundColor: 'rgba(23, 22, 33, 0.98)', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.12)', + overflow: 'hidden', + padding: 0, + }, + scrollContent: { + padding: gutters, + gap: gutters * 1.5, + paddingBottom: gutters * 2, + }, + closeButtonContainer: { + position: 'absolute', + top: 12, + right: 12, + zIndex: 100, + }, + closeButton: { + padding: 8, + backgroundColor: 'rgba(255,255,255,0.1)', + borderRadius: 20, + }, + modalTitle: { + fontFamily: FONT_FAMILY.InterBold, + fontSize: 24, + color: Palette.white, + textAlign: 'center', + marginTop: 10, + }, + shareSection: { + gap: 12, + alignItems: 'center', + }, + shareSubtitle: { + fontSize: 16, + fontFamily: FONT_FAMILY.InterSemiBold, + color: Palette.white, + opacity: 0.9, + }, + shareButtons: { + flexDirection: 'row', + gap: 20, + justifyContent: 'center', + }, + shareBtn: { + padding: 8, + borderRadius: 50, + backgroundColor: 'rgba(255, 255, 255, 0.1)', + }, + sectionContainer: { + gap: 16, + width: '100%', + alignItems: 'center', + borderTopWidth: 1, + borderTopColor: 'rgba(255, 255, 255, 0.08)', + paddingTop: gutters, + }, + sectionTitle: { + fontFamily: FONT_FAMILY.InterBold, + fontSize: 20, + color: Palette.white, + textAlign: 'center', + }, + creditsGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: 16, + justifyContent: 'center', + width: '100%', + }, + creditCard: { + backgroundColor: 'rgba(255, 255, 255, 0.05)', + borderRadius: 16, + padding: 16, + alignItems: 'center', + justifyContent: 'space-between', + width: isWeb ? 220 : '100%', + minHeight: 160, + gap: 12, + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.1)', + }, + creditInfoContainer: { + alignItems: 'center', + gap: 4, + }, + creditName: { + fontFamily: FONT_FAMILY.InterSemiBold, + fontSize: 14, + color: Palette.white, + textAlign: 'center', + marginBottom: 4, + }, + creditAmountCustom: { + fontFamily: FONT_FAMILY.InterBold, + fontSize: 16, + color: Palette.white, + textAlign: 'center', + }, + creditAmountText: { + fontSize: 20, + }, + packDisclaimer: { + fontFamily: FONT_FAMILY.InterRegular, + fontSize: 10, + color: 'rgba(255, 255, 255, 0.6)', + fontStyle: 'italic', + textAlign: 'center', + marginTop: 4, + }, + creditButton: { + width: '100%', + height: 36, + minHeight: 36, + }, + creditButtonText: { + fontSize: 13, + }, + segmentedControl: { + flexDirection: 'row', + alignSelf: 'center', + justifyContent: 'center', + padding: 4, + borderRadius: 999, + backgroundColor: 'rgba(255, 255, 255, 0.08)', + }, + 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, + }, + plansGrid: { + width: '100%', + flexDirection: isWeb ? 'row' : 'column', + flexWrap: 'wrap', + justifyContent: 'center', + gap: 16, + }, + // Subscription Card Styles + cardWrapper: { + width: '100%', + minHeight: 280, + 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, + backgroundColor: 'rgba(112, 35, 247, 0.1)', + }, + cardWrapperPressed: { + transform: [{ scale: 0.98 }], + }, + cardWrapperInactive: { + opacity: 0.6, + }, + cardBlur: { + flex: 1, + padding: 16, + gap: 16, + }, + badge: { + alignSelf: 'flex-start', + backgroundColor: 'rgba(112, 35, 247, 0.25)', + borderRadius: 12, + paddingHorizontal: 8, + paddingVertical: 4, + marginBottom: 8, + }, + badgeText: { + color: Palette.primary, + fontSize: 11, + fontFamily: FONT_FAMILY.InterSemiBold, + }, + cardHeader: { + gap: 4, + }, + titleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + planName: { + color: Palette.white, + fontSize: 18, + fontFamily: FONT_FAMILY.InterBold, + }, + planBadgeImage: { + width: 40, + height: 40, + }, + cardSubtitle: { + color: 'rgba(255,255,255,0.7)', + fontSize: 13, + }, + priceBlock: { + marginBottom: 8, + }, + priceRow: { + flexDirection: 'row', + alignItems: 'baseline', + gap: 6, + }, + priceValue: { + color: Palette.white, + fontSize: 20, + fontFamily: FONT_FAMILY.InterBold, + }, + period: { + color: 'rgba(255,255,255,0.6)', + fontSize: 13, + }, + coinsPromoText: { + color: Palette.primary, + fontSize: 14, + fontFamily: FONT_FAMILY.InterBold, + }, + coinsPerMonthRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 4, + }, + coinsPerMonth: { + fontSize: 14, + color: Palette.primary, + }, + coinsPerMonthSuffix: { + fontSize: 13, + color: Palette.primary, + }, + cardBenefits: { + gap: 6, + }, + benefitsTitle: { + color: Palette.white, + fontSize: 13, + fontFamily: FONT_FAMILY.InterSemiBold, + }, + benefitBlock: { + flexDirection: 'row', + }, + benefitText: { + color: 'rgba(255,255,255,0.8)', + fontSize: 12, + }, + benefitTextBold: { + fontFamily: FONT_FAMILY.InterSemiBold, + color: Palette.white, + }, + cardBenefitsItem: { + color: 'rgba(255,255,255,0.8)', + fontSize: 12, + }, + popularBadge: { + alignSelf: 'center', + backgroundColor: Palette.primary, + paddingHorizontal: 12, + paddingVertical: 4, + borderRadius: 12, + marginTop: 12, + }, + popularBadgeText: { + color: Palette.white, + fontSize: 10, + fontFamily: FONT_FAMILY.InterBold, + textTransform: 'uppercase', + }, + annualBadge: { + position: 'absolute', + top: 10, + right: 10, + backgroundColor: Palette.red, + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + annualBadgeText: { + color: Palette.white, + fontSize: 11, + fontFamily: FONT_FAMILY.InterBold, + }, + errorText: { + color: Palette.red, + textAlign: 'center', + marginTop: 10, + }, + broadcastSection: { + marginTop: gutters, + paddingTop: gutters, + borderTopWidth: 1, + borderTopColor: 'rgba(255, 255, 255, 0.08)', + gap: gutters, + width: '100%', + alignItems: 'center', + }, + consentContainer: { + width: '100%', + paddingHorizontal: gutters, + }, + broadcastButton: { + width: isWeb ? 320 : '100%', + }, +})