import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { canExecuteOrganisationAction } from '@documenso/lib/utils/organisations'; import { trpc } from '@documenso/trpc/react'; import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; import { Trans } from '@lingui/react/macro'; import { SubscriptionStatus } from '@prisma/client'; import { Loader } from 'lucide-react'; import { useEffect, useRef } from 'react'; import { useSearchParams } from 'react-router'; import type Stripe from 'stripe'; import { match, P } from 'ts-pattern'; import { BillingPlans } from '~/components/general/billing-plans'; import { OrganisationBillingPortalButton } from '~/components/general/organisations/organisation-billing-portal-button'; import { OrganisationBillingInvoicesTable } from '~/components/tables/organisation-billing-invoices-table'; import { appMetaTags } from '~/utils/meta'; export function meta() { return appMetaTags(msg`Billing`); } export default function TeamsSettingBillingPage() { const { _, i18n } = useLingui(); const organisation = useCurrentOrganisation(); const [searchParams, setSearchParams] = useSearchParams(); const utils = trpc.useUtils(); const { data: subscriptionQuery, isLoading: isLoadingSubscription } = trpc.enterprise.billing.subscription.get.useQuery({ organisationId: organisation.id, }); const { mutateAsync: syncSubscription, isPending: isSyncingSubscription } = trpc.enterprise.billing.subscription.sync.useMutation(); const hasTriggeredCheckoutSyncRef = useRef(false); const isCheckoutSuccess = searchParams.get('success') === 'true'; /** * Eagerly sync the subscription from Stripe when returning from a successful * checkout, since the webhook may not have arrived yet. */ useEffect(() => { if (!isCheckoutSuccess || hasTriggeredCheckoutSyncRef.current) { return; } hasTriggeredCheckoutSyncRef.current = true; void syncSubscription({ organisationId: organisation.id }) .catch(() => { // Non-fatal, webhooks will converge the subscription state shortly. }) .finally(() => { void utils.enterprise.billing.invalidate(); setSearchParams( (params) => { params.delete('success'); return params; }, { replace: true }, ); }); }, [isCheckoutSuccess, organisation.id]); if (isLoadingSubscription || !subscriptionQuery || isSyncingSubscription) { return (
); } const { subscription, plans } = subscriptionQuery; const canManageBilling = canExecuteOrganisationAction('MANAGE_BILLING', organisation.currentOrganisationRole); const { organisationSubscription, stripeSubscription } = subscription || {}; const currentProductName = // eslint-disable-next-line @typescript-eslint/consistent-type-assertions (stripeSubscription?.items.data[0].price.product as Stripe.Product | undefined)?.name; return (

Billing

{!organisationSubscription && (

You are currently on the Free Plan.

)} {organisationSubscription && match(organisationSubscription.status) .with('ACTIVE', () => (

{match(organisationSubscription) .with({ cancelAtPeriodEnd: true, periodEnd: P.nonNullable }, ({ periodEnd }) => currentProductName ? ( You are currently subscribed to {currentProductName}{' '} which is set to end on {i18n.date(periodEnd)}. ) : ( You currently have an active plan which is set to end on{' '} {i18n.date(periodEnd)}. ), ) .with({ cancelAtPeriodEnd: false, periodEnd: P.nonNullable }, ({ periodEnd }) => currentProductName ? ( You are currently subscribed to {currentProductName}{' '} which is set to automatically renew on{' '} {i18n.date(periodEnd)}. ) : ( You currently have an active plan which is set to automatically renew on{' '} {i18n.date(periodEnd)}. ), ) .otherwise(() => currentProductName ? ( You are currently subscribed to {currentProductName}. ) : ( You currently have an active plan. ), )}

)) .with('INACTIVE', () => (

{currentProductName ? ( You currently have an inactive {currentProductName}{' '} subscription. ) : ( Your current plan is inactive. )}

)) .with('PAST_DUE', () => (

{currentProductName ? ( Your current {currentProductName} plan is past due. Please update your payment information. ) : ( Your current plan is past due. )}

)) .otherwise(() => null)}

{(!subscription || subscription.organisationSubscription.status === SubscriptionStatus.INACTIVE) && canManageBilling && }
); }