diff --git a/.env.local.example b/.env.local.example index 2af7c641..6a02fa98 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,12 +1,17 @@ -# Update these with your Supabase details from your project settings > API -NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co +# Update these with the details of your local test instance from the Supabase CLI +# ("API URL", "anon key", and "service_role key") +NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= +# Update these with the details of your remote database +SUPABASE_PROJECT_REF= +SUPABASE_DB_PASSWORD= + # Update these with your Stripe credentials from https://dashboard.stripe.com/apikeys NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_1234 STRIPE_SECRET_KEY=sk_test_1234 STRIPE_WEBHOOK_SECRET=whsec_1234 -# Update this with your stable site URL only for the production environment. -NEXT_PUBLIC_SITE_URL=https://your-deployment-url.vercel.app +# Add NEXT_PUBLIC_SITE_URL to your Vercel environmental variables for the production environment following the example below. This defaults to http://localhost:3000 in development. +# NEXT_PUBLIC_SITE_URL=https://your-deployment-url.vercel.app diff --git a/.gitignore b/.gitignore index a0b0f82d..182915c8 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ yarn-error.log* # editors .vscode + +# certificates +certificates diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..51c5789f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,13 @@ +# Build artifacts +.next/ +.turbo/ +_next/ +__tmp__/ +dist/ +node_modules/ +target/ +compiled/ + +pnpm-lock.yaml + +types_db.ts diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..d8290ed7 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "arrowParens": "always", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none" +} diff --git a/README.md b/README.md index 8600f2af..f926f689 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,14 @@ When deploying this template, the sequence of steps is important. Follow the ste [![Deploy with Vercel](https://vercel.com/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) -The Vercel Deployment will create a new repository with this template on your GitHub account and guide your through a new Supabase project creation. The [Supabase Vercel Deploy Integration](https://vercel.com/integrations/supabase-v2) 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). +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-v2) 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). Should the automatic setup fail, please [create a Supabase account](https://app.supabase.com/projects), and a new project if needed. In your project, navigate to the [SQL editor](https://app.supabase.com/project/_/sql) and select the "Stripe Subscriptions" starter template from the Quick start section. ### Configure Auth +Follow [this guide](https://supabase.com/docs/guides/auth/social-login/auth-github) to set up 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. @@ -45,9 +47,9 @@ 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 +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 `schema.sql` file](./schema.sql), and click RUN to initialize the database. #### [Maybe Optional] - Set up Supabase environment variables (not needed if you installed via the Deploy Button) @@ -122,15 +124,51 @@ vercel login vercel link ``` -### Setting up the env vars locally - -Use the Vercel CLI to download the development env vars: +If you don't intend to use a local Supabase instance for development and testing, you can use the Vercel CLI to download the development env vars: ```bash vercel env pull .env.local ``` -Running this command will create a new `.env.local` file in your project folder. For security purposes, you will need to set the `SUPABASE_SERVICE_ROLE_KEY` manually from your [Supabase dashboard](https://app.supabase.io/) (`Settings > API`). +Running this command will create a new `.env.local` file in your project folder. For security purposes, you will need to set the `SUPABASE_SERVICE_ROLE_KEY` manually from your [Supabase dashboard](https://app.supabase.io/) (`Settings > API`). If you are not using a local Supabase instance, you should also change the `--local` flag to `--remote` in the `supabase:generate-types` script in `package.json`. + +### Local development with Supabase + +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 `.env.local.example` to `.env.local`. + +Next, run the following command to start a local Supabase instance and run the migrations to set up the database schema: + +```bash +# or `npm` or `yarn` instead of `pnpm` +pnpm run supabase:start +``` + +The terminal output will provide you with values for the environment variables `NEXT_PUBLIC_SUPABASE_URL`, `NEXT_PUBLIC_SUPABASE_ANON_KEY`, and `SUPABASE_SERVICE_ROLE_KEY`. Copy these into your `.env.local` file. + +The terminal output will also provide you with a URL to access the local Supabase Studio, where you can make changes to your local database instance. (You can always find the Supabase Studio later by opening up a Docker window, navigating to `Containers` tab, and clicking the link in the `Ports` column for the corresponding container.) + +To link your local Supabase instance to your project, you will need to set `SUPABASE_PROJECT_REF` and `SUPABASE_DB_PASSWORD` for your remote database in your `.env.local` file. You can find these values in the [Supabase dashboard](https://supabase.com/dashboard/projects) for your project. Then, run the following command to link your local Supabase instance to your project: + +```bash +pnpm run supabase:link +``` + +Once you've linked your project, you can make changes to the database schema in your local Supabase Studio and run the following command to generate TypeScript types to match your schema: + +```bash +pnpm run supabase:generate-types +``` + +You can also automatically generate a migration file with all the changes you've made to your local database schema and then push the migration to your remote database with the following commands: + +```bash +pnpm run supabase:generate-migration +pnpm run supabase:migrate +``` + +Remember to test your changes thoroughly in your local environment before deploying them to production! ### Use the Stripe CLI to test webhooks @@ -142,13 +180,16 @@ Next, start local webhook forwarding: stripe listen --forward-to=localhost:3000/api/webhooks ``` -Running this Stripe command will print a webhook secret (such as, `whsec_***`) to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your `.env.local` file. +Running this Stripe command will print a webhook secret (such as, `whsec_***`) to the console. Set `STRIPE_WEBHOOK_SECRET` to this value in your `.env.local` file. If you haven't already, you should also set `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` and `STRIPE_SECRET_KEY` in your `.env.local` file using the **test mode**(!) keys from your Stripe dashboard. ### Install dependencies and run the Next.js client In a separate terminal, run the following commands to install dependencies and start the development server: ```bash +pnpm install +pnpm run dev +# or npm install npm run dev # or @@ -175,7 +216,3 @@ To run the project in live mode and process payments with Stripe, switch Stripe Afterward, you will need to rebuild your production deployment for the changes to take effect. Within your project Dashboard, navigate to the "Deployments" tab, select the most recent deployment, click the overflow menu button (next to the "Visit" button) and select "Redeploy" (do NOT enable the "Use existing Build Cache" option). To verify you are running in production mode, test checking out with the [Stripe test card](https://stripe.com/docs/testing). The test card should not work. - -## A note on reliability - -This template mirrors completed Stripe transactions to the Supabase database. This means that if the Supabase database is unavailable, the Stripe transaction will still succeed, but the Supabase database will not be updated, and the application will pass an error code back to Stripe. [By default](https://stripe.com/docs/webhooks/best-practices), Stripe will retry sending its response to the webhook for up to three days, or until the database update succeeds. This means that the Stripe transaction will eventually be reflected in the Supabase database as long as the database comes back online within three days. You may want to implement a process to automatically reconcile the Supabase database with Stripe in case of a prolonged outage. diff --git a/app/account/ManageSubscriptionButton.tsx b/app/account/ManageSubscriptionButton.tsx deleted file mode 100644 index 15a5a445..00000000 --- a/app/account/ManageSubscriptionButton.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client'; - -import Button from '@/components/ui/Button'; -import { postData } from '@/utils/helpers'; - -import { Session } from '@supabase/supabase-js'; -import { useRouter } from 'next/navigation'; - -interface Props { - session: Session; -} - -export default function ManageSubscriptionButton({ session }: Props) { - const router = useRouter(); - const redirectToCustomerPortal = async () => { - try { - const { url } = await postData({ - url: '/api/create-portal-link' - }); - router.push(url); - } catch (error) { - if (error) return alert((error as Error).message); - } - }; - - return ( -
-

Manage your subscription on Stripe.

- -
- ); -} diff --git a/app/account/page.tsx b/app/account/page.tsx index 3fcffaa0..7fcd819f 100644 --- a/app/account/page.tsx +++ b/app/account/page.tsx @@ -1,67 +1,36 @@ -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 { cookies } from 'next/headers'; -import Link from 'next/link'; +'use server'; + +import CustomerPortalForm from '@/components/ui/AccountForms/CustomerPortalForm'; +import EmailForm from '@/components/ui/AccountForms/EmailForm'; +import NameForm from '@/components/ui/AccountForms/NameForm'; +import { createClient } from '@/utils/supabase/server'; 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 user = session?.user; - - if (!session) { - return redirect('/signin'); - } + const supabase = createClient(); - const subscriptionPrice = - subscription && - new Intl.NumberFormat('en-US', { - style: 'currency', - currency: subscription?.prices?.currency!, - minimumFractionDigits: 0 - }).format((subscription?.prices?.unit_amount || 0) / 100); + const { + data: { user } + } = await supabase.auth.getUser(); - const updateName = async (formData: FormData) => { - 'use server'; + const { data: userDetails } = await supabase + .from('users') + .select('*') + .single(); - const newName = formData.get('name') as string; - const supabase = createServerActionClient({ cookies }); - const session = await getSession(); - const user = session?.user; - const { error } = await supabase - .from('users') - .update({ full_name: newName }) - .eq('id', user?.id); - if (error) { - console.log(error); - } - revalidatePath('/account'); - }; + const { data: subscription, error } = await supabase + .from('subscriptions') + .select('*, prices(*, products(*))') + .in('status', ['trialing', 'active']) + .maybeSingle(); - const updateEmail = async (formData: FormData) => { - 'use server'; + if (error) { + console.log(error); + } - const newEmail = formData.get('email') as string; - const supabase = createServerActionClient({ cookies }); - const { error } = await supabase.auth.updateUser({ email: newEmail }); - if (error) { - console.log(error); - } - revalidatePath('/account'); - }; + if (!user) { + return redirect('/signin'); + } return (
@@ -76,110 +45,10 @@ export default async function Account() {
- } - > -
- {subscription ? ( - `${subscriptionPrice}/${subscription?.prices?.interval}` - ) : ( - Choose your plan - )} -
-
- -

64 characters maximum

- -
- } - > -
-
- -
-
- - -

- We will email you to verify the change. -

- - - } - > -
-
- -
-
-
+ + +
); } - -interface Props { - title: string; - description?: string; - footer?: ReactNode; - children: ReactNode; -} - -function Card({ title, description, footer, children }: Props) { - return ( -
-
-

{title}

-

{description}

- {children} -
-
- {footer} -
-
- ); -} diff --git a/app/api/create-checkout-session/route.ts b/app/api/create-checkout-session/route.ts deleted file mode 100644 index 6267e527..00000000 --- a/app/api/create-checkout-session/route.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { cookies, headers } from 'next/headers'; -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; -import { stripe } from '@/utils/stripe'; -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') { - // 1. Destructure the price and quantity from the POST body - const { price, quantity = 1, metadata = {} } = await req.json(); - - try { - // 2. Get the user from Supabase auth - const supabase = createRouteHandlerClient({cookies}); - const { - data: { user } - } = await supabase.auth.getUser(); - - // 3. Retrieve or create the customer in Stripe - const customer = await createOrRetrieveCustomer({ - uuid: user?.id || '', - email: user?.email || '' - }); - - // 4. Create a checkout session in Stripe - let session; - if (price.type === 'recurring') { - session = await stripe.checkout.sessions.create({ - payment_method_types: ['card'], - billing_address_collection: 'required', - customer, - customer_update: { - address: 'auto' - }, - line_items: [ - { - price: price.id, - quantity - } - ], - mode: 'subscription', - allow_promotion_codes: true, - subscription_data: { - trial_from_plan: true, - metadata - }, - success_url: `${getURL()}/account`, - cancel_url: `${getURL()}/` - }); - } else if (price.type === 'one_time') { - session = await stripe.checkout.sessions.create({ - payment_method_types: ['card'], - billing_address_collection: 'required', - customer, - customer_update: { - address: 'auto' - }, - line_items: [ - { - price: price.id, - quantity - } - ], - mode: 'payment', - allow_promotion_codes: true, - success_url: `${getURL()}/account`, - cancel_url: `${getURL()}/` - }); - } - - if (session) { - return new Response(JSON.stringify({ sessionId: session.id }), { - status: 200 - }); - } else { - return new Response( - JSON.stringify({ - error: { statusCode: 500, message: 'Session is not defined' } - }), - { status: 500 } - ); - } - } catch (err: any) { - console.log(err); - return new Response(JSON.stringify(err), { status: 500 }); - } - } else { - return new Response('Method Not Allowed', { - headers: { Allow: 'POST' }, - status: 405 - }); - } -} diff --git a/app/api/create-portal-link/route.ts b/app/api/create-portal-link/route.ts deleted file mode 100644 index 07c41e09..00000000 --- a/app/api/create-portal-link/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { cookies } from 'next/headers'; -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'; -import { stripe } from '@/utils/stripe'; -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({cookies}); - const { - data: { user } - } = await supabase.auth.getUser(); - - if (!user) throw Error('Could not get user'); - const customer = await createOrRetrieveCustomer({ - uuid: user.id || '', - email: user.email || '' - }); - - if (!customer) throw Error('Could not get customer'); - const { url } = await stripe.billingPortal.sessions.create({ - customer, - return_url: `${getURL()}/account` - }); - return new Response(JSON.stringify({ url }), { - status: 200 - }); - } catch (err: any) { - console.log(err); - return new Response( - JSON.stringify({ error: { statusCode: 500, message: err.message } }), - { - status: 500 - } - ); - } - } else { - return new Response('Method Not Allowed', { - headers: { Allow: 'POST' }, - status: 405 - }); - } -} diff --git a/app/api/webhooks/route.ts b/app/api/webhooks/route.ts index a5bdb963..8371d42e 100644 --- a/app/api/webhooks/route.ts +++ b/app/api/webhooks/route.ts @@ -1,16 +1,20 @@ import Stripe from 'stripe'; -import { stripe } from '@/utils/stripe'; +import { stripe } from '@/utils/stripe/config'; import { upsertProductRecord, upsertPriceRecord, - manageSubscriptionStatusChange -} from '@/utils/supabase-admin'; + manageSubscriptionStatusChange, + deleteProductRecord, + deletePriceRecord +} from '@/utils/supabase/admin'; const relevantEvents = new Set([ 'product.created', 'product.updated', + 'product.deleted', 'price.created', 'price.updated', + 'price.deleted', 'checkout.session.completed', 'customer.subscription.created', 'customer.subscription.updated', @@ -24,8 +28,10 @@ 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); + console.log(`🔔 Webhook received: ${event.type}`); } catch (err: any) { console.log(`❌ Error message: ${err.message}`); return new Response(`Webhook Error: ${err.message}`, { status: 400 }); @@ -42,6 +48,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': @@ -69,12 +81,16 @@ export async function POST(req: Request) { } catch (error) { console.log(error); return new Response( - 'Webhook handler failed. View your nextjs function logs.', + 'Webhook handler failed. View your Next.js function logs.', { status: 400 } ); } + } else { + return new Response(`Unsupported event type: ${event.type}`, { + status: 400 + }); } return new Response(JSON.stringify({ received: true })); } diff --git a/app/auth/callback/route.ts b/app/auth/callback/route.ts index 8f6ff5fa..8f4c9822 100644 --- a/app/auth/callback/route.ts +++ b/app/auth/callback/route.ts @@ -1,19 +1,36 @@ -import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs' -import { cookies } from 'next/headers' -import { NextResponse } from 'next/server' - -import type { NextRequest } from 'next/server' -import type { Database } from '@/types_db' +import { createClient } from '@/utils/supabase/server'; +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; +import { getErrorRedirect, getStatusRedirect } from '@/utils/helpers'; export async function GET(request: NextRequest) { - const requestUrl = new URL(request.url) - const code = requestUrl.searchParams.get('code') + // The `/auth/callback` route is required for the server-side auth flow implemented + // by the `@supabase/ssr` package. It exchanges an auth code for the user's session. + const requestUrl = new URL(request.url); + const code = requestUrl.searchParams.get('code'); if (code) { - const supabase = createRouteHandlerClient({ cookies }) - await supabase.auth.exchangeCodeForSession(code) + const supabase = createClient(); + + const { error } = await supabase.auth.exchangeCodeForSession(code); + + if (error) { + return NextResponse.redirect( + getErrorRedirect( + `${requestUrl.origin}/signin`, + error.name, + "Sorry, we weren't able to log you in. Please try again." + ) + ); + } } // URL to redirect to after sign in process completes - return NextResponse.redirect(requestUrl.origin) -} \ No newline at end of file + return NextResponse.redirect( + getStatusRedirect( + `${requestUrl.origin}/account`, + 'Success!', + 'You are now signed in.' + ) + ); +} diff --git a/app/auth/reset_password/route.ts b/app/auth/reset_password/route.ts new file mode 100644 index 00000000..f1b0147a --- /dev/null +++ b/app/auth/reset_password/route.ts @@ -0,0 +1,36 @@ +import { createClient } from '@/utils/supabase/server'; +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; +import { getErrorRedirect, getStatusRedirect } from '@/utils/helpers'; + +export async function GET(request: NextRequest) { + // The `/auth/callback` route is required for the server-side auth flow implemented + // by the `@supabase/ssr` package. It exchanges an auth code for the user's session. + const requestUrl = new URL(request.url); + const code = requestUrl.searchParams.get('code'); + + if (code) { + const supabase = createClient(); + + const { error } = await supabase.auth.exchangeCodeForSession(code); + + if (error) { + return NextResponse.redirect( + getErrorRedirect( + `${requestUrl.origin}/signin/forgot_password`, + error.name, + "Sorry, we weren't able to log you in. Please try again." + ) + ); + } + } + + // URL to redirect to after sign in process completes + return NextResponse.redirect( + getStatusRedirect( + `${requestUrl.origin}/signin/update_password`, + 'You are now signed in.', + 'Please enter a new password for your account.' + ) + ); +} diff --git a/app/layout.tsx b/app/layout.tsx index c793313b..e5f55716 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,7 +1,11 @@ -import SupabaseProvider from './supabase-provider'; +'use server'; + +import { Metadata } from 'next'; import Footer from '@/components/ui/Footer'; import Navbar from '@/components/ui/Navbar'; -import { PropsWithChildren } from 'react'; +import { Toaster } from '@/components/ui/Toasts/toaster'; +import { PropsWithChildren, Suspense } from 'react'; +import { getURL } from '@/utils/helpers'; import 'styles/main.css'; const meta = { @@ -10,36 +14,41 @@ const meta = { cardImage: '/og.png', robots: 'follow, index', favicon: '/favicon.ico', - url: 'https://subscription-starter.vercel.app', - type: 'website' + url: getURL() }; -export const metadata = { - title: meta.title, - description: meta.description, - cardImage: meta.cardImage, - robots: meta.robots, - favicon: meta.favicon, - url: meta.url, - type: meta.type, - openGraph: { - url: meta.url, - title: meta.title, - description: meta.description, - cardImage: meta.cardImage, - type: meta.type, - site_name: meta.title - }, - twitter: { - card: 'summary_large_image', - site: '@vercel', +export async function generateMetadata(): Promise { + return { title: meta.title, description: meta.description, - cardImage: meta.cardImage - } -}; + referrer: 'origin-when-cross-origin', + keywords: ['Vercel', 'Supabase', 'Next.js', 'Stripe', 'Subscription'], + authors: [{ name: 'Vercel', url: 'https://vercel.com/' }], + creator: 'Vercel', + publisher: 'Vercel', + robots: meta.robots, + icons: { icon: meta.favicon }, + metadataBase: new URL(meta.url), + openGraph: { + url: meta.url, + title: meta.title, + description: meta.description, + images: [meta.cardImage], + type: 'website', + siteName: meta.title + }, + twitter: { + card: 'summary_large_image', + site: '@Vercel', + creator: '@Vercel', + title: meta.title, + description: meta.description, + images: [meta.cardImage] + } + }; +} -export default function RootLayout({ +export default async function RootLayout({ // Layouts must accept a children prop. // This will be populated with nested layouts or pages children @@ -47,17 +56,17 @@ export default function RootLayout({ return ( - - {/* @ts-expect-error */} - -
- {children} -
-