├── supabase ├── seed.sql ├── .gitignore ├── config.toml └── migrations │ └── 20230530034630_init.sql ├── components ├── ui │ ├── Card │ │ ├── index.ts │ │ └── Card.tsx │ ├── Input │ │ ├── index.ts │ │ ├── Input.module.css │ │ └── Input.tsx │ ├── Button │ │ ├── index.ts │ │ ├── Button.module.css │ │ └── Button.tsx │ ├── Footer │ │ ├── index.ts │ │ └── Footer.tsx │ ├── Navbar │ │ ├── index.ts │ │ ├── Navbar.module.css │ │ ├── Navbar.tsx │ │ └── Navlinks.tsx │ ├── LogoCloud │ │ ├── index.ts │ │ └── LogoCloud.tsx │ ├── LoadingDots │ │ ├── index.ts │ │ ├── LoadingDots.tsx │ │ └── LoadingDots.module.css │ ├── AuthForms │ │ ├── Separator.tsx │ │ ├── OauthSignIn.tsx │ │ ├── UpdatePassword.tsx │ │ ├── EmailSignIn.tsx │ │ ├── ForgotPassword.tsx │ │ ├── Signup.tsx │ │ └── PasswordSignIn.tsx │ ├── AccountForms │ │ ├── NameForm.tsx │ │ ├── EmailForm.tsx │ │ └── CustomerPortalForm.tsx │ ├── Toasts │ │ ├── toaster.tsx │ │ ├── use-toast.ts │ │ └── toast.tsx │ └── Pricing │ │ └── Pricing.tsx └── icons │ ├── Logo.tsx │ └── GitHub.tsx ├── types_db.ts ├── public ├── demo.png ├── favicon.ico ├── vercel-deploy.png ├── architecture_diagram.png ├── stripe.svg ├── vercel.svg ├── github.svg ├── nextjs.svg ├── supabase.svg └── architecture_diagram.svg ├── app ├── opengraph-image.png ├── signin │ ├── page.tsx │ └── [id] │ │ └── page.tsx ├── page.tsx ├── layout.tsx ├── auth │ ├── callback │ │ └── route.ts │ └── reset_password │ │ └── route.ts ├── account │ └── page.tsx └── api │ └── webhooks │ └── route.ts ├── postcss.config.js ├── .prettierrc.json ├── .env.example ├── .prettierignore ├── utils ├── cn.ts ├── stripe │ ├── client.ts │ ├── config.ts │ └── server.ts ├── supabase │ ├── client.ts │ ├── queries.ts │ ├── server.ts │ ├── middleware.ts │ └── admin.ts ├── auth-helpers │ ├── client.ts │ ├── settings.ts │ └── server.ts └── helpers.ts ├── next-env.d.ts ├── components.json ├── .env.local.example ├── .gitignore ├── middleware.ts ├── tsconfig.json ├── LICENSE ├── tailwind.config.js ├── styles └── main.css ├── package.json ├── fixtures └── stripe-fixtures.json ├── schema.sql └── README.md /supabase/seed.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/Card/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Card'; 2 | -------------------------------------------------------------------------------- /components/ui/Input/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Input'; 2 | -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | .env 5 | -------------------------------------------------------------------------------- /components/ui/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Button'; 2 | -------------------------------------------------------------------------------- /components/ui/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Footer'; 2 | -------------------------------------------------------------------------------- /components/ui/Navbar/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './Navbar'; 2 | -------------------------------------------------------------------------------- /components/ui/LogoCloud/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LogoCloud'; 2 | -------------------------------------------------------------------------------- /components/ui/LoadingDots/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './LoadingDots'; 2 | -------------------------------------------------------------------------------- /types_db.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/nextjs-subscription-payments/HEAD/types_db.ts -------------------------------------------------------------------------------- /public/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/nextjs-subscription-payments/HEAD/public/demo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/nextjs-subscription-payments/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/nextjs-subscription-payments/HEAD/app/opengraph-image.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /public/vercel-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/nextjs-subscription-payments/HEAD/public/vercel-deploy.png -------------------------------------------------------------------------------- /public/architecture_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/nextjs-subscription-payments/HEAD/public/architecture_diagram.png -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "singleQuote": true, 4 | "tabWidth": 2, 5 | "trailingComma": "none" 6 | } 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SUPABASE_AUTH_EXTERNAL_GITHUB_REDIRECT_URI="http://127.0.0.1:54321/auth/v1/callback" 2 | SUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_ID= 3 | SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET= 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | .next/ 3 | .turbo/ 4 | _next/ 5 | __tmp__/ 6 | dist/ 7 | node_modules/ 8 | target/ 9 | compiled/ 10 | 11 | pnpm-lock.yaml 12 | 13 | types_db.ts 14 | -------------------------------------------------------------------------------- /utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /components/ui/Input/Input.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply bg-black py-2 px-3 w-full appearance-none transition duration-150 ease-in-out border border-zinc-500 text-zinc-200; 3 | } 4 | 5 | .root:focus { 6 | @apply outline-none; 7 | } 8 | -------------------------------------------------------------------------------- /components/ui/LoadingDots/LoadingDots.tsx: -------------------------------------------------------------------------------- 1 | import s from './LoadingDots.module.css'; 2 | 3 | const LoadingDots = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default LoadingDots; 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "styles/main.css", 9 | "baseColor": "zinc", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/utils/cn" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /utils/stripe/client.ts: -------------------------------------------------------------------------------- 1 | import { loadStripe, Stripe } from '@stripe/stripe-js'; 2 | 3 | let stripePromise: Promise; 4 | 5 | export const getStripe = () => { 6 | if (!stripePromise) { 7 | stripePromise = loadStripe( 8 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_LIVE ?? 9 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? 10 | '' 11 | ); 12 | } 13 | 14 | return stripePromise; 15 | }; 16 | -------------------------------------------------------------------------------- /app/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from 'next/navigation'; 2 | import { getDefaultSignInView } from '@/utils/auth-helpers/settings'; 3 | import { cookies } from 'next/headers'; 4 | 5 | export default function SignIn() { 6 | const preferredSignInView = 7 | cookies().get('preferredSignInView')?.value || null; 8 | const defaultView = getDefaultSignInView(preferredSignInView); 9 | 10 | return redirect(`/signin/${defaultView}`); 11 | } 12 | -------------------------------------------------------------------------------- /utils/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from '@supabase/ssr'; 2 | import { Database } from '@/types_db'; 3 | 4 | // Define a function to create a Supabase client for client-side operations 5 | export const createClient = () => 6 | createBrowserClient( 7 | // Pass Supabase URL and anonymous key from the environment to the client 8 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 9 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! 10 | ); 11 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SITE_URL="http://localhost:3000" 2 | 3 | # These environment variables are used for Supabase Local Dev 4 | NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0" 5 | NEXT_PUBLIC_SUPABASE_URL="http://127.0.0.1:54321" 6 | SUPABASE_SERVICE_ROLE_KEY= 7 | 8 | # Get these from Stripe dashboard 9 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= 10 | STRIPE_SECRET_KEY= 11 | STRIPE_WEBHOOK_SECRET= 12 | -------------------------------------------------------------------------------- /components/ui/AuthForms/Separator.tsx: -------------------------------------------------------------------------------- 1 | interface SeparatorProps { 2 | text: string; 3 | } 4 | 5 | export default function Separator({ text }: SeparatorProps) { 6 | return ( 7 |
8 |
9 |
10 | 11 | {text} 12 | 13 |
14 |
15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/ui/Navbar/Navbar.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply sticky top-0 bg-black z-40 transition-all duration-150 h-16 md:h-20; 3 | } 4 | 5 | .link { 6 | @apply inline-flex items-center leading-6 font-medium transition ease-in-out duration-75 cursor-pointer text-zinc-200 rounded-md p-1; 7 | } 8 | 9 | .link:hover { 10 | @apply text-zinc-100; 11 | } 12 | 13 | .link:focus { 14 | @apply outline-none text-zinc-100 ring-2 ring-pink-500 ring-opacity-50; 15 | } 16 | 17 | .logo { 18 | @apply cursor-pointer rounded-full transform duration-100 ease-in-out; 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/Logo.tsx: -------------------------------------------------------------------------------- 1 | const Logo = ({ ...props }) => ( 2 | 10 | 11 | 17 | 18 | ); 19 | 20 | export default Logo; 21 | -------------------------------------------------------------------------------- /components/ui/Navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { createClient } from '@/utils/supabase/server'; 2 | import s from './Navbar.module.css'; 3 | import Navlinks from './Navlinks'; 4 | 5 | export default async function Navbar() { 6 | const supabase = createClient(); 7 | 8 | const { 9 | data: { user } 10 | } = await supabase.auth.getUser(); 11 | 12 | return ( 13 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # editors 38 | .vscode 39 | 40 | # certificates 41 | certificates 42 | .env*.local 43 | -------------------------------------------------------------------------------- /utils/stripe/config.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | 3 | export const stripe = new Stripe( 4 | process.env.STRIPE_SECRET_KEY_LIVE ?? process.env.STRIPE_SECRET_KEY ?? '', 5 | { 6 | // https://github.com/stripe/stripe-node#configuration 7 | // https://stripe.com/docs/api/versioning 8 | // @ts-ignore 9 | apiVersion: null, 10 | // Register this as an official Stripe plugin. 11 | // https://stripe.com/docs/building-plugins#setappinfo 12 | appInfo: { 13 | name: 'Next.js Subscription Starter', 14 | version: '0.0.0', 15 | url: 'https://github.com/vercel/nextjs-subscription-payments' 16 | } 17 | } 18 | ); 19 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Pricing from '@/components/ui/Pricing/Pricing'; 2 | import { createClient } from '@/utils/supabase/server'; 3 | import { 4 | getProducts, 5 | getSubscription, 6 | getUser 7 | } from '@/utils/supabase/queries'; 8 | 9 | export default async function PricingPage() { 10 | const supabase = createClient(); 11 | const [user, products, subscription] = await Promise.all([ 12 | getUser(supabase), 13 | getProducts(supabase), 14 | getSubscription(supabase) 15 | ]); 16 | 17 | return ( 18 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/ui/LoadingDots/LoadingDots.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply inline-flex text-center items-center leading-7; 3 | } 4 | 5 | .root span { 6 | @apply bg-zinc-200 rounded-full h-2 w-2; 7 | animation-name: blink; 8 | animation-duration: 1.4s; 9 | animation-iteration-count: infinite; 10 | animation-fill-mode: both; 11 | margin: 0 2px; 12 | } 13 | 14 | .root span:nth-of-type(2) { 15 | animation-delay: 0.2s; 16 | } 17 | 18 | .root span:nth-of-type(3) { 19 | animation-delay: 0.4s; 20 | } 21 | 22 | @keyframes blink { 23 | 0% { 24 | opacity: 0.2; 25 | } 26 | 20% { 27 | opacity: 1; 28 | } 29 | 100% { 30 | opacity: 0.2; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from 'next/server'; 2 | import { updateSession } from '@/utils/supabase/middleware'; 3 | 4 | export async function middleware(request: NextRequest) { 5 | return await updateSession(request); 6 | } 7 | 8 | export const config = { 9 | matcher: [ 10 | /* 11 | * Match all request paths except: 12 | * - _next/static (static files) 13 | * - _next/image (image optimization files) 14 | * - favicon.ico (favicon file) 15 | * - images - .svg, .png, .jpg, .jpeg, .gif, .webp 16 | * Feel free to modify this pattern to include more paths. 17 | */ 18 | '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)' 19 | ] 20 | }; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./*"] 19 | }, 20 | "incremental": true, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /components/ui/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | interface Props { 4 | title: string; 5 | description?: string; 6 | footer?: ReactNode; 7 | children: ReactNode; 8 | } 9 | 10 | export default function Card({ title, description, footer, children }: Props) { 11 | return ( 12 |
13 |
14 |

{title}

15 |

{description}

16 | {children} 17 |
18 | {footer && ( 19 |
20 | {footer} 21 |
22 | )} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/ui/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply bg-white text-zinc-800 cursor-pointer inline-flex px-10 rounded-sm leading-6 transition ease-in-out duration-150 shadow-sm font-semibold text-center justify-center uppercase py-4 border border-transparent items-center; 3 | } 4 | 5 | .root:hover { 6 | @apply bg-zinc-800 text-white border border-white; 7 | } 8 | 9 | .root:focus { 10 | @apply outline-none ring-2 ring-pink-500 ring-opacity-50; 11 | } 12 | 13 | .root[data-active] { 14 | @apply bg-zinc-600; 15 | } 16 | 17 | .loading { 18 | @apply bg-zinc-700 text-zinc-500 border-zinc-600 cursor-not-allowed; 19 | } 20 | 21 | .slim { 22 | @apply py-2 transform-none normal-case; 23 | } 24 | 25 | .disabled, 26 | .disabled:hover { 27 | @apply text-zinc-400 border-zinc-600 bg-zinc-700 cursor-not-allowed; 28 | filter: grayscale(1); 29 | -webkit-transform: translateZ(0); 30 | -webkit-perspective: 1000; 31 | -webkit-backface-visibility: hidden; 32 | } 33 | -------------------------------------------------------------------------------- /components/ui/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { InputHTMLAttributes, ChangeEvent } from 'react'; 2 | import cn from 'classnames'; 3 | 4 | import s from './Input.module.css'; 5 | 6 | interface Props extends Omit, 'onChange'> { 7 | className?: string; 8 | onChange: (value: string) => void; 9 | } 10 | const Input = (props: Props) => { 11 | const { className, children, onChange, ...rest } = props; 12 | 13 | const rootClassName = cn(s.root, {}, className); 14 | 15 | const handleOnChange = (e: ChangeEvent) => { 16 | if (onChange) { 17 | onChange(e.target.value); 18 | } 19 | return null; 20 | }; 21 | 22 | return ( 23 | 34 | ); 35 | }; 36 | 37 | export default Input; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme'); 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: ['class', '[data-theme="dark"]'], 6 | content: [ 7 | 'app/**/*.{ts,tsx}', 8 | 'components/**/*.{ts,tsx}', 9 | 'pages/**/*.{ts,tsx}' 10 | ], 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: '2rem', 15 | screens: { 16 | '2xl': '1400px' 17 | } 18 | }, 19 | extend: { 20 | fontFamily: { 21 | sans: ['var(--font-sans)', ...fontFamily.sans] 22 | }, 23 | keyframes: { 24 | 'accordion-down': { 25 | from: { height: 0 }, 26 | to: { height: 'var(--radix-accordion-content-height)' } 27 | }, 28 | 'accordion-up': { 29 | from: { height: 'var(--radix-accordion-content-height)' }, 30 | to: { height: 0 } 31 | } 32 | }, 33 | animation: { 34 | 'accordion-down': 'accordion-down 0.2s ease-out', 35 | 'accordion-up': 'accordion-up 0.2s ease-out' 36 | } 37 | } 38 | }, 39 | plugins: [require('tailwindcss-animate')] 40 | }; 41 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import Footer from '@/components/ui/Footer'; 3 | import Navbar from '@/components/ui/Navbar'; 4 | import { Toaster } from '@/components/ui/Toasts/toaster'; 5 | import { PropsWithChildren, Suspense } from 'react'; 6 | import { getURL } from '@/utils/helpers'; 7 | import 'styles/main.css'; 8 | 9 | const title = 'Next.js Subscription Starter'; 10 | const description = 'Brought to you by Vercel, Stripe, and Supabase.'; 11 | 12 | export const metadata: Metadata = { 13 | metadataBase: new URL(getURL()), 14 | title: title, 15 | description: description, 16 | openGraph: { 17 | title: title, 18 | description: description 19 | } 20 | }; 21 | 22 | export default async function RootLayout({ children }: PropsWithChildren) { 23 | return ( 24 | 25 | 26 | 27 |
31 | {children} 32 |
33 |