pay for playback download checkout
This commit is contained in:
@@ -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,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,
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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 } = {}) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user