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) => {
try {
const uid = request?.auth?.uid
@@ -352,6 +431,7 @@ const createCoinPackCheckoutSession = onCall({ region: REGION }, async (request)
module.exports = {
createSubscriptionCheckoutSession,
createSongDownloadCheckoutSession,
createPlaybackDownloadCheckoutSession,
createCoinPackCheckoutSession,
resolveCheckoutUiMode,
}
+2
View File
@@ -2,6 +2,7 @@ const { listSubscriptionPlans, listCoinPacks } = require('./catalog')
const {
createSubscriptionCheckoutSession,
createSongDownloadCheckoutSession,
createPlaybackDownloadCheckoutSession,
createCoinPackCheckoutSession,
} = require('./checkout')
const { cancelActiveSubscription, getActiveSubscription } = require('./management')
@@ -12,6 +13,7 @@ module.exports = {
listSubscriptionPlans,
createSubscriptionCheckoutSession,
createSongDownloadCheckoutSession,
createPlaybackDownloadCheckoutSession,
cancelActiveSubscription,
getActiveSubscription,
listCoinPacks,
+2 -2
View File
@@ -137,7 +137,7 @@ const getActiveSubscription = onCall({ region: REGION }, async (request) => {
if (subscriptionId) {
const subscription = await stripe.subscriptions.retrieve(subscriptionId, {
expand: ['items.data.price.product'],
expand: ['items.data.price'],
})
if (subscription) {
return {
@@ -152,7 +152,7 @@ const getActiveSubscription = onCall({ region: REGION }, async (request) => {
customer: customerId,
status: 'all',
limit: 5,
expand: ['data.items.data.price.product'],
expand: ['data.items.data.price'],
})
const [subscription] = response?.data || []
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 } = {}) => {