feat: share and credits modal

This commit is contained in:
2026-01-16 15:58:22 +01:00
parent 9581a6fd4c
commit fc6c5da6db
4 changed files with 1010 additions and 5 deletions
+52 -2
View File
@@ -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) {
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()
}
}}
/>
<FullscreenIntroVideo url={showIntro} visible={!!showIntro} onClose={handleCloseIntro} />
<AdventureChoiceModal
isVisible={showAdventureModal}
setIsVisible={setShowAdventureModal}
onContinue={handleContinueAdventure}
onStop={handleStopAdventure}
/>
<ShareAndCreditsModal
isVisible={showShareModal}
setIsVisible={setShowShareModal}
onNavigateToCredits={() => {
setShowShareModal(false)
navigate(Routes.DownloadPrices, { action: 'song' })
}}
project={projectForStage}
selectedOption={selectedOption}
/>
</Page>
)
+1 -2
View File
@@ -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 (
<Page backgroundImg={background.studioBG2} headerType="NONE">
@@ -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 (
<Overlay isVisible={isVisible} setIsVisible={setIsVisible} blurIntensity={15}>
<View style={styles.modalCard}>
<Text style={styles.modalTitle}>Quelle est la suite ?</Text>
<View style={styles.modalActions}>
<GradientButton
title="Je continue l'aventure musicland"
onPress={onContinue}
containerStyle={styles.modalAction}
/>
<BorderGradientButton
title="J'arrête l'aventure musicland"
onPress={onStop}
containerStyle={styles.modalAction}
/>
</View>
</View>
</Overlay>
)
}
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%',
},
})
@@ -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 (
<Pressable
onPress={handleSelect}
style={({ pressed }) => [
styles.cardWrapper,
selected && styles.cardWrapperSelected,
pressed && styles.cardWrapperPressed,
plan?.active === false && styles.cardWrapperInactive,
isPopular && styles.cardWrapperPopular,
]}
disabled={plan?.active === false}
>
<BlurView intensity={18} tint="dark" style={styles.cardBlur}>
<View style={styles.cardContent}>
{plan?.badge ? (
<View style={styles.badge}>
<Text style={styles.badgeText}>{plan.badge}</Text>
</View>
) : null}
<View style={styles.cardHeader}>
<View style={styles.titleRow}>
<Text style={styles.planName}>{planName}</Text>
{planBadgeSource ? (
<ExpoImage
source={planBadgeSource}
style={styles.planBadgeImage}
contentFit="contain"
/>
) : null}
</View>
{plan?.nickname ? <Text style={styles.cardSubtitle}>{plan.nickname}</Text> : null}
</View>
{formattedPrice || coinsPerMonth !== null ? (
<View style={styles.priceBlock}>
{formattedPrice ? (
<View style={styles.priceRow}>
<Text style={styles.priceValue}>{formattedPrice}</Text>
{intervalLabel ? <Text style={styles.period}>{intervalLabel}</Text> : null}
</View>
) : 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>
) : 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>
)}
</View>
{isPopular ? (
<View style={styles.popularBadge}>
<Text style={styles.popularBadgeText}>Le plus populaire !</Text>
</View>
) : null}
</View>
</BlurView>
{isAnnual ? (
<View style={styles.annualBadge}>
<Text style={styles.annualBadgeText}>2 mois offert</Text>
</View>
) : null}
</Pressable>
)
}
// --- 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 (
<Overlay isVisible={isVisible} setIsVisible={setIsVisible} blurIntensity={20}>
<View style={styles.modalCard}>
<View style={styles.closeButtonContainer}>
<Pressable onPress={() => setIsVisible(false)} style={styles.closeButton}>
<MaterialCommunityIcons name="close" size={24} color={Palette.white} />
</Pressable>
</View>
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{/* SHARE SECTION */}
<Text style={styles.modalTitle}>Partage ton succès !</Text>
<View style={styles.shareSection}>
<Text style={styles.shareSubtitle}>Partage mon morceau</Text>
<View style={styles.shareButtons}>
<Pressable style={styles.shareBtn} onPress={() => handleShare('whatsapp')}>
<MaterialCommunityIcons name="whatsapp" size={32} color="#25D366" />
</Pressable>
<Pressable style={styles.shareBtn} onPress={() => handleShare('facebook')}>
<MaterialCommunityIcons name="facebook" size={32} color="#1877F2" />
</Pressable>
<Pressable style={styles.shareBtn} onPress={() => handleShare('instagram')}>
<MaterialCommunityIcons name="instagram" size={32} color="#C13584" />
</Pressable>
<Pressable style={styles.shareBtn} onPress={() => handleShare('tiktok')}>
<MaterialCommunityIcons name="music-note" size={32} color="#000000" />
</Pressable>
</View>
</View>
{/* CREDITS SECTION */}
{coinPacks && coinPacks.length > 0 ? (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>Recharger vos crédits</Text>
<View style={styles.creditsGrid}>
{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 (
<View key={pack.productId} style={styles.creditCard}>
<View style={styles.creditInfoContainer}>
<Text style={styles.creditName}>{displayName}</Text>
{creditsText ? (
<Text style={styles.creditAmountCustom}>{creditsText}</Text>
) : (
<CreditAmount
value={coins}
iconSize={24}
textStyle={styles.creditAmountText}
/>
)}
{details?.disclaimer ? (
<Text style={styles.packDisclaimer}>{details.disclaimer}</Text>
) : null}
</View>
<GradientButton
title={
processingPriceId === pack.productId
? '...'
: price || `${rawAmount / 100}`
}
onPress={() => handleBuyCredits(pack)}
disabled={Boolean(processingPriceId)}
style={styles.creditButton}
textStyle={styles.creditButtonText}
gradientStyle={{ paddingVertical: 8, paddingHorizontal: 16 }}
/>
</View>
)
})}
</View>
</View>
) : null}
{/* SUBSCRIPTIONS SECTION */}
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>Rejoignez le club MusicLand</Text>
{/* Period Toggle */}
<View style={styles.segmentedControl}>
{['monthly', 'annual'].map((key) => {
const isActive = selectedPeriodKey === key
const label = key === 'monthly' ? 'Mensuel' : 'Annuel'
const showAnnualPromo = key === 'annual'
return (
<Pressable
key={key}
onPress={() => {
setSelectedPeriodKey(key)
setSelectedPriceId(null)
}}
style={[styles.segmentButton, isActive && styles.segmentButtonActive]}
>
<Text style={[styles.segmentLabel, isActive && styles.segmentLabelActive]}>
{label}
</Text>
{showAnnualPromo && (
<View style={styles.segmentBadge}>
<Text style={styles.segmentBadgeText}>-16%</Text>
</View>
)}
</Pressable>
)
})}
</View>
{/* Plans Grid */}
<View style={styles.plansGrid}>
{isCatalogLoading && !currentPlans.length ? (
<ActivityIndicator color={Palette.white} />
) : (
currentPlans.map((plan) => (
<View key={plan.priceId} style={{ width: isWeb ? '30%' : '100%' }}>
<SubscriptionCard
plan={plan}
selected={selectedPriceId === plan.priceId}
onSelect={setSelectedPriceId}
isAnnual={selectedPeriodKey === 'annual'}
/>
</View>
))
)}
</View>
<View style={{ marginTop: 20, width: '100%', alignItems: 'center' }}>
<GradientButton
title={processingPriceId && !coinPacks.find(p => p.productId === processingPriceId) ? 'Traitement...' : 'Choisir cet abonnement'}
onPress={handleSubscribe}
disabled={!selectedPriceId || Boolean(processingPriceId)}
style={{ width: isWeb ? 320 : '100%' }}
/>
</View>
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
</View>
{/* BROADCAST SECTION */}
<View style={styles.broadcastSection}>
<View style={styles.consentContainer}>
<AppCheckbox
selected={hasAcceptedPublication}
onPress={() => setHasAcceptedPublication((prev) => !prev)}
label="J'accepte de diffuser mon contenu sur la plateforme de streaming de musicland et sur youtube"
/>
</View>
<GradientButton
title="Diffuser"
onPress={() => {
// No-op for now
console.log('Diffuser pressed', { hasAcceptedPublication })
}}
disabled={!hasAcceptedPublication}
containerStyle={styles.broadcastButton}
/>
</View>
</ScrollView>
</View>
</Overlay>
)
}
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%',
},
})