Skip to content

Commit 8697790

Browse files
authored
Convert to use Next.js app directory (vercel#183)
* Update package.json * working on migrate to app directory * work * working fairly well * mucho work * kill useUser * getting close * should be good * cleanup * fixes * webhook fix -removing server action for now -also fixed lifetime interval insertion/update * more fixes -revert schema.sql changes -remove prettier config -remove esling config -remove automatic tax in stripe -fix subscription query return * update to auth-helpers-nextjs v0.7 -server action to update name
1 parent e51b170 commit 8697790

40 files changed

+1301
-2331
lines changed
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client';
2+
3+
import Button from '@/components/ui/Button';
4+
import { postData } from '@/utils/helpers';
5+
6+
import { Session } from '@supabase/supabase-js';
7+
8+
interface Props {
9+
session: Session;
10+
}
11+
12+
export default function ManageSubscriptionButton({ session }: Props) {
13+
const redirectToCustomerPortal = async () => {
14+
try {
15+
const { url } = await postData({
16+
url: '/api/create-portal-link'
17+
});
18+
window.location.assign(url);
19+
} catch (error) {
20+
if (error) return alert((error as Error).message);
21+
}
22+
};
23+
24+
return (
25+
<div className="flex flex-col items-start justify-between sm:flex-row sm:items-center">
26+
<p className="pb-4 sm:pb-0">Manage your subscription on Stripe.</p>
27+
<Button
28+
variant="slim"
29+
disabled={!session}
30+
onClick={redirectToCustomerPortal}
31+
>
32+
Open customer portal
33+
</Button>
34+
</div>
35+
);
36+
}

app/account/page.tsx

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { ReactNode } from 'react';
2+
import { cookies } from 'next/headers'
3+
import { redirect } from 'next/navigation';
4+
import { revalidatePath } from 'next/cache'
5+
import Link from 'next/link';
6+
import {
7+
getSession,
8+
getUserDetails,
9+
getSubscription
10+
} from '@/app/supabase-server';
11+
import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
12+
import ManageSubscriptionButton from './ManageSubscriptionButton';
13+
import Button from '@/components/ui/Button';
14+
import { Database } from '@/types_db';
15+
16+
export default async function Account() {
17+
const session = await getSession();
18+
const user = session?.user;
19+
const userDetails = await getUserDetails();
20+
const subscription = await getSubscription();
21+
22+
if (!session) {
23+
redirect('/signin');
24+
}
25+
26+
const subscriptionPrice =
27+
subscription &&
28+
new Intl.NumberFormat('en-US', {
29+
style: 'currency',
30+
currency: subscription?.prices?.currency,
31+
minimumFractionDigits: 0
32+
}).format((subscription?.prices?.unit_amount || 0) / 100);
33+
34+
const updateName = async (formData: FormData) => {
35+
'use server'
36+
37+
const newName = formData.get('name')
38+
const supabase = createServerActionClient<Database>({ cookies })
39+
const session = await getSession();
40+
const user = session?.user;
41+
const { error } = await supabase.from('users').update({ full_name: newName }).eq('id', user?.id)
42+
if (error) {
43+
console.log(error)
44+
}
45+
revalidatePath('/account')
46+
}
47+
48+
const updateEmail = async (formData: FormData) => {
49+
'use server'
50+
51+
const newEmail = formData.get('email')
52+
const supabase = createServerActionClient<Database>({ cookies })
53+
const { error } = await supabase.auth.updateUser({ email: newEmail })
54+
if (error) {
55+
console.log(error)
56+
}
57+
revalidatePath('/account')
58+
}
59+
60+
return (
61+
<section className="mb-32 bg-black">
62+
<div className="max-w-6xl px-4 py-8 mx-auto sm:px-6 sm:pt-24 lg:px-8">
63+
<div className="sm:align-center sm:flex sm:flex-col">
64+
<h1 className="text-4xl font-extrabold text-white sm:text-center sm:text-6xl">
65+
Account
66+
</h1>
67+
<p className="max-w-2xl m-auto mt-5 text-xl text-zinc-200 sm:text-center sm:text-2xl">
68+
We partnered with Stripe for a simplified billing.
69+
</p>
70+
</div>
71+
</div>
72+
<div className="p-4">
73+
<Card
74+
title="Your Plan"
75+
description={
76+
subscription
77+
? `You are currently on the ${subscription?.prices?.products?.name} plan.`
78+
: 'You are not currently subscribed to any plan.'
79+
}
80+
footer={<ManageSubscriptionButton session={session} />}
81+
>
82+
<div className="mt-8 mb-4 text-xl font-semibold">
83+
{subscription ? (
84+
`${subscriptionPrice}/${subscription?.prices?.interval}`
85+
) : (
86+
<Link href="/">Choose your plan</Link>
87+
)}
88+
</div>
89+
</Card>
90+
<Card
91+
title="Your Name"
92+
description="Please enter your full name, or a display name you are comfortable with."
93+
footer={
94+
<div className="flex flex-col items-start justify-between sm:flex-row sm:items-center">
95+
<p className="pb-4 sm:pb-0">64 characters maximum</p>
96+
<Button variant="slim" type="submit" form="nameForm" disabled={true}>
97+
{/* WARNING - In Next.js 13.4.x server actions are in alpha and should not be used in production code! */}
98+
Update Name
99+
</Button>
100+
</div>
101+
}
102+
>
103+
<div className="mt-8 mb-4 text-xl font-semibold">
104+
<form id="nameForm" action={updateName}>
105+
<input
106+
type="text"
107+
name="name"
108+
className="w-1/2 p-3 rounded-md bg-zinc-800"
109+
defaultValue={userDetails?.full_name}
110+
placeholder="Your name"
111+
maxLength={64}
112+
/>
113+
</form>
114+
</div>
115+
</Card>
116+
<Card
117+
title="Your Email"
118+
description="Please enter the email address you want to use to login."
119+
footer={
120+
<div className="flex flex-col items-start justify-between sm:flex-row sm:items-center">
121+
<p className="pb-4 sm:pb-0">We will email you to verify the change.</p>
122+
<Button variant="slim" type="submit" form="emailForm" disabled={true}>
123+
{/* WARNING - In Next.js 13.4.x server actions are in alpha and should not be used in production code! */}
124+
Update Email
125+
</Button>
126+
</div>
127+
}
128+
>
129+
<div className="mt-8 mb-4 text-xl font-semibold">
130+
<form id="emailForm" action={updateEmail}>
131+
<input
132+
type="text"
133+
name="email"
134+
className="w-1/2 p-3 rounded-md bg-zinc-800"
135+
defaultValue={user ? user.email : ""}
136+
placeholder="Your email"
137+
maxLength={64}
138+
/>
139+
</form>
140+
</div>
141+
</Card>
142+
</div>
143+
</section>
144+
);
145+
}
146+
147+
interface Props {
148+
title: string;
149+
description?: string;
150+
footer?: ReactNode;
151+
children: ReactNode;
152+
}
153+
154+
function Card({ title, description, footer, children }: Props) {
155+
return (
156+
<div className="w-full max-w-3xl m-auto my-8 border rounded-md p border-zinc-700">
157+
<div className="px-5 py-4">
158+
<h3 className="mb-1 text-2xl font-medium">{title}</h3>
159+
<p className="text-zinc-300">{description}</p>
160+
{children}
161+
</div>
162+
<div className="p-4 border-t rounded-b-md border-zinc-700 bg-zinc-900 text-zinc-500">
163+
{footer}
164+
</div>
165+
</div>
166+
);
167+
}
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { cookies, headers } from 'next/headers';
2+
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
3+
import { stripe } from '@/utils/stripe';
4+
import { createOrRetrieveCustomer } from '@/utils/supabase-admin';
5+
import { getURL } from '@/utils/helpers';
6+
import { Database } from '@/types_db';
7+
8+
export async function POST(req: Request) {
9+
if (req.method === 'POST') {
10+
// 1. Destructure the price and quantity from the POST body
11+
const { price, quantity = 1, metadata = {} } = await req.json();
12+
13+
try {
14+
// 2. Get the user from Supabase auth
15+
const supabase = createRouteHandlerClient<Database>({
16+
headers,
17+
cookies
18+
});
19+
const {
20+
data: { user }
21+
} = await supabase.auth.getUser();
22+
23+
// 3. Retrieve or create the customer in Stripe
24+
const customer = await createOrRetrieveCustomer({
25+
uuid: user?.id || '',
26+
email: user?.email || ''
27+
});
28+
29+
// 4. Create a checkout session in Stripe
30+
let session;
31+
if (price.type === 'recurring') {
32+
session = await stripe.checkout.sessions.create({
33+
payment_method_types: ['card'],
34+
billing_address_collection: 'required',
35+
customer,
36+
customer_update: {
37+
address: 'auto'
38+
},
39+
line_items: [
40+
{
41+
price: price.id,
42+
quantity
43+
}
44+
],
45+
mode: 'subscription',
46+
allow_promotion_codes: true,
47+
subscription_data: {
48+
trial_from_plan: true,
49+
metadata
50+
},
51+
success_url: `${getURL()}/account`,
52+
cancel_url: `${getURL()}/`
53+
});
54+
} else if (price.type === 'one_time') {
55+
session = await stripe.checkout.sessions.create({
56+
payment_method_types: ['card'],
57+
billing_address_collection: 'required',
58+
customer,
59+
customer_update: {
60+
address: 'auto'
61+
},
62+
line_items: [
63+
{
64+
price: price.id,
65+
quantity
66+
}
67+
],
68+
mode: 'payment',
69+
allow_promotion_codes: true,
70+
success_url: `${getURL()}/account`,
71+
cancel_url: `${getURL()}/`
72+
});
73+
}
74+
75+
if (session) {
76+
return new Response(JSON.stringify({ sessionId: session.id }), {
77+
status: 200
78+
});
79+
} else {
80+
return new Response(
81+
JSON.stringify({
82+
error: { statusCode: 500, message: 'Session is not defined' }
83+
}),
84+
{ status: 500 }
85+
);
86+
}
87+
} catch (err: any) {
88+
console.log(err);
89+
return new Response(JSON.stringify(err), { status: 500 });
90+
}
91+
} else {
92+
return new Response('Method Not Allowed', {
93+
headers: { Allow: 'POST' },
94+
status: 405
95+
});
96+
}
97+
}
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
import { NextApiHandler } from 'next';
2-
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs';
3-
1+
import { cookies, headers } from 'next/headers';
2+
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
43
import { stripe } from '@/utils/stripe';
54
import { createOrRetrieveCustomer } from '@/utils/supabase-admin';
65
import { getURL } from '@/utils/helpers';
6+
import { Database } from '@/types_db';
77

8-
const CreatePortalLink: NextApiHandler = async (req, res) => {
8+
export async function POST(req: Request) {
99
if (req.method === 'POST') {
1010
try {
11-
const supabase = createServerSupabaseClient({ req, res });
11+
const supabase = createRouteHandlerClient<Database>({
12+
headers,
13+
cookies
14+
});
1215
const {
1316
data: { user }
1417
} = await supabase.auth.getUser();
@@ -24,18 +27,22 @@ const CreatePortalLink: NextApiHandler = async (req, res) => {
2427
customer,
2528
return_url: `${getURL()}/account`
2629
});
27-
28-
return res.status(200).json({ url });
30+
return new Response(JSON.stringify({ url }), {
31+
status: 200
32+
});
2933
} catch (err: any) {
3034
console.log(err);
31-
res
32-
.status(500)
33-
.json({ error: { statusCode: 500, message: err.message } });
35+
return new Response(
36+
JSON.stringify({ error: { statusCode: 500, message: err.message } }),
37+
{
38+
status: 500
39+
}
40+
);
3441
}
3542
} else {
36-
res.setHeader('Allow', 'POST');
37-
res.status(405).end('Method Not Allowed');
43+
return new Response('Method Not Allowed', {
44+
headers: { Allow: 'POST' },
45+
status: 405
46+
});
3847
}
39-
};
40-
41-
export default CreatePortalLink;
48+
}

0 commit comments

Comments
 (0)