From 9303e8f5dd381f97c27822b36bb5d6a80cb73103 Mon Sep 17 00:00:00 2001 From: giancarlo Date: Sat, 6 Apr 2024 14:09:17 +0800 Subject: [PATCH] Add loaders for team and personal account billing pages A new loader for team and personal account billing pages has been created to fetch subscription and customer data. This change offloads the task of loading account data from the account billing pages to the newly created loaders, thus simplifying the code in the billing pages. Furthermore, caching allows the same requested to be deduped across server components --- .../personal-account-billing-page.loader.ts | 25 +++++++++++ .../(dashboard)/home/(user)/billing/page.tsx | 44 ++++++------------- .../team-account-billing-page.loader.ts | 26 +++++++++++ .../_lib/server/team-billing.service.ts | 1 + .../home/[account]/billing/page.tsx | 25 ++--------- 5 files changed, 69 insertions(+), 52 deletions(-) create mode 100644 apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts create mode 100644 apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts diff --git a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts new file mode 100644 index 000000000..5604544a5 --- /dev/null +++ b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts @@ -0,0 +1,25 @@ +import { cache } from 'react'; + +import 'server-only'; + +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; + +export const loadPersonalAccountBillingPageData = cache((userId: string) => { + const client = getSupabaseServerComponentClient(); + + const subscription = client + .from('subscriptions') + .select('*, items: subscription_items !inner (*)') + .eq('account_id', userId) + .maybeSingle() + .then(({ data }) => data); + + const customer = client + .from('billing_customers') + .select('customer_id') + .eq('account_id', userId) + .maybeSingle() + .then(({ data }) => data?.customer_id); + + return Promise.all([subscription, customer]); +}); diff --git a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx index d7770f364..abe4b6c09 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/(user)/billing/page.tsx @@ -1,22 +1,24 @@ -import { SupabaseClient } from '@supabase/supabase-js'; +import { redirect } from 'next/navigation'; import { BillingPortalCard, CurrentSubscriptionCard, } from '@kit/billing-gateway/components'; -import { Database } from '@kit/supabase/database'; +import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { If } from '@kit/ui/if'; import { PageBody } from '@kit/ui/page'; import { Trans } from '@kit/ui/trans'; import { UserAccountHeader } from '~/(dashboard)/home/(user)/_components/user-account-header'; -import { createPersonalAccountBillingPortalSession } from '~/(dashboard)/home/(user)/billing/server-actions'; import billingConfig from '~/config/billing.config'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; +import { createPersonalAccountBillingPortalSession } from '../billing/server-actions'; import { PersonalAccountCheckoutForm } from './_components/personal-account-checkout-form'; +// user billing imports +import { loadPersonalAccountBillingPageData } from './_lib/server/personal-account-billing-page.loader'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -29,7 +31,15 @@ export const generateMetadata = async () => { async function PersonalAccountBillingPage() { const client = getSupabaseServerComponentClient(); - const [subscription, customerId] = await loadData(client); + const auth = await requireUser(client); + + if (auth.error) { + redirect(auth.redirectTo); + } + + const [subscription, customerId] = await loadPersonalAccountBillingPageData( + auth.data.id, + ); return ( <> @@ -79,29 +89,3 @@ function CustomerBillingPortalForm() { ); } - -async function loadData(client: SupabaseClient) { - const { data, error } = await client.auth.getUser(); - - if (error ?? !data?.user) { - throw new Error('Authentication required'); - } - - const user = data.user; - - const subscription = client - .from('subscriptions') - .select('*, items: subscription_items !inner (*)') - .eq('account_id', user.id) - .maybeSingle() - .then(({ data }) => data); - - const customer = client - .from('billing_customers') - .select('customer_id') - .eq('account_id', user.id) - .maybeSingle() - .then(({ data }) => data?.customer_id); - - return Promise.all([subscription, customer]); -} diff --git a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts new file mode 100644 index 000000000..850d7fe8a --- /dev/null +++ b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts @@ -0,0 +1,26 @@ +import { cache } from 'react'; + +import 'server-only'; + +import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; + +export const loadTeamAccountBillingPage = cache((accountId: string) => { + const client = getSupabaseServerComponentClient(); + + // TODO: improve these queries to only load the necessary data + const subscription = client + .from('subscriptions') + .select('*, items: subscription_items !inner (*)') + .eq('account_id', accountId) + .maybeSingle() + .then(({ data }) => data); + + const customerId = client + .from('billing_customers') + .select('customer_id') + .eq('account_id', accountId) + .maybeSingle() + .then(({ data }) => data?.customer_id); + + return Promise.all([subscription, customerId]); +}); diff --git a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts index 6d2e80152..a08fe67db 100644 --- a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts +++ b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-billing.service.ts @@ -1,5 +1,6 @@ import { SupabaseClient } from '@supabase/supabase-js'; +import 'server-only'; import { z } from 'zod'; import { LineItemSchema } from '@kit/billing'; diff --git a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx index 7e44a835d..8dd519fd2 100644 --- a/apps/web/app/(dashboard)/home/[account]/billing/page.tsx +++ b/apps/web/app/(dashboard)/home/[account]/billing/page.tsx @@ -4,7 +4,6 @@ import { BillingPortalCard, CurrentSubscriptionCard, } from '@kit/billing-gateway/components'; -import { getSupabaseServerComponentClient } from '@kit/supabase/server-component-client'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; import { If } from '@kit/ui/if'; import { PageBody, PageHeader } from '@kit/ui/page'; @@ -15,6 +14,7 @@ import billingConfig from '~/config/billing.config'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; +import { loadTeamAccountBillingPage } from '../_lib/server/team-account-billing-page.loader'; import { loadTeamWorkspace } from '../_lib/server/team-account-workspace.loader'; import { TeamAccountCheckoutForm } from './_components/team-account-checkout-form'; @@ -36,7 +36,8 @@ export const generateMetadata = async () => { async function TeamAccountBillingPage({ params }: Params) { const workspace = await loadTeamWorkspace(params.account); const accountId = workspace.account.id; - const [subscription, customerId] = await loadAccountData(accountId); + const [subscription, customerId] = + await loadTeamAccountBillingPage(accountId); const canManageBilling = workspace.account.permissions.includes('billing.manage'); @@ -119,23 +120,3 @@ function CannotManageBillingAlert() { ); } - -async function loadAccountData(accountId: string) { - const client = getSupabaseServerComponentClient(); - - const subscription = client - .from('subscriptions') - .select('*, items: subscription_items !inner (*)') - .eq('account_id', accountId) - .maybeSingle() - .then(({ data }) => data); - - const customerId = client - .from('billing_customers') - .select('customer_id') - .eq('account_id', accountId) - .maybeSingle() - .then(({ data }) => data?.customer_id); - - return Promise.all([subscription, customerId]); -}