pay for music
This commit is contained in:
@@ -152,6 +152,85 @@ const createSubscriptionCheckoutSession = onCall({ region: REGION }, async (requ
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const createSongDownloadCheckoutSession = onCall({ region: REGION }, async (request) => {
|
||||||
|
try {
|
||||||
|
const uid = request?.auth?.uid
|
||||||
|
if (!uid) {
|
||||||
|
throw new HttpsError('unauthenticated', 'Connecte-toi pour télécharger ton morceau.')
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Musicland',
|
||||||
|
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: 'SONG_DOWNLOAD',
|
||||||
|
projectId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uiMode,
|
||||||
|
successUrl,
|
||||||
|
cancelUrl,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return formatCheckoutSessionResponse(session)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[subscription-createSongDownloadCheckoutSession] 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
|
||||||
@@ -272,6 +351,7 @@ const createCoinPackCheckoutSession = onCall({ region: REGION }, async (request)
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createSubscriptionCheckoutSession,
|
createSubscriptionCheckoutSession,
|
||||||
|
createSongDownloadCheckoutSession,
|
||||||
createCoinPackCheckoutSession,
|
createCoinPackCheckoutSession,
|
||||||
resolveCheckoutUiMode,
|
resolveCheckoutUiMode,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
const { listSubscriptionPlans, listCoinPacks } = require('./catalog')
|
const { listSubscriptionPlans, listCoinPacks } = require('./catalog')
|
||||||
const { createSubscriptionCheckoutSession, createCoinPackCheckoutSession } = require('./checkout')
|
const {
|
||||||
|
createSubscriptionCheckoutSession,
|
||||||
|
createSongDownloadCheckoutSession,
|
||||||
|
createCoinPackCheckoutSession,
|
||||||
|
} = require('./checkout')
|
||||||
const { cancelActiveSubscription, getActiveSubscription } = require('./management')
|
const { cancelActiveSubscription, getActiveSubscription } = require('./management')
|
||||||
const { handleStripeWebhook } = require('./webhooks')
|
const { handleStripeWebhook } = require('./webhooks')
|
||||||
const { processAnnualSubscriptionAllowances } = require('./schedule')
|
const { processAnnualSubscriptionAllowances } = require('./schedule')
|
||||||
@@ -7,6 +11,7 @@ const { processAnnualSubscriptionAllowances } = require('./schedule')
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
listSubscriptionPlans,
|
listSubscriptionPlans,
|
||||||
createSubscriptionCheckoutSession,
|
createSubscriptionCheckoutSession,
|
||||||
|
createSongDownloadCheckoutSession,
|
||||||
cancelActiveSubscription,
|
cancelActiveSubscription,
|
||||||
getActiveSubscription,
|
getActiveSubscription,
|
||||||
listCoinPacks,
|
listCoinPacks,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { getStripeClient } = require('../../helpers/stripe')
|
|||||||
const { REGION } = require('./config')
|
const { REGION } = require('./config')
|
||||||
const {
|
const {
|
||||||
paymentsCollection,
|
paymentsCollection,
|
||||||
|
refsList,
|
||||||
resolveStripeWebhookSecret,
|
resolveStripeWebhookSecret,
|
||||||
getServerTimestamp,
|
getServerTimestamp,
|
||||||
toFirestoreTimestamp,
|
toFirestoreTimestamp,
|
||||||
@@ -149,6 +150,91 @@ const handleCheckoutSessionCompleted = async (session, event, { stripe } = {}) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
session.mode === 'payment' &&
|
||||||
|
(session.payment_status === 'paid' || session.payment_status === 'no_payment_required') &&
|
||||||
|
session.metadata?.purchaseType === 'SONG_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()?.downloadGrantedAt
|
||||||
|
)
|
||||||
|
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
downloadPurchase: {
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
downloadGrantedAt: getServerTimestamp(),
|
||||||
|
downloadProjectId: projectId,
|
||||||
|
},
|
||||||
|
{ merge: true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCustomerSubscriptionEvent = async (subscription, event, { stripe } = {}) => {
|
const handleCustomerSubscriptionEvent = async (subscription, event, { stripe } = {}) => {
|
||||||
|
|||||||
@@ -210,6 +210,38 @@ const StripeProvider = ({ children }) => {
|
|||||||
[canUseEmbeddedCheckout, runCheckoutSession]
|
[canUseEmbeddedCheckout, runCheckoutSession]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const createSongDownloadCheckout = 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-createSongDownloadCheckoutSession',
|
||||||
|
payload: {
|
||||||
|
projectId,
|
||||||
|
returnUrls: {
|
||||||
|
successUrl: STRIPE_SUCCESS_URL,
|
||||||
|
cancelUrl: STRIPE_CANCEL_URL,
|
||||||
|
},
|
||||||
|
uiMode: shouldUseEmbeddedCheckout ? 'embedded' : 'hosted',
|
||||||
|
},
|
||||||
|
logTag: 'song download checkout error',
|
||||||
|
useEmbeddedFlow: shouldUseEmbeddedCheckout,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[canUseEmbeddedCheckout, runCheckoutSession]
|
||||||
|
)
|
||||||
|
|
||||||
const createCoinPackCheckout = React.useCallback(
|
const createCoinPackCheckout = React.useCallback(
|
||||||
async (productId) => {
|
async (productId) => {
|
||||||
if (!productId) {
|
if (!productId) {
|
||||||
@@ -343,6 +375,7 @@ const StripeProvider = ({ children }) => {
|
|||||||
catalogError,
|
catalogError,
|
||||||
refreshCatalog: fetchStripeCatalog,
|
refreshCatalog: fetchStripeCatalog,
|
||||||
createSubscriptionCheckout,
|
createSubscriptionCheckout,
|
||||||
|
createSongDownloadCheckout,
|
||||||
createCoinPackCheckout,
|
createCoinPackCheckout,
|
||||||
openEmbeddedCheckout: setClientSecret,
|
openEmbeddedCheckout: setClientSecret,
|
||||||
closeEmbeddedCheckout,
|
closeEmbeddedCheckout,
|
||||||
@@ -359,6 +392,7 @@ const StripeProvider = ({ children }) => {
|
|||||||
catalogError,
|
catalogError,
|
||||||
fetchStripeCatalog,
|
fetchStripeCatalog,
|
||||||
createSubscriptionCheckout,
|
createSubscriptionCheckout,
|
||||||
|
createSongDownloadCheckout,
|
||||||
createCoinPackCheckout,
|
createCoinPackCheckout,
|
||||||
setClientSecret,
|
setClientSecret,
|
||||||
closeEmbeddedCheckout,
|
closeEmbeddedCheckout,
|
||||||
|
|||||||
+198
-107
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Image as ExpoImage } from 'expo-image'
|
import { Image as ExpoImage } from 'expo-image'
|
||||||
import {
|
import {
|
||||||
Linking,
|
Linking,
|
||||||
@@ -13,13 +13,12 @@ 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 AppCheckbox from '../../components/AppCheckbox'
|
|
||||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
|
||||||
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'
|
||||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||||
import { useUser } from '../../providers/UserDataProvider'
|
import { useUser } from '../../providers/UserDataProvider'
|
||||||
|
import { useStripe } from '../../providers/StripeProvider'
|
||||||
import { gutters, Palette, Style } from '../../styles'
|
import { gutters, Palette, Style } from '../../styles'
|
||||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||||
@@ -31,7 +30,6 @@ import { MaterialCommunityIcons } from '@expo/vector-icons'
|
|||||||
import { isWeb } from '../../hooks/useLayoutType'
|
import { isWeb } from '../../hooks/useLayoutType'
|
||||||
import { getArtistDisplayName } from '../../utils/artistName'
|
import { getArtistDisplayName } from '../../utils/artistName'
|
||||||
import { toDate } from '../../utils/dateFormatting'
|
import { toDate } from '../../utils/dateFormatting'
|
||||||
import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal'
|
|
||||||
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
||||||
import AdventureChoiceModal from './components/AdventureChoiceModal'
|
import AdventureChoiceModal from './components/AdventureChoiceModal'
|
||||||
import ShareAndCreditsModal from './components/ShareAndCreditsModal'
|
import ShareAndCreditsModal from './components/ShareAndCreditsModal'
|
||||||
@@ -43,21 +41,26 @@ const SongDownload = ({ route }) => {
|
|||||||
coverOptions: routeCoverOptions,
|
coverOptions: routeCoverOptions,
|
||||||
} = route?.params || {}
|
} = route?.params || {}
|
||||||
const { selectedProject, updateProjectData, hasActiveSubscription } = useUser()
|
const { selectedProject, updateProjectData, hasActiveSubscription } = useUser()
|
||||||
|
const { createSongDownloadCheckout } = useStripe()
|
||||||
const { setTooltip } = useMinuit()
|
const { setTooltip } = useMinuit()
|
||||||
const { setLoading } = useGlobalLoading()
|
const { setLoading } = useGlobalLoading()
|
||||||
const [isDownloading, setIsDownloading] = useState(false)
|
const [isDownloading, setIsDownloading] = useState(false)
|
||||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
const [isCheckoutLaunching, setIsCheckoutLaunching] = useState(false)
|
||||||
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
|
const [downloadPaymentPending, setDownloadPaymentPending] = useState(false)
|
||||||
const { videos } = useUser()
|
const { videos } = useUser()
|
||||||
const part1Url = isWeb ? videos?.benaiPart1 : videos?.benaiPart1
|
const part1Url = isWeb ? videos?.benaiPart1 : videos?.benaiPart1
|
||||||
const [showIntro, setShowIntro] = useState(null)
|
const [showIntro, setShowIntro] = useState(null)
|
||||||
const [showAdventureModal, setShowAdventureModal] = useState(false)
|
const [showAdventureModal, setShowAdventureModal] = useState(false)
|
||||||
const [showShareModal, setShowShareModal] = useState(false)
|
const [showShareModal, setShowShareModal] = useState(false)
|
||||||
|
const [adventureChoice, setAdventureChoice] = useState('pending')
|
||||||
|
const adventureGateTriggeredRef = useRef(false)
|
||||||
|
|
||||||
const projectForStage = useMemo(
|
const projectForStage = useMemo(() => {
|
||||||
() => routeProject || selectedProject || null,
|
if (routeProject?.id && selectedProject?.id && routeProject.id === selectedProject.id) {
|
||||||
[routeProject, selectedProject]
|
return selectedProject
|
||||||
)
|
}
|
||||||
|
return routeProject || selectedProject || null
|
||||||
|
}, [routeProject, selectedProject])
|
||||||
|
|
||||||
const coverOptions = useMemo(() => {
|
const coverOptions = useMemo(() => {
|
||||||
if (Array.isArray(routeCoverOptions) && routeCoverOptions.length) {
|
if (Array.isArray(routeCoverOptions) && routeCoverOptions.length) {
|
||||||
@@ -93,6 +96,47 @@ const SongDownload = ({ route }) => {
|
|||||||
typeof projectForStage?.title === 'string' && projectForStage.title.trim()
|
typeof projectForStage?.title === 'string' && projectForStage.title.trim()
|
||||||
? projectForStage.title.trim()
|
? projectForStage.title.trim()
|
||||||
: 'Musicland Track'
|
: 'Musicland Track'
|
||||||
|
const projectId = projectForStage?.id || null
|
||||||
|
const downloadPurchaseStatus = projectForStage?.downloadPurchase?.status || null
|
||||||
|
const hasPaidDownload = downloadPurchaseStatus === 'paid'
|
||||||
|
const canDownloadDirectly = hasActiveSubscription || hasPaidDownload
|
||||||
|
const downloadLabel = canDownloadDirectly
|
||||||
|
? 'Télécharger mon morceau'
|
||||||
|
: 'Acheter ce morceau pour 1,99€'
|
||||||
|
const shouldShowDownloadPage = adventureChoice === 'stop'
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (adventureGateTriggeredRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (adventureChoice !== 'pending') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeof videos === 'undefined') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
adventureGateTriggeredRef.current = true
|
||||||
|
if (part1Url) {
|
||||||
|
setShowIntro(part1Url)
|
||||||
|
} else {
|
||||||
|
setShowAdventureModal(true)
|
||||||
|
}
|
||||||
|
}, [adventureChoice, part1Url, videos])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (adventureChoice !== 'pending') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!adventureGateTriggeredRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (showIntro) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!showAdventureModal) {
|
||||||
|
setShowAdventureModal(true)
|
||||||
|
}
|
||||||
|
}, [adventureChoice, showAdventureModal, showIntro])
|
||||||
|
|
||||||
const continueFlow = useCallback(async () => {
|
const continueFlow = useCallback(async () => {
|
||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
@@ -137,32 +181,6 @@ const SongDownload = ({ route }) => {
|
|||||||
updateProjectData,
|
updateProjectData,
|
||||||
])
|
])
|
||||||
|
|
||||||
const handleContinue = useCallback(() => {
|
|
||||||
if (!hasAcceptedPublication) {
|
|
||||||
if (setTooltip) {
|
|
||||||
setTooltip({
|
|
||||||
type: 'error',
|
|
||||||
text: 'Confirme la diffusion sur Musicland et YouTube avant de continuer',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Alert.alert(
|
|
||||||
'Confirmation requise',
|
|
||||||
'Confirme la diffusion sur Musicland et YouTube avant de continuer'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (hasActiveSubscription) {
|
|
||||||
if (part1Url) {
|
|
||||||
setShowIntro(part1Url)
|
|
||||||
} else {
|
|
||||||
continueFlow()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setShowConfirmModal(true)
|
|
||||||
}, [continueFlow, hasAcceptedPublication, hasActiveSubscription, part1Url, setTooltip])
|
|
||||||
|
|
||||||
const handleCloseIntro = useCallback(() => {
|
const handleCloseIntro = useCallback(() => {
|
||||||
setShowIntro(null)
|
setShowIntro(null)
|
||||||
setShowAdventureModal(true)
|
setShowAdventureModal(true)
|
||||||
@@ -170,14 +188,10 @@ const SongDownload = ({ route }) => {
|
|||||||
|
|
||||||
const handleContinueAdventure = useCallback(() => {
|
const handleContinueAdventure = useCallback(() => {
|
||||||
setShowAdventureModal(false)
|
setShowAdventureModal(false)
|
||||||
|
setAdventureChoice('continue')
|
||||||
continueFlow()
|
continueFlow()
|
||||||
}, [continueFlow])
|
}, [continueFlow])
|
||||||
|
|
||||||
const handleStopAdventure = useCallback(() => {
|
|
||||||
setShowAdventureModal(false)
|
|
||||||
setShowShareModal(true)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleDownload = useCallback(async () => {
|
const handleDownload = useCallback(async () => {
|
||||||
const downloadUrl =
|
const downloadUrl =
|
||||||
projectForStage?.songUrl ||
|
projectForStage?.songUrl ||
|
||||||
@@ -387,64 +401,140 @@ const SongDownload = ({ route }) => {
|
|||||||
}
|
}
|
||||||
}, [coverUrl, isDownloading, projectForStage, selectedOption, trackTitle])
|
}, [coverUrl, isDownloading, projectForStage, selectedOption, trackTitle])
|
||||||
|
|
||||||
|
const startSongDownloadCheckout = useCallback(
|
||||||
|
async ({ force = false } = {}) => {
|
||||||
|
if (!projectId) {
|
||||||
|
if (setTooltip) {
|
||||||
|
setTooltip({
|
||||||
|
type: 'error',
|
||||||
|
text: "Impossible d'identifier le projet pour le paiement.",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Alert.alert('Paiement', "Impossible d'identifier le projet pour le paiement.")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force && (isCheckoutLaunching || downloadPaymentPending)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCheckoutLaunching(true)
|
||||||
|
setDownloadPaymentPending(true)
|
||||||
|
try {
|
||||||
|
await createSongDownloadCheckout(projectId)
|
||||||
|
} catch (error) {
|
||||||
|
setDownloadPaymentPending(false)
|
||||||
|
if (setTooltip) {
|
||||||
|
setTooltip({
|
||||||
|
type: 'error',
|
||||||
|
text: error?.message || "Une erreur est survenue lors du paiement.",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Alert.alert('Paiement', error?.message || "Une erreur est survenue lors du paiement.")
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsCheckoutLaunching(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
createSongDownloadCheckout,
|
||||||
|
downloadPaymentPending,
|
||||||
|
isCheckoutLaunching,
|
||||||
|
projectId,
|
||||||
|
setTooltip,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleDownloadPress = useCallback(() => {
|
||||||
|
if (isDownloading || isCheckoutLaunching) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (canDownloadDirectly) {
|
||||||
|
handleDownload()
|
||||||
|
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: () => startSongDownloadCheckout({ force: true }) },
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startSongDownloadCheckout()
|
||||||
|
}, [
|
||||||
|
canDownloadDirectly,
|
||||||
|
downloadPaymentPending,
|
||||||
|
handleDownload,
|
||||||
|
isCheckoutLaunching,
|
||||||
|
isDownloading,
|
||||||
|
startSongDownloadCheckout,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!downloadPaymentPending || !hasPaidDownload || isDownloading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setDownloadPaymentPending(false)
|
||||||
|
handleDownload()
|
||||||
|
}, [downloadPaymentPending, handleDownload, hasPaidDownload, isDownloading])
|
||||||
|
|
||||||
|
const handleStopAdventure = useCallback(() => {
|
||||||
|
setShowAdventureModal(false)
|
||||||
|
setAdventureChoice('stop')
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||||
<MusicLandHeader onPressBack={goBack} progress={84} />
|
<MusicLandHeader onPressBack={goBack} progress={84} />
|
||||||
<ScrollView
|
{shouldShowDownloadPage ? (
|
||||||
contentContainerStyle={[
|
<ScrollView
|
||||||
styles.container,
|
contentContainerStyle={[
|
||||||
!isWeb && styles.containerMobile,
|
styles.container,
|
||||||
styles.scrollContent,
|
!isWeb && styles.containerMobile,
|
||||||
]}
|
styles.scrollContent,
|
||||||
showsVerticalScrollIndicator={false}
|
]}
|
||||||
>
|
showsVerticalScrollIndicator={false}
|
||||||
<CreateLyricsHeader title="Ta pochette est validée" />
|
>
|
||||||
<View style={styles.coverRow}>
|
<CreateLyricsHeader title="Ta pochette est validée" />
|
||||||
{coverUrl ? (
|
<View style={styles.coverRow}>
|
||||||
<ExpoImage source={{ uri: coverUrl }} style={styles.coverImage} contentFit="cover" />
|
{coverUrl ? (
|
||||||
) : (
|
<ExpoImage source={{ uri: coverUrl }} style={styles.coverImage} contentFit="cover" />
|
||||||
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
) : (
|
||||||
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
<View style={[styles.coverImage, styles.coverPlaceholder]}>
|
||||||
|
<Text style={styles.placeholderText}>Aucune pochette</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={styles.downloadColumn}>
|
||||||
|
<Pressable
|
||||||
|
style={[
|
||||||
|
styles.downloadTile,
|
||||||
|
(isDownloading || isCheckoutLaunching) && { opacity: 0.6 },
|
||||||
|
]}
|
||||||
|
onPress={handleDownloadPress}
|
||||||
|
disabled={isDownloading || isCheckoutLaunching}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
||||||
|
<Text style={styles.downloadText}>{downloadLabel}</Text>
|
||||||
|
</Pressable>
|
||||||
|
{!canDownloadDirectly && downloadPaymentPending ? (
|
||||||
|
<Text style={styles.downloadPendingText}>
|
||||||
|
Paiement en attente de confirmation...
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
)}
|
</View>
|
||||||
|
|
||||||
<Pressable style={styles.downloadTile} onPress={handleDownload}>
|
<ClubAdvantagesCard style={styles.clubCardSpacing} />
|
||||||
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
</ScrollView>
|
||||||
<Text style={styles.downloadText}>Acheter ce morceau pour 1,99€</Text>
|
) : (
|
||||||
</Pressable>
|
<View style={styles.adventureGatePlaceholder} />
|
||||||
</View>
|
)}
|
||||||
|
|
||||||
<ClubAdvantagesCard style={styles.clubCardSpacing} />
|
|
||||||
|
|
||||||
<View style={styles.consentContainer}>
|
|
||||||
<AppCheckbox
|
|
||||||
selected={hasAcceptedPublication}
|
|
||||||
onPress={() => setHasAcceptedPublication((prev) => !prev)}
|
|
||||||
label="J'accepte la diffusion de mon contenu sur Musicland et YouTube."
|
|
||||||
/>
|
|
||||||
<Text style={styles.consentDescription}>
|
|
||||||
Cette confirmation est requise pour continuer.
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<BorderGradientButton
|
|
||||||
title={hasActiveSubscription ? 'Continuer' : 'Continuer sans générer de revenus'}
|
|
||||||
onPress={handleContinue}
|
|
||||||
containerStyle={styles.continueButton}
|
|
||||||
/>
|
|
||||||
</ScrollView>
|
|
||||||
<SubscriptionConfirmModal
|
|
||||||
isVisible={showConfirmModal}
|
|
||||||
setIsVisible={setShowConfirmModal}
|
|
||||||
onJoinClub={() => navigate(Routes.Payments)}
|
|
||||||
onContinue={() => {
|
|
||||||
if (part1Url) {
|
|
||||||
setShowIntro(part1Url)
|
|
||||||
} else {
|
|
||||||
continueFlow()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<FullscreenIntroVideo url={showIntro} visible={!!showIntro} onClose={handleCloseIntro} />
|
<FullscreenIntroVideo url={showIntro} visible={!!showIntro} onClose={handleCloseIntro} />
|
||||||
<AdventureChoiceModal
|
<AdventureChoiceModal
|
||||||
isVisible={showAdventureModal}
|
isVisible={showAdventureModal}
|
||||||
@@ -483,16 +573,6 @@ const styles = StyleSheet.create({
|
|||||||
scrollContent: {
|
scrollContent: {
|
||||||
paddingBottom: gutters * 2.6,
|
paddingBottom: gutters * 2.6,
|
||||||
},
|
},
|
||||||
consentContainer: {
|
|
||||||
gap: 6,
|
|
||||||
marginTop: 10,
|
|
||||||
},
|
|
||||||
consentDescription: {
|
|
||||||
fontSize: 13,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
color: Palette.white,
|
|
||||||
opacity: 0.8,
|
|
||||||
},
|
|
||||||
coverRow: {
|
coverRow: {
|
||||||
flexDirection: isWeb ? 'row' : 'column',
|
flexDirection: isWeb ? 'row' : 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -524,15 +604,26 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#8C4BFF',
|
backgroundColor: '#8C4BFF',
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
},
|
},
|
||||||
|
downloadColumn: {
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
},
|
||||||
downloadText: {
|
downloadText: {
|
||||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
},
|
},
|
||||||
clubCardSpacing: {
|
downloadPendingText: {
|
||||||
marginTop: gutters * 0.5,
|
marginTop: 6,
|
||||||
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
|
fontSize: 12,
|
||||||
|
color: Palette.white,
|
||||||
|
opacity: 0.8,
|
||||||
},
|
},
|
||||||
continueButton: {
|
adventureGatePlaceholder: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
clubCardSpacing: {
|
||||||
marginTop: gutters * 0.5,
|
marginTop: gutters * 0.5,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
|
||||||
import { Image as ExpoImage } from 'expo-image'
|
import { Image as ExpoImage } from 'expo-image'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { Pressable, Text, View } from 'react-native'
|
import { Pressable, Text, View } from 'react-native'
|
||||||
@@ -6,19 +5,14 @@ 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'
|
||||||
import { useUser, useUserData } from '../../providers/UserDataProvider'
|
import { useUserData } from '../../providers/UserDataProvider'
|
||||||
import { gutters, Palette, Style } from '../../styles'
|
import { gutters, Palette, Style } from '../../styles'
|
||||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||||
const ValidateCover = () => {
|
const ValidateCover = () => {
|
||||||
const { selectedProject, updateProjectData } = useUserData()
|
const { selectedProject, updateProjectData } = useUserData()
|
||||||
const { videos } = useUser()
|
|
||||||
const benhaiUrl = isWeb ? videos?.benhaiWeb || null : videos?.benhai
|
|
||||||
const [showIntro, setShowIntro] = useState(null)
|
|
||||||
|
|
||||||
const { setIsLoading } = useMinuit()
|
const { setIsLoading } = useMinuit()
|
||||||
|
|
||||||
const coverOptions = useMemo(() => {
|
const coverOptions = useMemo(() => {
|
||||||
@@ -91,17 +85,12 @@ const ValidateCover = () => {
|
|||||||
if (!selectedOption) {
|
if (!selectedOption) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setShowIntro(benhaiUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseIntro = useCallback(() => {
|
|
||||||
setShowIntro(null)
|
|
||||||
navigate(Routes.SongDownload, {
|
navigate(Routes.SongDownload, {
|
||||||
project: selectedProject,
|
project: selectedProject,
|
||||||
selectedOption,
|
selectedOption,
|
||||||
coverOptions,
|
coverOptions,
|
||||||
})
|
})
|
||||||
}, [coverOptions, selectedOption, selectedProject])
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||||
@@ -231,7 +220,6 @@ const ValidateCover = () => {
|
|||||||
disabled={!selectedOption || isSelecting}
|
disabled={!selectedOption || isSelecting}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<FullscreenIntroVideo url={showIntro} visible={!!showIntro} onClose={handleCloseIntro} />
|
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user