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]); -}