pay for playback download checkout

This commit is contained in:
2026-02-23 16:59:12 +01:00
parent 947d0e9641
commit e608c17775
8 changed files with 429 additions and 38 deletions
+80
View File
@@ -231,6 +231,85 @@ const createSongDownloadCheckoutSession = onCall({ region: REGION }, async (requ
} }
}) })
const createPlaybackDownloadCheckoutSession = onCall({ region: REGION }, async (request) => {
try {
const uid = request?.auth?.uid
if (!uid) {
throw new HttpsError('unauthenticated', 'Connecte-toi pour télécharger ton playback.')
}
const rawProjectId = request?.data?.projectId
const projectId = typeof rawProjectId === 'string' ? rawProjectId.trim() : ''
if (!projectId) {
throw new HttpsError('invalid-argument', 'Un identifiant de projet est requis.')
}
const stripe = getStripeClient()
const { lineItems } = await buildCheckoutLineItems(
[
{
label: 'Téléchargement playback',
unitAmount: 199,
currency: 'eur',
quantity: 1,
isRenewable: false,
},
],
{ stripe }
)
const uiMode = resolveCheckoutUiMode(request)
const shouldProvideReturnUrls = uiMode !== 'embedded'
const { successUrl, cancelUrl } = shouldProvideReturnUrls
? getReturnUrls(request?.data?.returnUrls)
: { successUrl: null, cancelUrl: null }
const { customerId } = await ensureStripeCustomer({
uid,
stripe,
refsList,
createIfMissing: true,
})
if (!customerId) {
throw new HttpsError(
'failed-precondition',
'Impossible de retrouver le client Stripe associé.'
)
}
const session = await stripe.checkout.sessions.create(
withCheckoutNavigationParams(
{
mode: 'payment',
customer: customerId,
line_items: lineItems,
allow_promotion_codes: false,
metadata: {
firebaseUID: uid,
purchaseType: 'PLAYBACK_DOWNLOAD',
projectId,
},
},
{
uiMode,
successUrl,
cancelUrl,
}
)
)
return formatCheckoutSessionResponse(session)
} catch (error) {
console.error('[subscription-createPlaybackDownloadCheckoutSession] error', error)
if (error instanceof HttpsError) {
throw error
}
throw mapStripeErrorToHttps(error, "Impossible de créer la session d'achat.")
}
})
const createCoinPackCheckoutSession = onCall({ region: REGION }, async (request) => { const createCoinPackCheckoutSession = onCall({ region: REGION }, async (request) => {
try { try {
const uid = request?.auth?.uid const uid = request?.auth?.uid
@@ -352,6 +431,7 @@ const createCoinPackCheckoutSession = onCall({ region: REGION }, async (request)
module.exports = { module.exports = {
createSubscriptionCheckoutSession, createSubscriptionCheckoutSession,
createSongDownloadCheckoutSession, createSongDownloadCheckoutSession,
createPlaybackDownloadCheckoutSession,
createCoinPackCheckoutSession, createCoinPackCheckoutSession,
resolveCheckoutUiMode, resolveCheckoutUiMode,
} }
+2
View File
@@ -2,6 +2,7 @@ const { listSubscriptionPlans, listCoinPacks } = require('./catalog')
const { const {
createSubscriptionCheckoutSession, createSubscriptionCheckoutSession,
createSongDownloadCheckoutSession, createSongDownloadCheckoutSession,
createPlaybackDownloadCheckoutSession,
createCoinPackCheckoutSession, createCoinPackCheckoutSession,
} = require('./checkout') } = require('./checkout')
const { cancelActiveSubscription, getActiveSubscription } = require('./management') const { cancelActiveSubscription, getActiveSubscription } = require('./management')
@@ -12,6 +13,7 @@ module.exports = {
listSubscriptionPlans, listSubscriptionPlans,
createSubscriptionCheckoutSession, createSubscriptionCheckoutSession,
createSongDownloadCheckoutSession, createSongDownloadCheckoutSession,
createPlaybackDownloadCheckoutSession,
cancelActiveSubscription, cancelActiveSubscription,
getActiveSubscription, getActiveSubscription,
listCoinPacks, listCoinPacks,
+2 -2
View File
@@ -137,7 +137,7 @@ const getActiveSubscription = onCall({ region: REGION }, async (request) => {
if (subscriptionId) { if (subscriptionId) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId, { const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
expand: ['items.data.price.product'], expand: ['items.data.price'],
}) })
if (subscription) { if (subscription) {
return { return {
@@ -152,7 +152,7 @@ const getActiveSubscription = onCall({ region: REGION }, async (request) => {
customer: customerId, customer: customerId,
status: 'all', status: 'all',
limit: 5, limit: 5,
expand: ['data.items.data.price.product'], expand: ['data.items.data.price'],
}) })
const [subscription] = response?.data || [] const [subscription] = response?.data || []
if (subscription && ACTIVE_SUBSCRIPTION_STATUSES.has(subscription.status)) { if (subscription && ACTIVE_SUBSCRIPTION_STATUSES.has(subscription.status)) {
+85
View File
@@ -235,6 +235,91 @@ const handleCheckoutSessionCompleted = async (session, event, { stripe } = {}) =
} }
} }
} }
if (
session.mode === 'payment' &&
(session.payment_status === 'paid' || session.payment_status === 'no_payment_required') &&
session.metadata?.purchaseType === 'PLAYBACK_DOWNLOAD'
) {
const rawProjectId = session.metadata?.projectId
const projectId = typeof rawProjectId === 'string' ? rawProjectId.trim() : ''
if (projectId && paymentDocRef) {
let paymentSnapshot = null
try {
paymentSnapshot = await paymentDocRef.get()
} catch (error) {
console.warn(
'[subscription-handleCheckoutSessionCompleted] Unable to read payment doc',
session.id,
error?.message || error
)
}
const alreadyGranted = Boolean(
paymentSnapshot?.exists && paymentSnapshot.data()?.playbackDownloadGrantedAt
)
if (!alreadyGranted) {
const projectRef = refsList?.projects?.doc(projectId) || null
let shouldGrant = true
if (projectRef) {
try {
const projectSnapshot = await projectRef.get()
if (!projectSnapshot.exists) {
shouldGrant = false
} else {
const projectData = projectSnapshot.data() || {}
const ownerId =
typeof projectData?.userId === 'string' ? projectData.userId.trim() : ''
const targetUserId = uid || firebaseUid || userRef?.id || null
if (ownerId && targetUserId && ownerId !== targetUserId) {
console.warn(
'[subscription-handleCheckoutSessionCompleted] Project owner mismatch',
projectId
)
shouldGrant = false
}
}
} catch (error) {
console.warn(
'[subscription-handleCheckoutSessionCompleted] Unable to read project',
projectId,
error?.message || error
)
}
} else {
shouldGrant = false
}
if (shouldGrant && projectRef) {
await projectRef.set(
{
playbackDownloadPurchase: {
status: 'paid',
paymentId: session.id || null,
paymentIntentId:
typeof session.payment_intent === 'string' ? session.payment_intent : null,
amount: session.amount_total ?? null,
currency: session.currency || null,
paidAt: getServerTimestamp(),
},
},
{ merge: true }
)
await paymentDocRef.set(
{
playbackDownloadGrantedAt: getServerTimestamp(),
playbackDownloadProjectId: projectId,
},
{ merge: true }
)
}
}
}
}
} }
const handleCustomerSubscriptionEvent = async (subscription, event, { stripe } = {}) => { const handleCustomerSubscriptionEvent = async (subscription, event, { stripe } = {}) => {
+34
View File
@@ -242,6 +242,38 @@ const StripeProvider = ({ children }) => {
[canUseEmbeddedCheckout, runCheckoutSession] [canUseEmbeddedCheckout, runCheckoutSession]
) )
const createPlaybackDownloadCheckout = React.useCallback(
async (projectId) => {
if (!projectId) {
throw new Error('Aucun projet sélectionné.')
}
if (isWeb && !canUseEmbeddedCheckout) {
console.error('[StripeProvider] checkout blocked (missing embedded support)', {
hasStripeKey: Boolean(stripePublishableKey),
isClientSecretReady: false,
})
throw new Error(
'Le paiement intégré Stripe est indisponible pour le moment (clé Stripe ou client secret absent).'
)
}
const shouldUseEmbeddedCheckout = canUseEmbeddedCheckout
await runCheckoutSession({
callableName: 'subscription-createPlaybackDownloadCheckoutSession',
payload: {
projectId,
returnUrls: {
successUrl: STRIPE_SUCCESS_URL,
cancelUrl: STRIPE_CANCEL_URL,
},
uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted',
},
logTag: 'playback download checkout error',
useEmbeddedFlow: shouldUseEmbeddedCheckout,
})
},
[canUseEmbeddedCheckout, runCheckoutSession]
)
const createCoinPackCheckout = React.useCallback( const createCoinPackCheckout = React.useCallback(
async (productId) => { async (productId) => {
if (!productId) { if (!productId) {
@@ -376,6 +408,7 @@ const StripeProvider = ({ children }) => {
refreshCatalog: fetchStripeCatalog, refreshCatalog: fetchStripeCatalog,
createSubscriptionCheckout, createSubscriptionCheckout,
createSongDownloadCheckout, createSongDownloadCheckout,
createPlaybackDownloadCheckout,
createCoinPackCheckout, createCoinPackCheckout,
openEmbeddedCheckout: setClientSecret, openEmbeddedCheckout: setClientSecret,
closeEmbeddedCheckout, closeEmbeddedCheckout,
@@ -393,6 +426,7 @@ const StripeProvider = ({ children }) => {
fetchStripeCatalog, fetchStripeCatalog,
createSubscriptionCheckout, createSubscriptionCheckout,
createSongDownloadCheckout, createSongDownloadCheckout,
createPlaybackDownloadCheckout,
createCoinPackCheckout, createCoinPackCheckout,
setClientSecret, setClientSecret,
closeEmbeddedCheckout, closeEmbeddedCheckout,
+190 -36
View File
@@ -1,11 +1,12 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native' import { Alert, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
import { MaterialCommunityIcons } from '@expo/vector-icons' import { MaterialCommunityIcons } from '@expo/vector-icons'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js' import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import * as FileSystem from 'expo-file-system' import * as FileSystem from 'expo-file-system'
import { shareAsync } from 'expo-sharing' import { shareAsync } from 'expo-sharing'
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo' import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
import BorderGradientButton from '../../components/BorderGradientButton' import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader' import MusicLandHeader from '../../components/MusicLandHeader'
import AppCheckbox from '../../components/AppCheckbox' import AppCheckbox from '../../components/AppCheckbox'
import firebase, { projectsRef, serverTimestamp } from '../../config/firebase' import firebase, { projectsRef, serverTimestamp } from '../../config/firebase'
@@ -14,6 +15,7 @@ import Page from '../../layouts/Page'
import { Routes } from '../../navigation' import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService' import { goBack, navigate } from '../../navigation/NavigationService'
import { useUserData, useUser } from '../../providers/UserDataProvider' import { useUserData, useUser } from '../../providers/UserDataProvider'
import { useStripe } from '../../providers/StripeProvider'
import { Palette } from '../../styles' import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts' import { FONT_FAMILY } from '../../styles/Fonts'
import { size } from '../../styles/Style' import { size } from '../../styles/Style'
@@ -134,26 +136,44 @@ const uploadSourceRecording = async ({ uri, uid, projectId }) => {
} }
const PlaybackDownload = ({ route }) => { const PlaybackDownload = ({ route }) => {
const { currentUID } = useUserData() const { currentUID, selectedProject } = useUserData()
const { hasActiveSubscription, videos } = useUser() || {} const { hasActiveSubscription, videos } = useUser() || {}
const { action: routeAction, uri, project } = route.params || {} const { createPlaybackDownloadCheckout } = useStripe()
const { action: routeAction, uri, project: routeProject } = route.params || {}
const action = routeAction || 'playback' const action = routeAction || 'playback'
console.log('[PlaybackDownload] route params', { console.log('[PlaybackDownload] route params', {
action, action,
projectId: project?.id, projectId: routeProject?.id,
hasUri: Boolean(uri), hasUri: Boolean(uri),
currentUID, currentUID,
}) })
const { setIsLoading, setTooltip } = useMinuit() const { setIsLoading, setTooltip } = useMinuit()
const [isAfterPlaybackVideoVisible, setIsAfterPlaybackVideoVisible] = useState(false) const [isAfterPlaybackVideoVisible, setIsAfterPlaybackVideoVisible] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false) const [showConfirmModal, setShowConfirmModal] = useState(false)
const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(project?.playbackUrl || null) const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(routeProject?.playbackUrl || null)
const [isPublishing, setIsPublishing] = useState(false) const [isPublishing, setIsPublishing] = useState(false)
const [isDownloading, setIsDownloading] = useState(false) const [isDownloading, setIsDownloading] = useState(false)
const [isCheckoutLaunching, setIsCheckoutLaunching] = useState(false)
const [downloadPaymentPending, setDownloadPaymentPending] = useState(false)
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true) const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
const hasShownAfterPlaybackRef = useRef(false) const hasShownAfterPlaybackRef = useRef(false)
const publishSuccessMessage = action === 'playback' ? 'Playback publié !' : 'Chanson publiée !' const publishSuccessMessage = action === 'playback' ? 'Playback publié !' : 'Chanson publiée !'
const projectForDownload = useMemo(() => {
if (routeProject?.id && selectedProject?.id && routeProject.id === selectedProject.id) {
return selectedProject
}
return routeProject || selectedProject || null
}, [routeProject, selectedProject])
const playbackDownloadStatus = projectForDownload?.playbackDownloadPurchase?.status || null
const hasPaidPlaybackDownload = playbackDownloadStatus === 'paid'
const canDownloadPlayback = hasActiveSubscription || hasPaidPlaybackDownload
const downloadLabel = canDownloadPlayback
? 'Télécharger le playback'
: 'Acheter le playback pour 1,99€'
const showSubscriptionCta = !hasActiveSubscription
const afterPlaybackUrl = useMemo(() => { const afterPlaybackUrl = useMemo(() => {
if (!videos) return null if (!videos) return null
return videos?.afterPlayback || null return videos?.afterPlayback || null
@@ -168,28 +188,30 @@ const PlaybackDownload = ({ route }) => {
}, [action, afterPlaybackUrl]) }, [action, afterPlaybackUrl])
useEffect(() => { useEffect(() => {
if (project?.playbackUrl) { if (projectForDownload?.playbackUrl) {
setPendingPlaybackUrl(project.playbackUrl) setPendingPlaybackUrl(projectForDownload.playbackUrl)
} }
}, [project?.playbackUrl]) }, [projectForDownload?.playbackUrl])
const resolveAudioUrl = useCallback(() => { const resolveAudioUrl = useCallback(() => {
if (typeof project?.songUrl === 'string' && project.songUrl.trim()) { if (typeof projectForDownload?.songUrl === 'string' && projectForDownload.songUrl.trim()) {
return project.songUrl.trim() return projectForDownload.songUrl.trim()
} }
const urls = Array.isArray(project?.musicUrls) ? project.musicUrls : [] const urls = Array.isArray(projectForDownload?.musicUrls) ? projectForDownload.musicUrls : []
if (!urls.length) return null if (!urls.length) return null
const idx = Number.isFinite(Number(project?.songIndex)) ? Number(project.songIndex) : 0 const idx = Number.isFinite(Number(projectForDownload?.songIndex))
? Number(projectForDownload.songIndex)
: 0
const candidate = urls[idx] const candidate = urls[idx]
if (typeof candidate === 'string' && candidate.trim()) { if (typeof candidate === 'string' && candidate.trim()) {
return candidate.trim() return candidate.trim()
} }
return null return null
}, [project]) }, [projectForDownload])
const handleDownloadUri = async () => { const handleDownloadUri = useCallback(async () => {
if (isPublishing || isDownloading) return if (isPublishing || isDownloading) return
if (action !== 'playback') { if (action !== 'playback') {
setTooltip({ setTooltip({
@@ -199,18 +221,18 @@ const PlaybackDownload = ({ route }) => {
return return
} }
const playbackUrlToDownload = pendingPlaybackUrl || project?.playbackUrl || null const playbackUrlToDownload = pendingPlaybackUrl || projectForDownload?.playbackUrl || null
if (playbackUrlToDownload) { if (playbackUrlToDownload) {
setIsDownloading(true) setIsDownloading(true)
try { try {
await triggerDownload(playbackUrlToDownload, project?.title) await triggerDownload(playbackUrlToDownload, projectForDownload?.title)
} finally { } finally {
setIsDownloading(false) setIsDownloading(false)
} }
return return
} }
if (!project?.id) { if (!projectForDownload?.id) {
setTooltip({ setTooltip({
type: 'error', type: 'error',
text: 'Projet introuvable pour ce playback', text: 'Projet introuvable pour ce playback',
@@ -243,9 +265,8 @@ const PlaybackDownload = ({ route }) => {
return return
} }
// Publication du playback
console.log('[PlaybackDownload] handleDownloadUri playback', { console.log('[PlaybackDownload] handleDownloadUri playback', {
projectId: project.id, projectId: projectForDownload.id,
uri, uri,
currentUID, currentUID,
}) })
@@ -257,17 +278,17 @@ const PlaybackDownload = ({ route }) => {
const { sourcePath, videoUrl } = await uploadSourceRecording({ const { sourcePath, videoUrl } = await uploadSourceRecording({
uri, uri,
uid: currentUID, uid: currentUID,
projectId: project.id, projectId: projectForDownload.id,
}) })
tempSourcePath = sourcePath tempSourcePath = sourcePath
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio') const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio')
const payload = { const payload = {
projectId: project?.id, projectId: projectForDownload?.id,
videoUrl, videoUrl,
audioUrl, audioUrl,
storagePath: `users/${currentUID}/projects/${project.id}/playback.mp4`, storagePath: `users/${currentUID}/projects/${projectForDownload.id}/playback.mp4`,
} }
console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload) console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload)
@@ -284,7 +305,7 @@ const PlaybackDownload = ({ route }) => {
type: 'success', type: 'success',
text: 'Playback prêt à télécharger', text: 'Playback prêt à télécharger',
}) })
await triggerDownload(resultURI, project?.title) await triggerDownload(resultURI, projectForDownload?.title)
if (tempSourcePath) { if (tempSourcePath) {
try { try {
await firebase.storage().ref(tempSourcePath).delete() await firebase.storage().ref(tempSourcePath).delete()
@@ -322,7 +343,92 @@ const PlaybackDownload = ({ route }) => {
setIsDownloading(false) setIsDownloading(false)
setIsLoading(false) setIsLoading(false)
} }
} }, [
action,
currentUID,
isDownloading,
isPublishing,
pendingPlaybackUrl,
projectForDownload,
resolveAudioUrl,
setIsLoading,
setTooltip,
uri,
])
const startPlaybackDownloadCheckout = useCallback(
async ({ force = false } = {}) => {
if (!projectForDownload?.id) {
setTooltip({
type: 'error',
text: "Impossible d'identifier le projet pour le paiement.",
})
return
}
if (!force && (isCheckoutLaunching || downloadPaymentPending)) {
return
}
setIsCheckoutLaunching(true)
setDownloadPaymentPending(true)
try {
await createPlaybackDownloadCheckout(projectForDownload.id)
} catch (error) {
setDownloadPaymentPending(false)
setTooltip({
type: 'error',
text: error?.message || "Une erreur est survenue lors du paiement.",
})
} finally {
setIsCheckoutLaunching(false)
}
},
[
createPlaybackDownloadCheckout,
downloadPaymentPending,
isCheckoutLaunching,
projectForDownload,
setTooltip,
]
)
const handleDownloadPress = useCallback(() => {
if (isDownloading || isCheckoutLaunching) {
return
}
if (canDownloadPlayback) {
handleDownloadUri()
return
}
if (downloadPaymentPending) {
Alert.alert(
'Paiement en attente',
"Le paiement n'est pas encore confirmé. Si tu as déjà payé, patiente quelques instants.",
[
{ text: 'Attendre', style: 'cancel' },
{ text: 'Relancer le paiement', onPress: () => startPlaybackDownloadCheckout({ force: true }) },
]
)
return
}
startPlaybackDownloadCheckout()
}, [
canDownloadPlayback,
downloadPaymentPending,
handleDownloadUri,
isCheckoutLaunching,
isDownloading,
startPlaybackDownloadCheckout,
])
useEffect(() => {
if (!downloadPaymentPending || !hasPaidPlaybackDownload || isDownloading) {
return
}
setDownloadPaymentPending(false)
handleDownloadUri()
}, [downloadPaymentPending, handleDownloadUri, hasPaidPlaybackDownload, isDownloading])
const handlePublish = async () => { const handlePublish = async () => {
if (isPublishing) return if (isPublishing) return
@@ -333,7 +439,7 @@ const PlaybackDownload = ({ route }) => {
}) })
return return
} }
if (!project?.id) { if (!projectForDownload?.id) {
setTooltip({ setTooltip({
type: 'success', type: 'success',
text: publishSuccessMessage, text: publishSuccessMessage,
@@ -342,7 +448,7 @@ const PlaybackDownload = ({ route }) => {
return return
} }
let tempSourcePath = null let tempSourcePath = null
let playbackUrlToSave = pendingPlaybackUrl || project?.playbackUrl || null let playbackUrlToSave = pendingPlaybackUrl || projectForDownload?.playbackUrl || null
const audioUrl = resolveAudioUrl() const audioUrl = resolveAudioUrl()
if (!audioUrl && !playbackUrlToSave) { if (!audioUrl && !playbackUrlToSave) {
setTooltip({ setTooltip({
@@ -367,17 +473,17 @@ const PlaybackDownload = ({ route }) => {
const { sourcePath, videoUrl } = await uploadSourceRecording({ const { sourcePath, videoUrl } = await uploadSourceRecording({
uri, uri,
uid: currentUID, uid: currentUID,
projectId: project.id, projectId: projectForDownload.id,
}) })
tempSourcePath = sourcePath tempSourcePath = sourcePath
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio') const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio')
const payload = { const payload = {
projectId: project?.id, projectId: projectForDownload?.id,
videoUrl, videoUrl,
audioUrl, audioUrl,
storagePath: `users/${currentUID}/projects/${project.id}/playback.mp4`, storagePath: `users/${currentUID}/projects/${projectForDownload.id}/playback.mp4`,
} }
console.log('[PlaybackDownload] publish: calling merge', payload) console.log('[PlaybackDownload] publish: calling merge', payload)
@@ -391,7 +497,7 @@ const PlaybackDownload = ({ route }) => {
setPendingPlaybackUrl(playbackUrlToSave) setPendingPlaybackUrl(playbackUrlToSave)
} }
await projectsRef.doc(project.id).set( await projectsRef.doc(projectForDownload.id).set(
{ {
playbackUrl: playbackUrlToSave, playbackUrl: playbackUrlToSave,
updatedAt: serverTimestamp(), updatedAt: serverTimestamp(),
@@ -452,9 +558,9 @@ const PlaybackDownload = ({ route }) => {
<CreateLyricsHeader title={'Publier ton playback'} /> <CreateLyricsHeader title={'Publier ton playback'} />
<View style={styles.coverRow}> <View style={styles.coverRow}>
{project?.coverUrl ? ( {projectForDownload?.coverUrl ? (
<ExpoImage <ExpoImage
source={{ uri: project?.coverUrl }} source={{ uri: projectForDownload?.coverUrl }}
style={styles.coverImage} style={styles.coverImage}
contentFit="cover" contentFit="cover"
/> />
@@ -465,15 +571,40 @@ const PlaybackDownload = ({ route }) => {
)} )}
<Pressable <Pressable
style={[styles.downloadTile, (isPublishing || isDownloading) && { opacity: 0.6 }]} style={[
onPress={handleDownloadUri} styles.downloadTile,
disabled={isPublishing || isDownloading} (isPublishing || isDownloading || isCheckoutLaunching) && { opacity: 0.6 },
]}
onPress={handleDownloadPress}
disabled={isPublishing || isDownloading || isCheckoutLaunching}
> >
<MaterialCommunityIcons name="download" size={22} color={Palette.white} /> <MaterialCommunityIcons name="download" size={22} color={Palette.white} />
<Text style={styles.downloadText}>Télécharger le playback</Text> <Text style={styles.downloadText}>{downloadLabel}</Text>
</Pressable> </Pressable>
{!canDownloadPlayback && downloadPaymentPending ? (
<Text style={styles.downloadPendingText}>
Paiement en attente de confirmation...
</Text>
) : null}
</View> </View>
{showSubscriptionCta ? (
<View style={styles.subscriptionCta}>
<Text style={styles.subscriptionNote}>
Avec un abonnement, le téléchargement est gratuit.
</Text>
<GradientButton
title="Voir les abonnements"
onPress={() => navigate(Routes.Payments)}
containerStyle={styles.subscriptionButton}
/>
</View>
) : (
<Text style={styles.subscriptionNote}>
Avec ton abonnement, le téléchargement est gratuit.
</Text>
)}
<ClubAdvantagesCard style={styles.clubCardSpacing} /> <ClubAdvantagesCard style={styles.clubCardSpacing} />
<View style={styles.publishConsentContainer}> <View style={styles.publishConsentContainer}>
@@ -547,6 +678,29 @@ const styles = StyleSheet.create({
fontSize: 15, fontSize: 15,
color: Palette.white, color: Palette.white,
}, },
downloadPendingText: {
marginTop: 6,
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 12,
color: Palette.white,
opacity: 0.8,
},
subscriptionCta: {
alignItems: 'center',
gap: 10,
marginTop: 6,
},
subscriptionNote: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
color: Palette.white,
opacity: 0.8,
textAlign: 'center',
},
subscriptionButton: {
width: '100%',
maxWidth: 280,
},
clubCardSpacing: { clubCardSpacing: {
marginTop: 10, marginTop: 10,
}, },
+35
View File
@@ -13,6 +13,7 @@ import {
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js' import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import * as FileSystem from 'expo-file-system' import * as FileSystem from 'expo-file-system'
import * as Sharing from 'expo-sharing' import * as Sharing from 'expo-sharing'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader' import MusicLandHeader from '../../components/MusicLandHeader'
import Page from '../../layouts/Page' import Page from '../../layouts/Page'
import { Routes } from '../../navigation/Routes' import { Routes } from '../../navigation/Routes'
@@ -103,6 +104,7 @@ const SongDownload = ({ route }) => {
const downloadLabel = canDownloadDirectly const downloadLabel = canDownloadDirectly
? 'Télécharger mon morceau' ? 'Télécharger mon morceau'
: 'Acheter ce morceau pour 1,99€' : 'Acheter ce morceau pour 1,99€'
const showSubscriptionCta = !hasActiveSubscription
const shouldShowDownloadPage = adventureChoice === 'stop' const shouldShowDownloadPage = adventureChoice === 'stop'
useEffect(() => { useEffect(() => {
@@ -530,6 +532,23 @@ const SongDownload = ({ route }) => {
</View> </View>
</View> </View>
{showSubscriptionCta ? (
<View style={styles.subscriptionCta}>
<Text style={styles.subscriptionNote}>
Avec un abonnement, le téléchargement est gratuit.
</Text>
<GradientButton
title="Voir les abonnements"
onPress={() => navigate(Routes.Payments)}
containerStyle={styles.subscriptionButton}
/>
</View>
) : (
<Text style={styles.subscriptionNote}>
Avec ton abonnement, le téléchargement est gratuit.
</Text>
)}
<ClubAdvantagesCard style={styles.clubCardSpacing} /> <ClubAdvantagesCard style={styles.clubCardSpacing} />
</ScrollView> </ScrollView>
) : ( ) : (
@@ -620,6 +639,22 @@ const styles = StyleSheet.create({
color: Palette.white, color: Palette.white,
opacity: 0.8, opacity: 0.8,
}, },
subscriptionCta: {
alignItems: 'center',
gap: gutters * 0.6,
marginTop: gutters * 0.4,
},
subscriptionNote: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 13,
color: Palette.white,
opacity: 0.8,
textAlign: 'center',
},
subscriptionButton: {
width: '100%',
maxWidth: 280,
},
adventureGatePlaceholder: { adventureGatePlaceholder: {
flex: 1, flex: 1,
}, },
+1
View File
@@ -5,6 +5,7 @@ import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import { background } from '../../assets' import { background } from '../../assets'
import GradientButton from '../../components/GradientButton' import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader' import MusicLandHeader from '../../components/MusicLandHeader'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page' import Page from '../../layouts/Page'
import { Routes } from '../../navigation' import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService' import { goBack, navigate } from '../../navigation/NavigationService'