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 { 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()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
)
|
||||
|
||||
@@ -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%',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user