feat: add direct marketing to billing subscription flow

This commit is contained in:
Ephraim Atta-Duncan
2025-07-11 11:48:49 +00:00
parent d6c11bd195
commit e04e5a7d2e
4 changed files with 156 additions and 52 deletions

View File

@ -1,6 +1,7 @@
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { useSearchParams } from 'react-router';
import type Stripe from 'stripe';
import { match } from 'ts-pattern';
@ -19,9 +20,14 @@ export function meta() {
export default function TeamsSettingBillingPage() {
const { _, i18n } = useLingui();
const [searchParams] = useSearchParams();
const organisation = useCurrentOrganisation();
const selectedPlan = searchParams.get('plan');
const selectedCycle = searchParams.get('cycle') as 'monthly' | 'yearly' | null;
const source = searchParams.get('source');
const { data: subscriptionQuery, isLoading: isLoadingSubscription } =
trpc.billing.subscription.get.useQuery({
organisationId: organisation.id,
@ -48,8 +54,21 @@ export default function TeamsSettingBillingPage() {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
(stripeSubscription?.items.data[0].price.product as Stripe.Product | undefined)?.name;
const isFromPricingPage = source === 'pricing';
return (
<div>
{isFromPricingPage && selectedPlan && !subscription && (
<div className="bg-muted mb-4 rounded-lg p-4">
<p className="text-sm">
<Trans>
Select a plan below to upgrade <strong>{organisation.name}</strong> to the{' '}
{selectedPlan} plan
</Trans>
</p>
</div>
)}
<div className="flex flex-row items-end justify-between">
<div>
<h3 className="text-2xl font-semibold">
@ -134,7 +153,14 @@ export default function TeamsSettingBillingPage() {
<hr className="my-4" />
{!subscription && canManageBilling && <BillingPlans plans={plans} />}
{!subscription && canManageBilling && (
<BillingPlans
plans={plans}
selectedPlan={selectedPlan}
selectedCycle={selectedCycle}
isFromPricingPage={source === 'pricing'}
/>
)}
<section className="mt-6">
<OrganisationBillingInvoicesTable

View File

@ -0,0 +1,47 @@
import { redirect } from 'react-router';
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
import { isPersonalLayout } from '@documenso/lib/utils/organisations';
import { getOrganisations } from '@documenso/trpc/server/organisation-router/get-organisations';
import type { Route } from './+types/billing-redirect';
export async function loader({ request }: Route.LoaderArgs) {
const session = await getOptionalSession(request);
if (!session.isAuthenticated) {
throw redirect('/signin');
}
const url = new URL(request.url);
const plan = url.searchParams.get('plan');
const cycle = url.searchParams.get('cycle');
const source = url.searchParams.get('source');
const queryParams = new URLSearchParams();
if (plan) {
queryParams.set('plan', plan);
}
if (cycle) {
queryParams.set('cycle', cycle);
}
if (source) {
queryParams.set('source', source);
}
const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
const organisations = await getOrganisations({ userId: session.user.id });
if (isPersonalLayout(organisations)) {
return redirect(`/settings/billing${queryString}`);
}
const personalOrg = organisations.find((org) => org.type === 'PERSONAL') || organisations[0];
if (personalOrg) {
return redirect(`/o/${personalOrg.url}/settings/billing${queryString}`);
}
return redirect('/settings/profile');
}
export default function BillingRedirect() {
return null;
}