Update billing page data handling and version bump to 2.12.0 (#300)

* Update billing page data handling and version bump to 2.12.0

- Refactored billing page components to streamline data loading for subscriptions and orders.
- Introduced `getProductPlan` function to encapsulate product plan resolution logic.
- Updated `package.json` version from 2.11.0 to 2.12.0.
This commit is contained in:
Giancarlo Buomprisco
2025-07-16 16:11:55 +07:00
committed by GitHub
parent abcf1ae3d7
commit da8a3a903d
6 changed files with 116 additions and 125 deletions

View File

@@ -2,26 +2,9 @@ import 'server-only';
import { cache } from 'react'; import { cache } from 'react';
import { z } from 'zod';
import { createAccountsApi } from '@kit/accounts/api'; import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
/**
* The variable BILLING_MODE represents the billing mode for a service. It can
* have either the value 'subscription' or 'one-time'. If not provided, the default
* value is 'subscription'. The value can be overridden by the environment variable
* BILLING_MODE.
*
* If the value is 'subscription', we fetch the subscription data for the user.
* If the value is 'one-time', we fetch the orders data for the user.
* if none of these suits your needs, please override the below function.
*/
const BILLING_MODE = z
.enum(['subscription', 'one-time'])
.default('subscription')
.parse(process.env.BILLING_MODE);
/** /**
* Load the personal account billing page data for the given user. * Load the personal account billing page data for the given user.
* @param userId * @param userId
@@ -36,12 +19,9 @@ function personalAccountBillingPageDataLoader(userId: string) {
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createAccountsApi(client); const api = createAccountsApi(client);
const data = const subscription = api.getSubscription(userId);
BILLING_MODE === 'subscription' const order = api.getOrder(userId);
? api.getSubscription(userId)
: api.getOrder(userId);
const customerId = api.getCustomerId(userId); const customerId = api.getCustomerId(userId);
return Promise.all([data, customerId]); return Promise.all([subscription, order, customerId]);
} }

View File

@@ -35,24 +35,21 @@ export const generateMetadata = async () => {
async function PersonalAccountBillingPage() { async function PersonalAccountBillingPage() {
const user = await requireUserInServerComponent(); const user = await requireUserInServerComponent();
const [data, customerId] = await loadPersonalAccountBillingPageData(user.id); const [subscription, order, customerId] =
await loadPersonalAccountBillingPageData(user.id);
let productPlan: { const subscriptionProductPlan = subscription
product: ProductSchema; ? await getProductPlan(
plan: z.infer<typeof PlanSchema>; subscription.items[0]?.variant_id,
} | null = null; subscription.currency,
)
: undefined;
if (data) { const orderProductPlan = order
const firstLineItem = data.items[0]; ? await getProductPlan(order.items[0]?.variant_id, order.currency)
: undefined;
if (firstLineItem) { const hasBillingData = subscription || order;
productPlan = await resolveProductPlan(
billingConfig,
firstLineItem.variant_id,
data.currency,
);
}
}
return ( return (
<> <>
@@ -63,7 +60,7 @@ async function PersonalAccountBillingPage() {
<PageBody> <PageBody>
<div className={'flex flex-col space-y-4'}> <div className={'flex flex-col space-y-4'}>
<If condition={!data}> <If condition={!hasBillingData}>
<PersonalAccountCheckoutForm customerId={customerId} /> <PersonalAccountCheckoutForm customerId={customerId} />
<If condition={customerId}> <If condition={customerId}>
@@ -71,32 +68,36 @@ async function PersonalAccountBillingPage() {
</If> </If>
</If> </If>
<If condition={data}> <If condition={hasBillingData}>
{(data) => ( <div className={'flex w-full max-w-2xl flex-col space-y-6'}>
<div className={'flex w-full max-w-2xl flex-col space-y-6'}> <If condition={subscription}>
{'active' in data ? ( {(subscription) => {
<CurrentSubscriptionCard return (
subscription={data} <CurrentSubscriptionCard
product={productPlan!.product} subscription={subscription}
plan={productPlan!.plan} product={subscriptionProductPlan!.product}
/> plan={subscriptionProductPlan!.plan}
) : ( />
<CurrentLifetimeOrderCard );
order={data} }}
product={productPlan!.product} </If>
plan={productPlan!.plan}
/>
)}
<If condition={!data}> <If condition={order}>
<PersonalAccountCheckoutForm customerId={customerId} /> {(order) => {
</If> return (
<CurrentLifetimeOrderCard
order={order}
product={orderProductPlan!.product}
plan={orderProductPlan!.plan}
/>
);
}}
</If>
</div>
</If>
<If condition={customerId}> <If condition={customerId}>
<CustomerBillingPortalForm /> <CustomerBillingPortalForm />
</If>
</div>
)}
</If> </If>
</div> </div>
</PageBody> </PageBody>
@@ -113,3 +114,20 @@ function CustomerBillingPortalForm() {
</form> </form>
); );
} }
async function getProductPlan(
variantId: string | undefined,
currency: string,
): Promise<
| {
product: ProductSchema;
plan: z.infer<typeof PlanSchema>;
}
| undefined
> {
if (!variantId) {
return undefined;
}
return resolveProductPlan(billingConfig, variantId, currency);
}

View File

@@ -815,7 +815,7 @@ export function PageViewsChart() {
desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0), desktop: chartData.reduce((acc, curr) => acc + curr.desktop, 0),
mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0), mobile: chartData.reduce((acc, curr) => acc + curr.mobile, 0),
}), }),
[], [chartData],
); );
return ( return (

View File

@@ -2,26 +2,9 @@ import 'server-only';
import { cache } from 'react'; import { cache } from 'react';
import { z } from 'zod';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { createTeamAccountsApi } from '@kit/team-accounts/api'; import { createTeamAccountsApi } from '@kit/team-accounts/api';
/**
* The variable BILLING_MODE represents the billing mode for a service. It can
* have either the value 'subscription' or 'one-time'. If not provided, the default
* value is 'subscription'. The value can be overridden by the environment variable
* BILLING_MODE.
*
* If the value is 'subscription', we fetch the subscription data for the user.
* If the value is 'one-time', we fetch the orders data for the user.
* if none of these suits your needs, please override the below function.
*/
const BILLING_MODE = z
.enum(['subscription', 'one-time'])
.default('subscription')
.parse(process.env.BILLING_MODE);
/** /**
* @name loadTeamAccountBillingPage * @name loadTeamAccountBillingPage
* @description Load the team account billing page data for the given account. * @description Load the team account billing page data for the given account.
@@ -32,12 +15,9 @@ function teamAccountBillingPageLoader(accountId: string) {
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createTeamAccountsApi(client); const api = createTeamAccountsApi(client);
const data = const subscription = api.getSubscription(accountId);
BILLING_MODE === 'subscription' const order = api.getOrder(accountId);
? api.getSubscription(accountId)
: api.getOrder(accountId);
const customerId = api.getCustomerId(accountId); const customerId = api.getCustomerId(accountId);
return Promise.all([data, customerId]); return Promise.all([subscription, order, customerId]);
} }

View File

@@ -44,24 +44,21 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
const workspace = await loadTeamWorkspace(account); const workspace = await loadTeamWorkspace(account);
const accountId = workspace.account.id; const accountId = workspace.account.id;
const [data, customerId] = await loadTeamAccountBillingPage(accountId); const [subscription, order, customerId] =
await loadTeamAccountBillingPage(accountId);
let productPlan: { const subscriptionProductPlan = subscription
product: ProductSchema; ? await getProductPlan(
plan: z.infer<typeof PlanSchema>; subscription.items[0]?.variant_id,
} | null = null; subscription.currency,
)
: undefined;
if (data) { const orderProductPlan = order
const firstLineItem = data.items[0]; ? await getProductPlan(order.items[0]?.variant_id, order.currency)
: undefined;
if (firstLineItem) { const hasBillingData = subscription || order;
productPlan = await resolveProductPlan(
billingConfig,
firstLineItem.variant_id,
data.currency,
);
}
}
const canManageBilling = const canManageBilling =
workspace.account.permissions.includes('billing.manage'); workspace.account.permissions.includes('billing.manage');
@@ -102,33 +99,32 @@ async function TeamAccountBillingPage({ params }: TeamAccountBillingPageProps) {
<PageBody> <PageBody>
<div <div
className={cn(`flex w-full flex-col space-y-4`, { className={cn(`flex w-full flex-col space-y-4`, {
'max-w-2xl': data, 'max-w-2xl': hasBillingData,
})} })}
> >
<If <If condition={!hasBillingData}>
condition={data} <Checkout />
fallback={ </If>
<div>
<Checkout />
</div>
}
>
{(data) => {
if ('active' in data) {
return (
<CurrentSubscriptionCard
subscription={data}
product={productPlan!.product}
plan={productPlan!.plan}
/>
);
}
<If condition={subscription}>
{(subscription) => {
return (
<CurrentSubscriptionCard
subscription={subscription}
product={subscriptionProductPlan!.product}
plan={subscriptionProductPlan!.plan}
/>
);
}}
</If>
<If condition={order}>
{(order) => {
return ( return (
<CurrentLifetimeOrderCard <CurrentLifetimeOrderCard
order={data} order={order}
product={productPlan!.product} product={orderProductPlan!.product}
plan={productPlan!.plan} plan={orderProductPlan!.plan}
/> />
); );
}} }}
@@ -158,3 +154,20 @@ function CannotManageBillingAlert() {
</Alert> </Alert>
); );
} }
async function getProductPlan(
variantId: string | undefined,
currency: string,
): Promise<
| {
product: ProductSchema;
plan: z.infer<typeof PlanSchema>;
}
| undefined
> {
if (!variantId) {
return undefined;
}
return resolveProductPlan(billingConfig, variantId, currency);
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-supabase-saas-kit-turbo", "name": "next-supabase-saas-kit-turbo",
"version": "2.11.0", "version": "2.12.0",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"engines": { "engines": {