feat: share and credits modal
This commit is contained in:
@@ -32,6 +32,9 @@ import { isWeb } from '../../hooks/useLayoutType'
|
|||||||
import { getArtistDisplayName } from '../../utils/artistName'
|
import { getArtistDisplayName } from '../../utils/artistName'
|
||||||
import { toDate } from '../../utils/dateFormatting'
|
import { toDate } from '../../utils/dateFormatting'
|
||||||
import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal'
|
import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal'
|
||||||
|
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
||||||
|
import AdventureChoiceModal from './components/AdventureChoiceModal'
|
||||||
|
import ShareAndCreditsModal from './components/ShareAndCreditsModal'
|
||||||
|
|
||||||
const SongDownload = ({ route }) => {
|
const SongDownload = ({ route }) => {
|
||||||
const {
|
const {
|
||||||
@@ -45,6 +48,11 @@ const SongDownload = ({ route }) => {
|
|||||||
const [isDownloading, setIsDownloading] = useState(false)
|
const [isDownloading, setIsDownloading] = useState(false)
|
||||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||||
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
|
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(
|
const projectForStage = useMemo(
|
||||||
() => routeProject || selectedProject || null,
|
() => routeProject || selectedProject || null,
|
||||||
@@ -145,11 +153,30 @@ const SongDownload = ({ route }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (hasActiveSubscription) {
|
if (hasActiveSubscription) {
|
||||||
continueFlow()
|
if (part1Url) {
|
||||||
|
setShowIntro(part1Url)
|
||||||
|
} else {
|
||||||
|
continueFlow()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setShowConfirmModal(true)
|
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 handleDownload = useCallback(async () => {
|
||||||
const downloadUrl =
|
const downloadUrl =
|
||||||
@@ -410,7 +437,30 @@ const SongDownload = ({ route }) => {
|
|||||||
isVisible={showConfirmModal}
|
isVisible={showConfirmModal}
|
||||||
setIsVisible={setShowConfirmModal}
|
setIsVisible={setShowConfirmModal}
|
||||||
onJoinClub={() => navigate(Routes.Payments)}
|
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>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { goBack, navigate } from '../../navigation/NavigationService'
|
|||||||
import { useUser, useUserData } from '../../providers/UserDataProvider'
|
import { useUser, useUserData } from '../../providers/UserDataProvider'
|
||||||
import { gutters, Palette, Style } from '../../styles'
|
import { gutters, Palette, Style } from '../../styles'
|
||||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||||
|
|
||||||
const ValidateCover = () => {
|
const ValidateCover = () => {
|
||||||
const { selectedProject, updateProjectData } = useUserData()
|
const { selectedProject, updateProjectData } = useUserData()
|
||||||
const { videos } = useUser()
|
const { videos } = useUser()
|
||||||
@@ -102,7 +101,7 @@ const ValidateCover = () => {
|
|||||||
selectedOption,
|
selectedOption,
|
||||||
coverOptions,
|
coverOptions,
|
||||||
})
|
})
|
||||||
}, [selectedProject, selectedOption, coverOptions])
|
}, [coverOptions, selectedOption, selectedProject])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
<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%',
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user