stripe embedded and other fixes
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
const { HttpsError, onCall } = require("firebase-functions/https");
|
||||
|
||||
const {
|
||||
getStripeClient,
|
||||
buildCheckoutLineItems,
|
||||
ensureStripeCustomer,
|
||||
getReturnUrls,
|
||||
formatCheckoutSessionResponse,
|
||||
mapStripeErrorToHttps,
|
||||
} = require("../../helpers/stripe");
|
||||
const { REGION } = require("./config");
|
||||
const {
|
||||
ALL_SUBSCRIPTION_PRICE_IDS,
|
||||
COIN_PACK_PRODUCT_IDS,
|
||||
COIN_PACK_PRODUCT_MAP,
|
||||
} = require("./constants");
|
||||
const {
|
||||
refsList,
|
||||
formatCoinPack,
|
||||
getSubscriptionMetaFromPrice,
|
||||
} = require("./shared");
|
||||
|
||||
const CHECKOUT_UI_MODES = new Set(["hosted", "embedded"]);
|
||||
|
||||
const sanitizePriceId = (value) => {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
return value.trim();
|
||||
};
|
||||
|
||||
const resolveCheckoutUiMode = (request) => {
|
||||
if (!request || !request.data || typeof request.data.uiMode === "undefined") {
|
||||
return "hosted";
|
||||
}
|
||||
|
||||
if (typeof request.data.uiMode !== "string") {
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
'uiMode doit être une chaîne de caractères ("hosted" ou "embedded").',
|
||||
);
|
||||
}
|
||||
|
||||
const normalizedUiMode = request.data.uiMode.trim().toLowerCase();
|
||||
if (!CHECKOUT_UI_MODES.has(normalizedUiMode)) {
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
`uiMode "${request.data.uiMode}" n'est pas supporté pour Stripe Checkout.`,
|
||||
);
|
||||
}
|
||||
|
||||
return normalizedUiMode;
|
||||
};
|
||||
|
||||
const withCheckoutNavigationParams = (
|
||||
baseParams,
|
||||
{ uiMode, successUrl, cancelUrl },
|
||||
) => {
|
||||
if (uiMode === "embedded") {
|
||||
return {
|
||||
...baseParams,
|
||||
ui_mode: "embedded",
|
||||
return_url: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseParams,
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
};
|
||||
};
|
||||
|
||||
const createSubscriptionCheckoutSession = onCall(
|
||||
{ region: REGION },
|
||||
async (request) => {
|
||||
try {
|
||||
const uid = request?.auth?.uid;
|
||||
if (!uid) {
|
||||
throw new HttpsError(
|
||||
"unauthenticated",
|
||||
"Connecte-toi pour souscrire un abonnement.",
|
||||
);
|
||||
}
|
||||
|
||||
const rawPriceId = request?.data?.priceId;
|
||||
const priceId = sanitizePriceId(rawPriceId);
|
||||
if (!priceId) {
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
"Un identifiant de prix Stripe est requis.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!ALL_SUBSCRIPTION_PRICE_IDS.includes(priceId)) {
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
`L'identifiant de prix ${priceId} n'est pas pris en charge.`,
|
||||
);
|
||||
}
|
||||
|
||||
const stripe = getStripeClient();
|
||||
const priceMeta = getSubscriptionMetaFromPrice(priceId) || {};
|
||||
|
||||
const { lineItems, summary } = await buildCheckoutLineItems(
|
||||
[
|
||||
{
|
||||
priceID: priceId,
|
||||
quantity: 1,
|
||||
isRenewable: true,
|
||||
},
|
||||
],
|
||||
{ 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: "subscription",
|
||||
customer: customerId,
|
||||
line_items: lineItems,
|
||||
allow_promotion_codes: true,
|
||||
metadata: {
|
||||
firebaseUID: uid,
|
||||
priceId,
|
||||
purchaseType: "SUBSCRIPTION",
|
||||
subscriptionLevel: priceMeta.level || null,
|
||||
subscriptionBillingPeriod: priceMeta.billingPeriod || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
uiMode,
|
||||
successUrl,
|
||||
cancelUrl,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return formatCheckoutSessionResponse(session);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[subscription-createSubscriptionCheckoutSession] error",
|
||||
error,
|
||||
);
|
||||
if (error instanceof HttpsError) {
|
||||
throw error;
|
||||
}
|
||||
throw mapStripeErrorToHttps(
|
||||
error,
|
||||
"Impossible de créer la session d'abonnement Stripe.",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const createCoinPackCheckoutSession = onCall(
|
||||
{ region: REGION },
|
||||
async (request) => {
|
||||
try {
|
||||
const uid = request?.auth?.uid;
|
||||
if (!uid) {
|
||||
throw new HttpsError(
|
||||
"unauthenticated",
|
||||
"Connecte-toi pour acheter un pack de pièces.",
|
||||
);
|
||||
}
|
||||
|
||||
const rawProductId = request?.data?.productId;
|
||||
const productId =
|
||||
typeof rawProductId === "string" ? rawProductId.trim() : "";
|
||||
|
||||
if (!productId) {
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
"Un identifiant de produit Stripe est requis.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!COIN_PACK_PRODUCT_IDS.includes(productId)) {
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
`Le produit ${productId} n'est pas un pack de pièces autorisé`,
|
||||
);
|
||||
}
|
||||
|
||||
const stripe = getStripeClient();
|
||||
|
||||
const product = await stripe.products.retrieve(productId, {
|
||||
expand: ["default_price"],
|
||||
});
|
||||
|
||||
let resolvedPrice = null;
|
||||
if (typeof product?.default_price === "string") {
|
||||
resolvedPrice = await stripe.prices.retrieve(product.default_price);
|
||||
} else if (
|
||||
product?.default_price &&
|
||||
typeof product.default_price === "object"
|
||||
) {
|
||||
resolvedPrice = product.default_price;
|
||||
}
|
||||
|
||||
let coinPack = null;
|
||||
try {
|
||||
coinPack = formatCoinPack({
|
||||
product,
|
||||
price: resolvedPrice,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[subscription-createCoinPackCheckoutSession] invalid coin pack metadata",
|
||||
productId,
|
||||
error?.message || error,
|
||||
);
|
||||
throw new HttpsError(
|
||||
"failed-precondition",
|
||||
"Le pack Stripe est mal configuré (metadata.coins manquant).",
|
||||
);
|
||||
}
|
||||
|
||||
if (!coinPack?.priceId) {
|
||||
throw new HttpsError(
|
||||
"failed-precondition",
|
||||
"Impossible de déterminer le prix Stripe pour ce pack.",
|
||||
);
|
||||
}
|
||||
|
||||
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: [
|
||||
{
|
||||
price: coinPack.priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
allow_promotion_codes: false,
|
||||
metadata: {
|
||||
firebaseUID: uid,
|
||||
purchaseType: "COIN_PACK",
|
||||
coinPackProductId: coinPack.productId,
|
||||
coinPackPriceId: coinPack.priceId,
|
||||
coinAmount: coinPack.coinAmount,
|
||||
coinPackKey: COIN_PACK_PRODUCT_MAP[productId]?.key || null,
|
||||
},
|
||||
},
|
||||
{
|
||||
uiMode,
|
||||
successUrl,
|
||||
cancelUrl,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return formatCheckoutSessionResponse(session);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[subscription-createCoinPackCheckoutSession] error",
|
||||
error,
|
||||
);
|
||||
if (error instanceof HttpsError) {
|
||||
throw error;
|
||||
}
|
||||
throw mapStripeErrorToHttps(
|
||||
error,
|
||||
"Impossible de créer la session d'achat de pièces.",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
createSubscriptionCheckoutSession,
|
||||
createCoinPackCheckoutSession,
|
||||
resolveCheckoutUiMode,
|
||||
};
|
||||
Reference in New Issue
Block a user