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

@@ -18,6 +18,7 @@ EMAIL_PASSWORD=password
# STRIPE # STRIPE
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51K9cWKI1i3VnbZTq2HGstY2S8wt3peF1MOqPXFO4LR8ln2QgS7GxL8XyKaKLvn7iFHeqAnvdDw0o48qN7rrwwcHU00jOtKhjsf NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51K9cWKI1i3VnbZTq2HGstY2S8wt3peF1MOqPXFO4LR8ln2QgS7GxL8XyKaKLvn7iFHeqAnvdDw0o48qN7rrwwcHU00jOtKhjsf
STRIPE_UI_MODE=embedded_page # TESTS ONLY SUPPORT THIS MODE, KEEP AS IS
CONTACT_EMAIL=test@makerkit.dev CONTACT_EMAIL=test@makerkit.dev

View File

@@ -1,4 +1,6 @@
import 'server-only'; import 'server-only';
import { redirect } from 'next/navigation';
import { SupabaseClient } from '@supabase/supabase-js'; import { SupabaseClient } from '@supabase/supabase-js';
import * as z from 'zod'; import * as z from 'zod';
@@ -81,9 +83,12 @@ class UserBillingService {
`User requested a personal account checkout session. Contacting provider...`, `User requested a personal account checkout session. Contacting provider...`,
); );
let checkoutToken: string | null | undefined;
let url: string | null | undefined;
try { try {
// call the payment gateway to create the checkout session // call the payment gateway to create the checkout session
const { checkoutToken } = await service.createCheckoutSession({ const checkout = await service.createCheckoutSession({
returnUrl, returnUrl,
accountId, accountId,
customerEmail: user.email, customerEmail: user.email,
@@ -93,6 +98,45 @@ class UserBillingService {
enableDiscountField: product.enableDiscountField, enableDiscountField: product.enableDiscountField,
}); });
checkoutToken = checkout.checkoutToken;
url = checkout.url;
} catch (error) {
const message = Error.isError(error) ? error.message : error;
logger.error(
{
name: `billing.personal-account`,
planId,
customerId,
accountId,
error: message
},
`Checkout session not created due to an error`,
);
throw new Error(`Failed to create a checkout session`, { cause: error });
}
if (!url && !checkoutToken) {
throw new Error(
'Checkout session returned neither a URL nor a checkout token',
);
}
// if URL provided, we redirect to the provider's hosted page
if (url) {
logger.info(
{
userId: user.id,
},
`Checkout session created. Redirecting to hosted page...`,
);
redirect(url);
}
// return the checkout token to the client
// so we can call the payment gateway to complete the checkout
logger.info( logger.info(
{ {
userId: user.id, userId: user.id,
@@ -100,25 +144,9 @@ class UserBillingService {
`Checkout session created. Returning checkout token to client...`, `Checkout session created. Returning checkout token to client...`,
); );
// return the checkout token to the client
// so we can call the payment gateway to complete the checkout
return { return {
checkoutToken, checkoutToken,
}; };
} catch (error) {
logger.error(
{
name: `billing.personal-account`,
planId,
customerId,
accountId,
error,
},
`Checkout session not created due to an error`,
);
throw new Error(`Failed to create a checkout session`, { cause: error });
}
} }
/** /**

View File

@@ -1,4 +1,6 @@
import 'server-only'; import 'server-only';
import { redirect } from 'next/navigation';
import { SupabaseClient } from '@supabase/supabase-js'; import { SupabaseClient } from '@supabase/supabase-js';
import * as z from 'zod'; import * as z from 'zod';
@@ -106,9 +108,12 @@ class TeamBillingService {
`Creating checkout session...`, `Creating checkout session...`,
); );
let checkoutToken: string | null = null;
let url: string | null | undefined;
try { try {
// call the payment gateway to create the checkout session // call the payment gateway to create the checkout session
const { checkoutToken } = await service.createCheckoutSession({ const checkout = await service.createCheckoutSession({
accountId, accountId,
plan, plan,
returnUrl, returnUrl,
@@ -118,22 +123,37 @@ class TeamBillingService {
enableDiscountField: product.enableDiscountField, enableDiscountField: product.enableDiscountField,
}); });
// return the checkout token to the client checkoutToken = checkout.checkoutToken;
// so we can call the payment gateway to complete the checkout url = checkout.url;
return {
checkoutToken,
};
} catch (error) { } catch (error) {
const message = Error.isError(error) ? error.message : error;
logger.error( logger.error(
{ {
...ctx, ...ctx,
error, error: message
}, },
`Error creating the checkout session`, `Error creating the checkout session`,
); );
throw new Error(`Checkout not created`, { cause: error }); throw new Error(`Checkout not created`, { cause: error });
} }
// if URL provided, we redirect to the provider's hosted page
if (url) {
logger.info(
ctx,
`Checkout session created. Redirecting to hosted page...`,
);
redirect(url);
}
// return the checkout token to the client
// so we can call the payment gateway to complete the checkout
return {
checkoutToken,
};
} }
/** /**

View File

@@ -31,6 +31,7 @@ NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
| `STRIPE_SECRET_KEY` | Server-side API key | Dashboard → Developers → API keys | | `STRIPE_SECRET_KEY` | Server-side API key | Dashboard → Developers → API keys |
| `STRIPE_WEBHOOK_SECRET` | Webhook signature verification | Generated by Stripe CLI or Dashboard | | `STRIPE_WEBHOOK_SECRET` | Webhook signature verification | Generated by Stripe CLI or Dashboard |
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Client-side key (safe to expose) | Dashboard → Developers → API keys | | `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Client-side key (safe to expose) | Dashboard → Developers → API keys |
| `STRIPE_UI_MODE` | Checkout UI mode: `embedded_page` (default) or `hosted_page` (optional) | - |
{% alert type="error" title="Never commit secret keys" %} {% alert type="error" title="Never commit secret keys" %}
Add `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` to `.env.local` only. Never add them to `.env` or commit them to your repository. Add `STRIPE_SECRET_KEY` and `STRIPE_WEBHOOK_SECRET` to `.env.local` only. Never add them to `.env` or commit them to your repository.
@@ -187,6 +188,21 @@ When deploying to production, configure webhooks in the Stripe Dashboard:
Webhook URLs must be publicly accessible. Vercel preview deployments with authentication enabled won't work. Test by visiting the URL in an incognito browser window. Webhook URLs must be publicly accessible. Vercel preview deployments with authentication enabled won't work. Test by visiting the URL in an incognito browser window.
{% /alert %} {% /alert %}
## Checkout UI Mode
Stripe supports two checkout UI modes:
- **`embedded_page`** (default): Embeds the checkout form directly in your application as a dialog popup. Requires `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`.
- **`hosted_page`**: Redirects users to a Stripe-hosted checkout page. The publishable key is not required in this mode.
Configure this with the `STRIPE_UI_MODE` environment variable:
```bash
STRIPE_UI_MODE=hosted_page
```
If not set, it defaults to `embedded_page`.
## Free Trials Without Credit Card ## Free Trials Without Credit Card
Allow users to start a trial without entering payment information: Allow users to start a trial without entering payment information:

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-supabase-saas-kit-turbo", "name": "next-supabase-saas-kit-turbo",
"version": "3.0.5", "version": "3.1.0",
"private": true, "private": true,
"author": { "author": {
"name": "MakerKit", "name": "MakerKit",
@@ -48,5 +48,5 @@
"engines": { "engines": {
"node": ">=20.10.0" "node": ">=20.10.0"
}, },
"packageManager": "pnpm@10.32.1" "packageManager": "pnpm@10.33.0"
} }

View File

@@ -33,7 +33,8 @@ export abstract class BillingStrategyProviderService {
abstract createCheckoutSession( abstract createCheckoutSession(
params: z.output<typeof CreateBillingCheckoutSchema>, params: z.output<typeof CreateBillingCheckoutSchema>,
): Promise<{ ): Promise<{
checkoutToken: string; checkoutToken: string | null;
url?: string | null;
}>; }>;
abstract cancelSubscription( abstract cancelSubscription(

View File

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

View File

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

View File

@@ -1,13 +1,20 @@
import type { Stripe } from 'stripe'; import type { Stripe } from "stripe";
import * as z from 'zod'; 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 * @description If set to true, users can start a trial without entering their credit card details
*/ */
const enableTrialWithoutCreditCard = 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 * @name createStripeCheckout
@@ -30,9 +37,9 @@ export async function createStripeCheckout(
// docs: https://stripe.com/docs/billing/subscriptions/build-subscription // docs: https://stripe.com/docs/billing/subscriptions/build-subscription
const mode: Stripe.Checkout.SessionCreateParams.Mode = 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; let trialDays: number | null | undefined = params.plan.trialDays;
@@ -46,7 +53,7 @@ export async function createStripeCheckout(
? { ? {
trial_settings: { trial_settings: {
end_behavior: { 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({ const urls = getUrls({
returnUrl: params.returnUrl, returnUrl: params.returnUrl,
uiMode,
}); });
// we use the embedded mode, so the user does not leave the page
const uiMode = 'embedded';
const customerData = customer const customerData = customer
? { ? {
customer, customer,
@@ -84,10 +89,10 @@ export async function createStripeCheckout(
const customerCreation = const customerCreation =
isSubscription || customer isSubscription || customer
? ({} as Record<string, string>) ? ({} as Record<string, string>)
: { customer_creation: 'always' }; : { customer_creation: "always" };
const lineItems = params.plan.lineItems.map((item) => { const lineItems = params.plan.lineItems.map((item) => {
if (item.type === 'metered') { if (item.type === "metered") {
return { return {
price: item.id, price: item.id,
}; };
@@ -109,7 +114,7 @@ export async function createStripeCheckout(
const paymentCollectionMethod = const paymentCollectionMethod =
enableTrialWithoutCreditCard && params.plan.trialDays 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 }) { function getUrls(params: {
const returnUrl = `${params.returnUrl}?session_id={CHECKOUT_SESSION_ID}`; 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 {
return_url: returnUrl, return_url: url,
}; };
} }

View File

@@ -47,9 +47,9 @@ export class StripeBillingStrategyService implements BillingStrategyProviderServ
logger.info(ctx, 'Creating checkout session...'); 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'); logger.error(ctx, 'Failed to create checkout session');
throw new Error('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'); 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 'server-only';
import { StripeServerEnvSchema } from '../schema/stripe-server-env.schema'; 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 * @description returns a Stripe instance

View File

@@ -625,7 +625,8 @@ export const envVariables: EnvVariableModel[] = [
{ {
name: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY', name: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
displayName: 'Stripe Publishable Key', displayName: 'Stripe Publishable Key',
description: 'Your Stripe publishable key.', description:
'Your Stripe publishable key. Required when using embedded checkout (default), optional when STRIPE_UI_MODE is set to hosted_page.',
hint: `Ex. pk_test_123456789012345678901234`, hint: `Ex. pk_test_123456789012345678901234`,
category: 'Billing', category: 'Billing',
type: 'string', type: 'string',
@@ -635,7 +636,13 @@ export const envVariables: EnvVariableModel[] = [
variable: 'NEXT_PUBLIC_BILLING_PROVIDER', variable: 'NEXT_PUBLIC_BILLING_PROVIDER',
condition: (value) => value === 'stripe', condition: (value) => value === 'stripe',
message: message:
'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is required when NEXT_PUBLIC_BILLING_PROVIDER is set to "stripe"', 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is required when NEXT_PUBLIC_BILLING_PROVIDER is set to "stripe" and STRIPE_UI_MODE is not "hosted_page"',
},
{
variable: 'STRIPE_UI_MODE',
condition: (value) => value !== 'hosted_page',
message:
'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is required when STRIPE_UI_MODE is not set to "hosted_page"',
}, },
], ],
validate: ({ value }) => { validate: ({ value }) => {
@@ -1391,6 +1398,21 @@ export const envVariables: EnvVariableModel[] = [
return z.coerce.boolean().optional().safeParse(value); return z.coerce.boolean().optional().safeParse(value);
}, },
}, },
{
name: 'STRIPE_UI_MODE',
displayName: 'Stripe Checkout UI Mode',
description:
'Controls whether Stripe Checkout uses an embedded page or a hosted page. Defaults to embedded_page.',
category: 'Billing',
type: 'enum',
values: ['embedded_page', 'hosted_page'],
validate: ({ value }) => {
return z
.enum(['embedded_page', 'hosted_page'])
.optional()
.safeParse(value);
},
},
{ {
name: 'NEXT_PUBLIC_THEME_COLOR', name: 'NEXT_PUBLIC_THEME_COLOR',
displayName: 'Theme Color', displayName: 'Theme Color',

View File

@@ -106,40 +106,40 @@
"test:unit": "vitest run" "test:unit": "vitest run"
}, },
"dependencies": { "dependencies": {
"@base-ui/react": "^1.3.0", "@base-ui/react": "catalog:",
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "catalog:",
"@kit/shared": "workspace:*", "@kit/shared": "workspace:*",
"clsx": "^2.1.1", "clsx": "catalog:",
"cmdk": "^1.1.1", "cmdk": "catalog:",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "catalog:",
"input-otp": "^1.4.2", "input-otp": "catalog:",
"lucide-react": "catalog:", "lucide-react": "catalog:",
"react-dropzone": "^15.0.0", "react-dropzone": "catalog:",
"react-resizable-panels": "catalog:", "react-resizable-panels": "catalog:",
"react-top-loading-bar": "^3.0.2", "react-top-loading-bar": "catalog:",
"recharts": "3.7.0", "recharts": "catalog:",
"tailwind-merge": "^3.5.0" "tailwind-merge": "catalog:"
}, },
"devDependencies": { "devDependencies": {
"@kit/i18n": "workspace:*", "@kit/i18n": "workspace:*",
"@kit/tsconfig": "workspace:*", "@kit/tsconfig": "workspace:*",
"@supabase/supabase-js": "catalog:", "@supabase/supabase-js": "catalog:",
"@tanstack/react-query": "catalog:", "@tanstack/react-query": "catalog:",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "catalog:",
"@types/react": "catalog:", "@types/react": "catalog:",
"@types/react-dom": "catalog:", "@types/react-dom": "catalog:",
"class-variance-authority": "^0.7.1", "class-variance-authority": "catalog:",
"date-fns": "^4.1.0", "date-fns": "catalog:",
"next": "catalog:", "next": "catalog:",
"next-intl": "^4.8.3", "next-intl": "catalog:",
"next-safe-action": "^8.1.8", "next-safe-action": "catalog:",
"next-themes": "0.4.6", "next-themes": "catalog:",
"react-day-picker": "^9.14.0", "react-day-picker": "catalog:",
"react-hook-form": "catalog:", "react-hook-form": "catalog:",
"shadcn": "catalog:", "shadcn": "catalog:",
"sonner": "^2.0.7", "sonner": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"vaul": "^1.1.2", "vaul": "catalog:",
"vitest": "catalog:", "vitest": "catalog:",
"zod": "catalog:" "zod": "catalog:"
} }

1289
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,48 +2,52 @@ packages:
- apps/* - apps/*
- packages/** - packages/**
- tooling/* - tooling/*
catalog: catalog:
'@base-ui/react': ^1.3.0
'@faker-js/faker': ^10.4.0 '@faker-js/faker': ^10.4.0
'@hookform/resolvers': ^5.2.2 '@hookform/resolvers': ^5.2.2
'@keystatic/core': 0.5.49 '@keystatic/core': 0.5.50
'@keystatic/next': ^5.0.4 '@keystatic/next': ^5.0.4
'@lemonsqueezy/lemonsqueezy.js': 4.0.0 '@lemonsqueezy/lemonsqueezy.js': 4.0.0
'@makerkit/data-loader-supabase-core': ^0.0.10 '@makerkit/data-loader-supabase-core': ^0.0.10
'@makerkit/data-loader-supabase-nextjs': ^1.2.5 '@makerkit/data-loader-supabase-nextjs': ^1.2.5
'@manypkg/cli': ^0.25.1 '@manypkg/cli': ^0.25.1
'@markdoc/markdoc': ^0.5.6 '@markdoc/markdoc': ^0.5.7
'@marsidev/react-turnstile': ^1.4.2 '@marsidev/react-turnstile': ^1.5.0
'@modelcontextprotocol/sdk': 1.28.0 '@modelcontextprotocol/sdk': 1.28.0
'@next/bundle-analyzer': 16.2.1 '@next/bundle-analyzer': 16.2.1
'@nosecone/next': 1.3.0 '@nosecone/next': 1.3.1
'@playwright/test': ^1.58.2 '@playwright/test': ^1.58.2
'@react-email/components': 1.0.10 '@react-email/components': 1.0.10
'@sentry/nextjs': 10.46.0 '@sentry/nextjs': 10.46.0
'@stripe/react-stripe-js': 5.6.1 '@stripe/react-stripe-js': 6.1.0
'@stripe/stripe-js': 8.11.0 '@stripe/stripe-js': 9.0.1
'@supabase/ssr': ^0.9.0 '@supabase/ssr': ^0.10.0
'@supabase/supabase-js': 2.100.0 '@supabase/supabase-js': 2.101.0
'@tailwindcss/postcss': ^4.2.2 '@tailwindcss/postcss': ^4.2.2
'@tanstack/react-query': 5.95.2 '@tanstack/react-query': 5.95.2
'@tanstack/react-table': ^8.21.3 '@tanstack/react-table': ^8.21.3
'@turbo/gen': ^2.8.20 '@turbo/gen': ^2.9.1
'@types/node': 25.5.0 '@types/node': 25.5.0
'@types/nodemailer': 7.0.11 '@types/nodemailer': 7.0.11
'@types/react': 19.2.14 '@types/react': 19.2.14
'@types/react-dom': 19.2.3 '@types/react-dom': 19.2.3
babel-plugin-react-compiler: 1.0.0 babel-plugin-react-compiler: 1.0.0
class-variance-authority: ^0.7.1 class-variance-authority: ^0.7.1
clsx: ^2.1.1
cmdk: ^1.1.1
cross-env: ^10.0.0 cross-env: ^10.0.0
cssnano: ^7.1.3 cssnano: ^7.1.4
date-fns: ^4.1.0 date-fns: ^4.1.0
dotenv: 17.3.1 dotenv: 17.3.1
embla-carousel-react: ^8.6.0
input-otp: ^1.4.2
lucide-react: 1.7.0 lucide-react: 1.7.0
nanoid: ^5.1.7 nanoid: ^5.1.7
next: 16.2.1 next: 16.2.1
next-intl: ^4.8.3 next-intl: ^4.8.3
next-runtime-env: 3.3.0 next-runtime-env: 3.3.0
next-safe-action: ^8.1.8 next-safe-action: ^8.3.0
next-sitemap: ^4.2.3 next-sitemap: ^4.2.3
next-themes: 0.4.6 next-themes: 0.4.6
node-html-parser: ^7.1.0 node-html-parser: ^7.1.0
@@ -54,32 +58,33 @@ catalog:
pino-pretty: 13.0.0 pino-pretty: 13.0.0
postgres: 3.4.8 postgres: 3.4.8
react: 19.2.4 react: 19.2.4
react-day-picker: ^9.14.0
react-dom: 19.2.4 react-dom: 19.2.4
react-dropzone: ^15.0.0
react-hook-form: 7.72.0 react-hook-form: 7.72.0
react-resizable-panels: ^4.7.6 react-resizable-panels: ^4.8.0
react-top-loading-bar: ^3.0.2
recharts: 3.7.0 recharts: 3.7.0
rxjs: ^7.8.2 rxjs: ^7.8.2
server-only: ^0.0.1 server-only: ^0.0.1
shadcn: 4.1.0 shadcn: 4.1.1
sonner: ^2.0.7 sonner: ^2.0.7
stripe: 20.4.1 stripe: 21.0.1
supabase: 2.84.4 supabase: 2.84.5
tailwind-merge: ^3.5.0 tailwind-merge: ^3.5.0
tailwindcss: 4.2.2 tailwindcss: 4.2.2
totp-generator: ^2.0.1 totp-generator: ^2.0.1
tsup: 8.5.1 tsup: 8.5.1
turbo: 2.8.20 turbo: 2.9.1
tw-animate-css: 1.4.0 tw-animate-css: 1.4.0
typescript: ^6.0.2 typescript: ^6.0.2
urlpattern-polyfill: ^10.1.0 urlpattern-polyfill: ^10.1.0
vitest: ^4.1.1 vaul: ^1.1.2
vitest: ^4.1.2
wp-types: ^4.69.0 wp-types: ^4.69.0
zod: 4.3.6 zod: 4.3.6
catalogMode: prefer catalogMode: prefer
cleanupUnusedCatalogs: true cleanupUnusedCatalogs: true
onlyBuiltDependencies: onlyBuiltDependencies:
- '@tailwindcss/oxide' - '@tailwindcss/oxide'
- '@sentry/cli' - '@sentry/cli'