Updated dependencies, Added Hosted mode for Stripe checkout

* chore: bump version to 3.1.0 and update dependencies in package.json, pnpm-lock.yaml, and pnpm-workspace.yaml; enhance billing services with support for hosted checkout page in Stripe integration

* Enhance error handling in billing services to log error messages instead of objects; update documentation for Stripe integration to clarify publishable key requirements based on UI mode.
This commit is contained in:
Giancarlo Buomprisco
2026-03-31 12:44:30 +08:00
committed by GitHub
parent 9d7c7f8030
commit 6268d1bab0
15 changed files with 1018 additions and 566 deletions

View File

@@ -16,7 +16,7 @@ const { publishableKey } = StripeClientEnvSchema.parse({
publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
});
const stripePromise = loadStripe(publishableKey);
const stripePromise = loadStripe(publishableKey as string);
export function StripeCheckout({
checkoutToken,

View File

@@ -1,11 +1,19 @@
import * as z from 'zod';
const isHostedMode = process.env.STRIPE_UI_MODE === 'hosted_page';
export const StripeClientEnvSchema = z
.object({
publishableKey: z.string().min(1),
publishableKey: isHostedMode
? z.string().optional()
: z.string().min(1),
})
.refine(
(schema) => {
if (isHostedMode || !schema.publishableKey) {
return true;
}
return schema.publishableKey.startsWith('pk_');
},
{

View File

@@ -1,13 +1,20 @@
import type { Stripe } from 'stripe';
import * as z from 'zod';
import type { Stripe } from "stripe";
import * as z from "zod";
import type { CreateBillingCheckoutSchema } from '@kit/billing/schema';
import type { CreateBillingCheckoutSchema } from "@kit/billing/schema";
/**
* @description If set to true, users can start a trial without entering their credit card details
*/
const enableTrialWithoutCreditCard =
process.env.STRIPE_ENABLE_TRIAL_WITHOUT_CC === 'true';
process.env.STRIPE_ENABLE_TRIAL_WITHOUT_CC === "true";
const UI_MODE_VALUES = ["embedded_page", "hosted_page"] as const;
const uiMode = z
.enum(UI_MODE_VALUES)
.default("embedded_page")
.parse(process.env.STRIPE_UI_MODE);
/**
* @name createStripeCheckout
@@ -30,9 +37,9 @@ export async function createStripeCheckout(
// docs: https://stripe.com/docs/billing/subscriptions/build-subscription
const mode: Stripe.Checkout.SessionCreateParams.Mode =
params.plan.paymentType === 'recurring' ? 'subscription' : 'payment';
params.plan.paymentType === "recurring" ? "subscription" : "payment";
const isSubscription = mode === 'subscription';
const isSubscription = mode === "subscription";
let trialDays: number | null | undefined = params.plan.trialDays;
@@ -46,7 +53,7 @@ export async function createStripeCheckout(
? {
trial_settings: {
end_behavior: {
missing_payment_method: 'cancel' as const,
missing_payment_method: "cancel" as const,
},
},
}
@@ -68,11 +75,9 @@ export async function createStripeCheckout(
const urls = getUrls({
returnUrl: params.returnUrl,
uiMode,
});
// we use the embedded mode, so the user does not leave the page
const uiMode = 'embedded';
const customerData = customer
? {
customer,
@@ -84,10 +89,10 @@ export async function createStripeCheckout(
const customerCreation =
isSubscription || customer
? ({} as Record<string, string>)
: { customer_creation: 'always' };
: { customer_creation: "always" };
const lineItems = params.plan.lineItems.map((item) => {
if (item.type === 'metered') {
if (item.type === "metered") {
return {
price: item.id,
};
@@ -109,7 +114,7 @@ export async function createStripeCheckout(
const paymentCollectionMethod =
enableTrialWithoutCreditCard && params.plan.trialDays
? {
payment_method_collection: 'if_required' as const,
payment_method_collection: "if_required" as const,
}
: {};
@@ -127,10 +132,20 @@ export async function createStripeCheckout(
});
}
function getUrls(params: { returnUrl: string }) {
const returnUrl = `${params.returnUrl}?session_id={CHECKOUT_SESSION_ID}`;
function getUrls(params: {
returnUrl: string;
uiMode: (typeof UI_MODE_VALUES)[number];
}) {
const url = `${params.returnUrl}?session_id={CHECKOUT_SESSION_ID}`;
if (params.uiMode === "hosted_page") {
return {
success_url: url,
cancel_url: params.returnUrl,
};
}
return {
return_url: returnUrl,
return_url: url,
};
}

View File

@@ -47,9 +47,9 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
logger.info(ctx, 'Creating checkout session...');
const { client_secret } = await createStripeCheckout(stripe, params);
const { client_secret, url } = await createStripeCheckout(stripe, params);
if (!client_secret) {
if (!client_secret && !url) {
logger.error(ctx, 'Failed to create checkout session');
throw new Error('Failed to create checkout session');
@@ -57,7 +57,10 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
logger.info(ctx, 'Checkout session created successfully');
return { checkoutToken: client_secret };
return {
checkoutToken: client_secret ?? null,
url,
};
}
/**

View File

@@ -1,7 +1,7 @@
import 'server-only';
import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema';
const STRIPE_API_VERSION = '2026-02-25.clover';
const STRIPE_API_VERSION = '2026-03-25.dahlia';
/**
* @description returns a Stripe instance