diff --git a/.env.local.example b/.env.local.example
index 4944be14d..46d13ab9b 100644
--- a/.env.local.example
+++ b/.env.local.example
@@ -6,6 +6,6 @@ NEXT_PUBLIC_SUPABASE_URL="http://127.0.0.1:54321"
SUPABASE_SERVICE_ROLE_KEY=
# Get these from Stripe dashboard
-NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
-STRIPE_SECRET_KEY=
-STRIPE_WEBHOOK_SECRET=
+NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_placeholder"
+STRIPE_SECRET_KEY="sk_test_placeholder"
+STRIPE_WEBHOOK_SECRET=
\ No newline at end of file
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 000000000..6b10a5b73
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,6 @@
+{
+ "extends": [
+ "next/core-web-vitals",
+ "next/typescript"
+ ]
+}
diff --git a/README.md b/README.md
index 32b9a8312..9e468f845 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,5 @@
# Next.js Subscription Payments Starter
-
-> [!WARNING]
-> This repo has been sunset and replaced by a new template: https://github.com/nextjs/saas-starter
-
## Features
- Secure user management and authentication with [Supabase](https://supabase.io/docs/guides/auth)
@@ -29,7 +25,7 @@ When deploying this template, the sequence of steps is important. Follow the ste
#### Vercel Deploy Button
-[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fnextjs-subscription-payments&env=NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,STRIPE_SECRET_KEY&envDescription=Enter%20your%20Stripe%20API%20keys.&envLink=https%3A%2F%2Fdashboard.stripe.com%2Fapikeys&project-name=nextjs-subscription-payments&repository-name=nextjs-subscription-payments&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6&external-id=https%3A%2F%2Fgithub.com%2Fvercel%2Fnextjs-subscription-payments%2Ftree%2Fmain)
+[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase-community%2Fnextjs-subscription-payments&env=NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,STRIPE_SECRET_KEY&envDescription=Enter%20your%20Stripe%20API%20keys.&envLink=https%3A%2F%2Fdashboard.stripe.com%2Fapikeys&project-name=nextjs-subscription-payments&repository-name=nextjs-subscription-payments&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6&external-id=https%3A%2F%2Fgithub.com%2Fsupabase-community%2Fnextjs-subscription-payments%2Ftree%2Fmain)
The Vercel Deployment will create a new repository with this template on your GitHub account and guide you through a new Supabase project creation. The [Supabase Vercel Deploy Integration](https://vercel.com/integrations/supabase) will set up the necessary Supabase environment variables and run the [SQL migrations](./supabase/migrations/20230530034630_init.sql) to set up the Database schema on your account. You can inspect the created tables in your project's [Table editor](https://app.supabase.com/project/_/editor).
@@ -71,12 +67,21 @@ For the following steps, make sure you have the ["Test Mode" toggle](https://str
We need to create a webhook in the `Developers` section of Stripe. Pictured in the architecture diagram above, this webhook is the piece that connects Stripe to your Vercel Serverless Functions.
-1. Click the "Add Endpoint" button on the [test Endpoints page](https://dashboard.stripe.com/test/webhooks).
-1. Enter your production deployment URL followed by `/api/webhooks` for the endpoint URL. (e.g. `https://your-deployment-url.vercel.app/api/webhooks`)
-1. Click `Select events` under the `Select events to listen to` heading.
-1. Click `Select all events` in the `Select events to send` section.
-1. Copy `Signing secret` as we'll need that in the next step (e.g `whsec_xxx`) (/!\ be careful not to copy the webook id we_xxxx).
-1. In addition to the `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` and the `STRIPE_SECRET_KEY` we've set earlier during deployment, we need to add the webhook secret as `STRIPE_WEBHOOK_SECRET` env var.
+1. Click the "Add Endpoint" (or "Create an event destination") button on the [test Endpoints page](https://dashboard.stripe.com/test/webhooks).
+2. **Important:** When selecting events, you may see a notice about "Snapshot payloads" vs "Thin payloads". This template uses **snapshot payloads** (the traditional format). Only select events that use snapshot payloads, or if given the option, skip creating a destination for thin payloads.
+3. Select the following event types (these all use snapshot payloads):
+ - `customer.*`
+ - `product.*`
+ - `price.*`
+ - `checkout.session.*`
+ - `invoice.*`
+ - `subscription.*`
+
+ Alternatively, you can select "All events" but be aware you may need to create separate destinations for different payload styles.
+4. Click "Continue" and select "Webhook endpoint" as the destination type.
+5. Enter your production deployment URL followed by `/api/webhooks` for the endpoint URL. (e.g. `https://your-deployment-url.vercel.app/api/webhooks`)
+6. Copy the `Signing secret` as we'll need that in the next step (e.g `whsec_xxx`) (/!\ be careful not to copy the webhook id `we_xxxx`).
+7. In addition to the `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` and the `STRIPE_SECRET_KEY` we've set earlier during deployment, we need to add the webhook secret as `STRIPE_WEBHOOK_SECRET` env var.
#### Redeploy with new env vars
@@ -148,7 +153,13 @@ Running this command will create a new `.env.local` file in your project folder.
It's highly recommended to use a local Supabase instance for development and testing. We have provided a set of custom commands for this in `package.json`.
-First, you will need to install [Docker](https://www.docker.com/get-started/). You should also copy or rename:
+First, you will need to install [Docker](https://www.docker.com/get-started/). You will also need to install the [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started):
+
+```bash
+brew install supabase/tap/supabase
+```
+
+Next, copy or rename:
- `.env.local.example` -> `.env.local`
- `.env.example` -> `.env`
diff --git a/app/account/page.tsx b/app/account/page.tsx
index 0a2e9cf81..eef93284a 100644
--- a/app/account/page.tsx
+++ b/app/account/page.tsx
@@ -10,7 +10,7 @@ import {
} from '@/utils/supabase/queries';
export default async function Account() {
- const supabase = createClient();
+ const supabase = await createClient();
const [user, userDetails, subscription] = await Promise.all([
getUser(supabase),
getUserDetails(supabase),
@@ -40,4 +40,4 @@ export default async function Account() {
);
-}
+}
\ No newline at end of file
diff --git a/app/api/webhooks/route.ts b/app/api/webhooks/route.ts
index 8371d42e4..ea972e1f7 100644
--- a/app/api/webhooks/route.ts
+++ b/app/api/webhooks/route.ts
@@ -28,13 +28,14 @@ export async function POST(req: Request) {
let event: Stripe.Event;
try {
- if (!sig || !webhookSecret)
- return new Response('Webhook secret not found.', { status: 400 });
+ if (!stripe || !sig || !webhookSecret)
+ return new Response('Stripe or webhook configuration not found.', { status: 400 });
event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
console.log(`🔔 Webhook received: ${event.type}`);
- } catch (err: any) {
- console.log(`❌ Error message: ${err.message}`);
- return new Response(`Webhook Error: ${err.message}`, { status: 400 });
+ } catch (err: unknown) {
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
+ console.log(`❌ Error message: ${errorMessage}`);
+ return new Response(`Webhook Error: ${errorMessage}`, { status: 400 });
}
if (relevantEvents.has(event.type)) {
diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts
index 8f4c98222..82baa31f6 100644
--- a/app/auth/callback/route.ts
+++ b/app/auth/callback/route.ts
@@ -10,7 +10,7 @@ export async function GET(request: NextRequest) {
const code = requestUrl.searchParams.get('code');
if (code) {
- const supabase = createClient();
+ const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
diff --git a/app/auth/reset_password/route.ts b/app/auth/reset_password/route.ts
index f1b0147a4..ff12b0684 100644
--- a/app/auth/reset_password/route.ts
+++ b/app/auth/reset_password/route.ts
@@ -10,7 +10,7 @@ export async function GET(request: NextRequest) {
const code = requestUrl.searchParams.get('code');
if (code) {
- const supabase = createClient();
+ const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
diff --git a/app/page.tsx b/app/page.tsx
index 9098e1382..16ad2e615 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -7,7 +7,7 @@ import {
} from '@/utils/supabase/queries';
export default async function PricingPage() {
- const supabase = createClient();
+ const supabase = await createClient();
const [user, products, subscription] = await Promise.all([
getUser(supabase),
getProducts(supabase),
diff --git a/app/signin/[id]/page.tsx b/app/signin/[id]/page.tsx
index 7cda8bf4c..830f6afd6 100644
--- a/app/signin/[id]/page.tsx
+++ b/app/signin/[id]/page.tsx
@@ -5,8 +5,7 @@ import { redirect } from 'next/navigation';
import {
getAuthTypes,
getViewTypes,
- getDefaultSignInView,
- getRedirectMethod
+ getDefaultSignInView
} from '@/utils/auth-helpers/settings';
import Card from '@/components/ui/Card';
import PasswordSignIn from '@/components/ui/AuthForms/PasswordSignIn';
@@ -21,28 +20,32 @@ export default async function SignIn({
params,
searchParams
}: {
- params: { id: string };
- searchParams: { disable_button: boolean };
+ params: Promise<{ id: string }>;
+ searchParams: Promise<{ disable_button: boolean }>;
}) {
+ // Await params and searchParams in Next.js 15
+ const { id } = await params;
+ const { disable_button } = await searchParams;
+
const { allowOauth, allowEmail, allowPassword } = getAuthTypes();
const viewTypes = getViewTypes();
- const redirectMethod = getRedirectMethod();
// Declare 'viewProp' and initialize with the default value
let viewProp: string;
// Assign url id to 'viewProp' if it's a valid string and ViewTypes includes it
- if (typeof params.id === 'string' && viewTypes.includes(params.id)) {
- viewProp = params.id;
+ if (typeof id === 'string' && viewTypes.includes(id)) {
+ viewProp = id;
} else {
+ const cookieStore = await cookies();
const preferredSignInView =
- cookies().get('preferredSignInView')?.value || null;
+ cookieStore.get('preferredSignInView')?.value || null;
viewProp = getDefaultSignInView(preferredSignInView);
return redirect(`/signin/${viewProp}`);
}
// Check if the user is already logged in and redirect to the account page if so
- const supabase = createClient();
+ const supabase = await createClient();
const {
data: { user }
@@ -74,28 +77,25 @@ export default async function SignIn({
{viewProp === 'password_signin' && (
- Don't have an account? Sign up + Don't have an account? Sign up
> diff --git a/components/ui/AuthForms/ForgotPassword.tsx b/components/ui/AuthForms/ForgotPassword.tsx index bdf8f6b7e..52d441e75 100644 --- a/components/ui/AuthForms/ForgotPassword.tsx +++ b/components/ui/AuthForms/ForgotPassword.tsx @@ -10,16 +10,14 @@ import { useState } from 'react'; // Define prop type with allowEmail boolean interface ForgotPasswordProps { allowEmail: boolean; - redirectMethod: string; disableButton?: boolean; } export default function ForgotPassword({ allowEmail, - redirectMethod, disableButton }: ForgotPasswordProps) { - const router = redirectMethod === 'client' ? useRouter() : null; + const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = async (e: React.FormEvent- Don't have an account? Sign up + Don't have an account? Sign up
diff --git a/components/ui/AuthForms/PasswordSignIn.tsx b/components/ui/AuthForms/PasswordSignIn.tsx index 3ec8297d2..edba8afef 100644 --- a/components/ui/AuthForms/PasswordSignIn.tsx +++ b/components/ui/AuthForms/PasswordSignIn.tsx @@ -10,14 +10,12 @@ import React, { useState } from 'react'; // Define prop type with allowEmail boolean interface PasswordSignInProps { allowEmail: boolean; - redirectMethod: string; } export default function PasswordSignIn({ - allowEmail, - redirectMethod + allowEmail }: PasswordSignInProps) { - const router = redirectMethod === 'client' ? useRouter() : null; + const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = async (e: React.FormEvent- Don't have an account? Sign up + Don't have an account? Sign up
diff --git a/components/ui/AuthForms/Signup.tsx b/components/ui/AuthForms/Signup.tsx index cd98f0407..7ed6cb321 100644 --- a/components/ui/AuthForms/Signup.tsx +++ b/components/ui/AuthForms/Signup.tsx @@ -11,11 +11,10 @@ import { useState } from 'react'; // Define prop type with allowEmail boolean interface SignUpProps { allowEmail: boolean; - redirectMethod: string; } -export default function SignUp({ allowEmail, redirectMethod }: SignUpProps) { - const router = redirectMethod === 'client' ? useRouter() : null; +export default function SignUp({ allowEmail }: SignUpProps) { + const router = useRouter(); const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = async (e: React.FormEvent