Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next.js 14 & Supabase SSR #278

Merged
merged 103 commits into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
fd3e0f2
Update helpers.ts
chriscarrollsmith Nov 14, 2023
e679650
Update helpers.ts
chriscarrollsmith Nov 14, 2023
0b7597f
Update helpers.ts
chriscarrollsmith Nov 14, 2023
2468bf2
Support propagating product deletion to DB
chriscarrollsmith Nov 17, 2023
07130cd
initial
dalkommatt Nov 27, 2023
2f9aa32
Replace 'var' with 'const'
chriscarrollsmith Nov 27, 2023
135a1c5
add toast
dalkommatt Nov 28, 2023
7e7d633
add password recovery
dalkommatt Nov 28, 2023
29893ac
fixes
dalkommatt Nov 28, 2023
541c619
implement open PRs
dalkommatt Nov 28, 2023
c012ff0
implement more PRs
dalkommatt Nov 28, 2023
7520e3d
fix createOrRetrieveCustomer
dalkommatt Nov 28, 2023
a6644a3
Give button a displayName for logging purposes
chriscarrollsmith Nov 28, 2023
1a0cd38
Created a 'Card' ui component
chriscarrollsmith Nov 28, 2023
4b64f0b
Remove deprecated supabase auth helpers and update
chriscarrollsmith Nov 29, 2023
44b481a
use latest stripe api version, remove payment type
chriscarrollsmith Nov 29, 2023
d5e0c0c
Merge pull request #1 from dalkommatt/nextjs-14-supabase-ssr
chriscarrollsmith Nov 29, 2023
4446b80
Replaced trial_end with trial_period_days
chriscarrollsmith Nov 29, 2023
289bcea
Add signout as server action, move server calls
chriscarrollsmith Nov 29, 2023
41f6875
Added toasts
chriscarrollsmith Nov 29, 2023
c57bf26
'message' -> 'status' or 'error' in toast calls
chriscarrollsmith Nov 29, 2023
1b41ef8
Removed ring focus from toasts container and
chriscarrollsmith Nov 30, 2023
d6667d0
Remove obsolete NextJS config
chriscarrollsmith Nov 30, 2023
1506472
Update route.ts
chriscarrollsmith Nov 30, 2023
4f5b603
Merge pull request #2 from chriscarrollsmith/issue170
chriscarrollsmith Nov 30, 2023
4fad338
Merge pull request #3 from chriscarrollsmith/issue269
chriscarrollsmith Nov 30, 2023
ad95b6d
Merge pull request #5 from chriscarrollsmith/ui-refactor
chriscarrollsmith Nov 30, 2023
3fc4962
merged supabase-auth branch
chriscarrollsmith Nov 30, 2023
f06fd3c
Merged branch pr200 into dev
chriscarrollsmith Nov 30, 2023
1b6dca8
Merge branch 'toasts' into dev
chriscarrollsmith Nov 30, 2023
16a8e8a
Merge branch 'stripe-checkout' into dev
chriscarrollsmith Nov 30, 2023
e50fc43
Rolled back stripe API version change
chriscarrollsmith Nov 30, 2023
ac26a2e
Allow stripe-js to use user's default API version
chriscarrollsmith Nov 30, 2023
89fb774
Update dependencies to latest
chriscarrollsmith Nov 30, 2023
19dbc77
refactor supabase auth
dijonmusters Dec 1, 2023
ac5cc80
Merge pull request #1 from dijonmusters/nextjs-14-supabase-ssr
dalkommatt Dec 1, 2023
a4af3cc
add magic link/otp auth
dalkommatt Dec 1, 2023
a5aca7b
Update package.json
dalkommatt Dec 1, 2023
77f0724
Implement half-working password recovery
chriscarrollsmith Dec 1, 2023
61ea541
Merge pull request #7 from dalkommatt/nextjs-14-supabase-ssr
chriscarrollsmith Dec 3, 2023
cfb1137
Added signup, password signin, password reset
chriscarrollsmith Dec 3, 2023
ddadebc
Updated dependencies
chriscarrollsmith Dec 3, 2023
c1598bc
Squashed commit of the following:
chriscarrollsmith Dec 4, 2023
532efe8
API routes for the various auth pathways
chriscarrollsmith Dec 4, 2023
049d4d1
Updated dependencies
chriscarrollsmith Dec 4, 2023
b79a729
Eliminated API endpoints
chriscarrollsmith Dec 4, 2023
8b088c3
Added simple switches in auth-helpers.ts to
chriscarrollsmith Dec 4, 2023
0fefdac
Merge branch 'auth-helpers' into dev
chriscarrollsmith Dec 5, 2023
77fedf7
Refactor/streamline createOrRetrieveCustomer
chriscarrollsmith Dec 5, 2023
cc13e0f
getURL now takes a path argument and handles leading slashes
chriscarrollsmith Dec 6, 2023
23f5e24
Fixed bug that broke checkout session if trial period was not set
chriscarrollsmith Dec 6, 2023
72fc682
Cascade user deletion through database
chriscarrollsmith Dec 6, 2023
09bf861
Added full support for toast error handling when
chriscarrollsmith Dec 7, 2023
98e54c4
repaired/improved Stripe webhook error handling
chriscarrollsmith Dec 7, 2023
bb651b7
- Added Suspense around Toaster per NextJS docs on `useSearchParams`
chriscarrollsmith Dec 7, 2023
fdae906
Added package.json npm command for stripe fixtures
chriscarrollsmith Dec 7, 2023
9834e4b
Fixed mishandled magic link condition and handled default sign-in vie…
chriscarrollsmith Dec 7, 2023
1a8b046
Handled edge case where user's preferredSignInView
chriscarrollsmith Dec 7, 2023
55ddbe9
- gitignored some local dev files
chriscarrollsmith Dec 10, 2023
8994d0d
- gitignored some local dev files
chriscarrollsmith Dec 10, 2023
23c8515
Enhanced control of routing and redirects
chriscarrollsmith Dec 11, 2023
6c9b5cb
Deleted defunct Card components
chriscarrollsmith Dec 11, 2023
a94034a
Toaster passes through additional searchParams\nDisable button after …
chriscarrollsmith Dec 11, 2023
1788514
Separated server and client Navbar components
chriscarrollsmith Dec 12, 2023
aea6e6e
Simplified sign-in redirect
chriscarrollsmith Dec 12, 2023
ab11095
Sorted stripe helpers into client and server files
chriscarrollsmith Dec 13, 2023
e7273cf
gitignore vscode workspace settings
chriscarrollsmith Dec 13, 2023
2091adb
Merge branch 'pr278' into dev
chriscarrollsmith Dec 13, 2023
1dc1b60
Replaced stripe checkout API with server action
chriscarrollsmith Dec 14, 2023
784eb64
Replace Stripe portal API with server action
chriscarrollsmith Dec 14, 2023
3482f3e
Update middleware.ts
dalkommatt Dec 15, 2023
7d30272
Delete next.config.js
dalkommatt Dec 15, 2023
ca20608
Merge pull request #8 from dalkommatt/nextjs-14-supabase-ssr
chriscarrollsmith Dec 16, 2023
9743f1f
Disable buttons while submitting, fix password reset bug
chriscarrollsmith Dec 17, 2023
1fd8124
Merge branch 'pr278' of https://github.com/chriscarrollsmith/nextjs-s…
chriscarrollsmith Dec 17, 2023
22cc94e
- Separate client/server account functions
chriscarrollsmith Dec 26, 2023
b599f98
Fixed missing leading slash in error redirect
chriscarrollsmith Dec 27, 2023
5bec024
bump packages
dalkommatt Dec 27, 2023
8848e8f
Add display index to Stripe fixtures
chriscarrollsmith Dec 27, 2023
3fc8666
Redirect to home page on successful signup
chriscarrollsmith Dec 27, 2023
f2f553c
Set default trial period with a variable
chriscarrollsmith Dec 27, 2023
80d9a6b
Updated dependencies
chriscarrollsmith Dec 27, 2023
87a35ba
Universally implement loading dots
chriscarrollsmith Jan 5, 2024
edc045a
Handle database migrations appropriately
chriscarrollsmith Jan 5, 2024
8df1d3f
Added Supabase local development workflow
chriscarrollsmith Jan 5, 2024
50199e0
Script to link to supabase remote
chriscarrollsmith Jan 5, 2024
f358328
Fixed some bugs in link implementation
chriscarrollsmith Jan 5, 2024
b2461f0
Removed accidental redundant file
chriscarrollsmith Jan 5, 2024
a10edf6
Enable buttons when user is not logged in
chriscarrollsmith Jan 6, 2024
5acd31f
Fixed supabase:migrate command
chriscarrollsmith Jan 15, 2024
2a6756c
Fixed AuthApiError that arises in testing
chriscarrollsmith Feb 4, 2024
2bbb6ff
Documented how to develop locally with Supabase
chriscarrollsmith Feb 4, 2024
b8781e8
bump again
dalkommatt Feb 5, 2024
ac51cb1
Ignore svg routes in middleware pattern matcher
chriscarrollsmith Feb 5, 2024
d86e4c4
update dependencies, use turbo for local dev
chriscarrollsmith Feb 5, 2024
c523be3
Correctly export Next Metadata
chriscarrollsmith Feb 5, 2024
2976b13
Merge branch 'pr/2' into nextjs-14-supabase-ssr
dalkommatt Feb 8, 2024
8218435
fix regex on middleware matcher
dijonmusters Feb 8, 2024
91d75e5
fix case on Next.js
dijonmusters Feb 8, 2024
4298e43
refactor cookies to helpers
dijonmusters Feb 8, 2024
33e2794
add prettier script
dijonmusters Feb 8, 2024
f0af873
Merge pull request #4 from dijonmusters/other-fixes-from-pr-review
dalkommatt Feb 8, 2024
8879c19
cleanup and bump
dalkommatt Feb 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Should the automatic setup fail, please [create a Supabase account](https://app.

### Configure Auth

Follow [this guide](https://supabase.com/docs/guides/auth/social-login/auth-github) to setup an OAuth app with GitHub and configure Supabase to use it as an auth provider.

In your Supabase project, navigate to [auth > URL configuration](https://app.supabase.com/project/_/auth/url-configuration) and set your main production URL (e.g. https://your-deployment-url.vercel.app) as the site url.

Next, in your Vercel deployment settings, add a new **Production** environment variable called `NEXT_PUBLIC_SITE_URL` and set it to the same URL. Make sure to deselect preview and development environments to make sure that preview branches and local development work correctly.
Expand All @@ -45,9 +47,11 @@ If you've deployed this template via the "Deploy to Vercel" button above, you ca

Otherwise, for auth redirects (email confirmations, magic links, OAuth providers) to work correctly in deploy previews, navigate to the [auth settings](https://app.supabase.com/project/_/auth/url-configuration) and add the following wildcard URL to "Redirect URLs": `https://*-username.vercel.app/**`. You can read more about redirect wildcard patterns in the [docs](https://supabase.com/docs/guides/auth#redirect-urls-and-wildcards).

#### [Optional] - Set up OAuth providers
#### [Maybe Optional] - Set up database schema (not needed if you installed via the Deploy Button)

If you've deployed this template via the "Deploy to Vercel" button above, you can skip this step. The Supabase Vercel Integration will have run database migrations for you. You can check this by going to [the Table Editor for your Supabase project](https://supabase.com/dashboard/project/_/editor), and confirming there are tables with seed data.

You can use third-party login providers like GitHub or Google. Refer to the [docs](https://supabase.io/docs/guides/auth#third-party-logins) to learn how to configure these. Once configured, you can add them to the `provider` array of the [`Auth` component](./app/signin/AuthUI.tsx) page.
Otherwise navigate to the [SQL Editor](https://supabase.com/dashboard/project/_/sql/new), paste the contents of [the Supabase migration file](./supabase/migrations/20230530034630_init.sql) and click RUN.

#### [Maybe Optional] - Set up Supabase environment variables (not needed if you installed via the Deploy Button)

Expand Down
8 changes: 4 additions & 4 deletions app/account/ManageSubscriptionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import Button from '@/components/ui/Button';
import { postData } from '@/utils/helpers';

import { Session } from '@supabase/supabase-js';
import { User } from '@supabase/supabase-js';
import { useRouter } from 'next/navigation';

interface Props {
session: Session;
user: User;
}

export default function ManageSubscriptionButton({ session }: Props) {
export default function ManageSubscriptionButton({ user }: Props) {
const router = useRouter();
const redirectToCustomerPortal = async () => {
try {
Expand All @@ -28,7 +28,7 @@ export default function ManageSubscriptionButton({ session }: Props) {
<p className="pb-4 sm:pb-0">Manage your subscription on Stripe.</p>
<Button
variant="slim"
disabled={!session}
disabled={!user}
onClick={redirectToCustomerPortal}
>
Open customer portal
Expand Down
151 changes: 88 additions & 63 deletions app/account/page.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,35 @@
import ManageSubscriptionButton from './ManageSubscriptionButton';
import {
getSession,
getUserDetails,
getSubscription
} from '@/app/supabase-server';
import Button from '@/components/ui/Button';
import { Database } from '@/types_db';
import { createServerActionClient } from '@supabase/auth-helpers-nextjs';
import { revalidatePath } from 'next/cache';
import Card from '@/components/ui/Card';
import { createClient } from '@/utils/supabase/server';
import { cookies } from 'next/headers';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { ReactNode } from 'react';

export default async function Account() {
const [session, userDetails, subscription] = await Promise.all([
getSession(),
getUserDetails(),
getSubscription()
]);
const cookieStore = cookies();
const supabase = createClient(cookieStore);

const user = session?.user;
const {
data: { user }
} = await supabase.auth.getUser();

if (!session) {
const { data: userDetails } = await supabase
.from('users')
.select('*')
.single();

const { data: subscription, error } = await supabase
.from('subscriptions')
.select('*, prices(*, products(*))')
.in('status', ['trialing', 'active'])
.maybeSingle();

if (error) {
console.log(error);
}

if (!user) {
return redirect('/signin');
}

Expand All @@ -38,29 +45,81 @@ export default async function Account() {
'use server';

const newName = formData.get('name') as string;
const supabase = createServerActionClient<Database>({ cookies });
const session = await getSession();
const user = session?.user;
const cookieStore = cookies();
const supabase = createClient(cookieStore);

const {
data: { user }
} = await supabase.auth.getUser();

if (!user) return redirect('/signin');

const { error } = await supabase
.from('users')
.update({ full_name: newName })
.eq('id', user?.id);
.eq('id', user.id);

if (error) {
console.log(error);
return redirect(
`/account?error=${encodeURI(
'Hmm... Something went wrong.'
)}&error_description=${encodeURI('Your name could not be updated.')}`
);
}
revalidatePath('/account');

return redirect(
`/account?status=${encodeURI('Success!')}&status_description=${encodeURI(
'Your name has been updated.'
)}`
);
};

const updateEmail = async (formData: FormData) => {
'use server';

const newEmail = formData.get('email') as string;
const supabase = createServerActionClient<Database>({ cookies });
const { error } = await supabase.auth.updateUser({ email: newEmail });
const cookieStore = cookies();
const supabase = createClient(cookieStore);

const { error } = await supabase.auth.updateUser(
{ email: newEmail },
{
emailRedirectTo:
process.env.NEXT_PUBLIC_SITE_URL +
`/account?status=${encodeURI(
'Success!'
)}&status_description=${encodeURI(
`Your email has been successfully updated to ${newEmail}`
)}`
}
);

if (error) {
console.log(error);
if (
error.message ===
'A user with this email address has already been registered'
) {
return redirect(
`/account?error=${encodeURI('Oops!')}&error_description=${encodeURI(
'It looks like that email is already in use. Please try another one.'
)}`
);
}

return redirect(
`/account?error=${encodeURI(
'Hmm... Something went wrong.'
)}&error_description=${encodeURI('Your email could not be updated.')}`
);
}
revalidatePath('/account');

return redirect(
`/account?status=${encodeURI(
'Confirmation Emails Sent'
)}&error_description=${encodeURI(
`You will need to confirm the update by clicking the link sent to both ${user?.email} and ${newEmail}.`
)}`
);
};

return (
Expand All @@ -83,7 +142,7 @@ export default async function Account() {
? `You are currently on the ${subscription?.prices?.products?.name} plan.`
: 'You are not currently subscribed to any plan.'
}
footer={<ManageSubscriptionButton session={session} />}
footer={<ManageSubscriptionButton user={user} />}
>
<div className="mt-8 mb-4 text-xl font-semibold">
{subscription ? (
Expand All @@ -99,13 +158,7 @@ export default async function Account() {
footer={
<div className="flex flex-col items-start justify-between sm:flex-row sm:items-center">
<p className="pb-4 sm:pb-0">64 characters maximum</p>
<Button
variant="slim"
type="submit"
form="nameForm"
disabled={true}
>
{/* WARNING - In Next.js 13.4.x server actions are in alpha and should not be used in production code! */}
<Button variant="slim" type="submit" form="nameForm">
Update Name
</Button>
</div>
Expand All @@ -132,13 +185,7 @@ export default async function Account() {
<p className="pb-4 sm:pb-0">
We will email you to verify the change.
</p>
<Button
variant="slim"
type="submit"
form="emailForm"
disabled={true}
>
{/* WARNING - In Next.js 13.4.x server actions are in alpha and should not be used in production code! */}
<Button variant="slim" type="submit" form="emailForm">
Update Email
</Button>
</div>
Expand All @@ -161,25 +208,3 @@ export default async function Account() {
</section>
);
}

interface Props {
title: string;
description?: string;
footer?: ReactNode;
children: ReactNode;
}

function Card({ title, description, footer, children }: Props) {
return (
<div className="w-full max-w-3xl m-auto my-8 border rounded-md p border-zinc-700">
<div className="px-5 py-4">
<h3 className="mb-1 text-2xl font-medium">{title}</h3>
<p className="text-zinc-300">{description}</p>
{children}
</div>
<div className="p-4 border-t rounded-b-md border-zinc-700 bg-zinc-900 text-zinc-500">
{footer}
</div>
</div>
);
}
17 changes: 7 additions & 10 deletions app/api/create-checkout-session/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { cookies, headers } from 'next/headers';
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
import { createClient } from '@/utils/supabase/server';
import { stripe } from '@/utils/stripe';
import { createOrRetrieveCustomer } from '@/utils/supabase-admin';
import { createOrRetrieveCustomer } from '@/utils/supabase/admin';
import { getURL } from '@/utils/helpers';
import { Database } from '@/types_db';

export async function POST(req: Request) {
if (req.method === 'POST') {
Expand All @@ -12,7 +11,9 @@ export async function POST(req: Request) {

try {
// 2. Get the user from Supabase auth
const supabase = createRouteHandlerClient<Database>({cookies});
const cookieStore = cookies();
const supabase = createClient(cookieStore);

const {
data: { user }
} = await supabase.auth.getUser();
Expand All @@ -27,7 +28,6 @@ export async function POST(req: Request) {
let session;
if (price.type === 'recurring') {
session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
billing_address_collection: 'required',
customer,
customer_update: {
Expand All @@ -42,15 +42,12 @@ export async function POST(req: Request) {
mode: 'subscription',
allow_promotion_codes: true,
subscription_data: {
trial_from_plan: true,
metadata
},
success_url: `${getURL()}/account`,
cancel_url: `${getURL()}/`
success_url: `${getURL()}/account`
});
} else if (price.type === 'one_time') {
session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
billing_address_collection: 'required',
customer,
customer_update: {
Expand Down
9 changes: 5 additions & 4 deletions app/api/create-portal-link/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { cookies } from 'next/headers';
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { createClient } from '@/utils/supabase/server';
import { stripe } from '@/utils/stripe';
import { createOrRetrieveCustomer } from '@/utils/supabase-admin';
import { createOrRetrieveCustomer } from '@/utils/supabase/admin';
import { getURL } from '@/utils/helpers';
import { Database } from '@/types_db';

export async function POST(req: Request) {
if (req.method === 'POST') {
try {
const supabase = createRouteHandlerClient<Database>({cookies});
const cookieStore = cookies();
const supabase = createClient(cookieStore);

const {
data: { user }
} = await supabase.auth.getUser();
Expand Down
21 changes: 18 additions & 3 deletions app/api/webhooks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import { stripe } from '@/utils/stripe';
import {
upsertProductRecord,
upsertPriceRecord,
manageSubscriptionStatusChange
} from '@/utils/supabase-admin';
manageSubscriptionStatusChange,
deleteProductRecord,
deletePriceRecord
} from '@/utils/supabase/admin';

const relevantEvents = new Set([
'product.created',
'product.updated',
'price.created',
'price.updated',
'price.deleted',
'product.deleted',
'checkout.session.completed',
'customer.subscription.created',
'customer.subscription.updated',
Expand All @@ -24,7 +28,8 @@ export async function POST(req: Request) {
let event: Stripe.Event;

try {
if (!sig || !webhookSecret) return;
if (!sig || !webhookSecret)
return new Response('Webhook secret not found.', { status: 400 });
event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
} catch (err: any) {
console.log(`❌ Error message: ${err.message}`);
Expand All @@ -42,6 +47,12 @@ export async function POST(req: Request) {
case 'price.updated':
await upsertPriceRecord(event.data.object as Stripe.Price);
break;
case 'price.deleted':
await deletePriceRecord(event.data.object as Stripe.Price);
break;
case 'product.deleted':
await deleteProductRecord(event.data.object as Stripe.Product);
break;
case 'customer.subscription.created':
case 'customer.subscription.updated':
case 'customer.subscription.deleted':
Expand Down Expand Up @@ -75,6 +86,10 @@ export async function POST(req: Request) {
}
);
}
} else {
return new Response(`Unsupported event type: ${event.type}`, {
status: 400
});
}
return new Response(JSON.stringify({ received: true }));
}
Loading