page.tsx90 lines · main
1import { headers } from 'next/headers';
2import { redirect } from 'next/navigation';
3
4import { apiFetch } from '../../../../../lib/api';
5import { requireUser } from '../../../../../lib/session';
6
7export const metadata = { title: 'upgrade' };
8export const dynamic = 'force-dynamic';
9
10interface CheckoutBody {
11 tier: 'pro' | 'team';
12 successURL: string;
13}
14
15/**
16 * Server-action bounce: landing-page "get pro" links point here with
17 * ?tier=pro|team. We require auth (signin handles the next= redirect back),
18 * POST /v1/billing/checkout, and forward the user to the Polar-hosted URL.
19 */
20export default async function UpgradePage({
21 searchParams,
22}: {
23 searchParams: Promise<{ tier?: string }>;
24}) {
25 await requireUser();
26 const { tier } = await searchParams;
27
28 if (tier !== 'pro' && tier !== 'team') {
29 return (
30 <main className="mx-auto max-w-lg px-6 py-16 font-mono text-sm">
31 <h1 className="text-xl">unknown plan</h1>
32 <p className="mt-2 text-[var(--color-text-muted)]">
33 pick a plan from the{' '}
34 <a href="/#pricing" className="text-[var(--color-text-link)]">
35 pricing section
36 </a>
37 .
38 </p>
39 </main>
40 );
41 }
42
43 const h = await headers();
44 const proto = h.get('x-forwarded-proto') ?? 'https';
45 const host = h.get('x-forwarded-host') ?? h.get('host') ?? 'briven.tech';
46 const origin = `${proto}://${host}`;
47
48 const body: CheckoutBody = {
49 tier,
50 successURL: `${origin}/dashboard/billing?checkout=success`,
51 };
52
53 const res = await apiFetch('/v1/billing/checkout', {
54 method: 'POST',
55 headers: { 'content-type': 'application/json' },
56 body: JSON.stringify(body),
57 });
58
59 if (!res.ok) {
60 const errBody = (await res.json().catch(() => ({}))) as { message?: string; code?: string };
61 const code = errBody.code ?? '';
62 const notConfigured =
63 code === 'polar_not_configured' ||
64 code === 'unknown_tier' ||
65 res.status === 503;
66 return (
67 <main className="mx-auto max-w-lg px-6 py-16 font-mono text-sm">
68 <h1 className="text-xl">
69 {notConfigured ? 'paid plans not switched on yet' : 'couldn\u2019t start checkout'}
70 </h1>
71 <p className="mt-2 text-[var(--color-text-muted)]">
72 {notConfigured
73 ? 'the price buttons work, but the card payment setup (Polar product IDs + access token) is not filled in on the server yet. free tier still works.'
74 : (errBody.message ?? errBody.code ?? `http ${res.status}`)}
75 </p>
76 <p className="mt-6 flex flex-wrap gap-4">
77 <a href="/pricing" className="text-[var(--color-text-link)]">
78 back to pricing
79 </a>
80 <a href="/dashboard/billing" className="text-[var(--color-text-link)]">
81 billing
82 </a>
83 </p>
84 </main>
85 );
86 }
87
88 const { url } = (await res.json()) as { url: string };
89 redirect(url);
90}