[
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# vercel\n.vercel\n\n# editors\n.vscode\n\n# certificates\ncertificates\n.env*.local\n"
  },
  {
    "path": ".prettierignore",
    "content": "# Build artifacts\n.next/\n.turbo/\n_next/\n__tmp__/\ndist/\nnode_modules/\ntarget/\ncompiled/\n\npnpm-lock.yaml\n\ntypes_db.ts\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{\n  \"arrowParens\": \"always\",\n  \"singleQuote\": true,\n  \"tabWidth\": 2,\n  \"trailingComma\": \"none\"\n}\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2024 Vercel, Inc.\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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."
  },
  {
    "path": "README.md",
    "content": "# Next.js Subscription Payments Starter\n\n\n> [!WARNING]  \n> This repo has been sunset and replaced by a new template: https://github.com/nextjs/saas-starter\n\n## Features\n\n- Secure user management and authentication with [Supabase](https://supabase.io/docs/guides/auth)\n- Powerful data access & management tooling on top of PostgreSQL with [Supabase](https://supabase.io/docs/guides/database)\n- Integration with [Stripe Checkout](https://stripe.com/docs/payments/checkout) and the [Stripe customer portal](https://stripe.com/docs/billing/subscriptions/customer-portal)\n- Automatic syncing of pricing plans and subscription statuses via [Stripe webhooks](https://stripe.com/docs/webhooks)\n\n## Demo\n\n- https://subscription-payments.vercel.app/\n\n[![Screenshot of demo](./public/demo.png)](https://subscription-payments.vercel.app/)\n\n## Architecture\n\n![Architecture diagram](./public/architecture_diagram.png)\n\n## Step-by-step setup\n\nWhen deploying this template, the sequence of steps is important. Follow the steps below in order to get up and running.\n\n### Initiate Deployment\n\n#### Vercel Deploy Button\n\n[![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)\n\nThe 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).\n\nShould 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.\n\n### Configure Auth\n\nFollow [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.\n\nIn 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.\n\nNext, 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.\n\n#### [Optional] - Set up redirect wildcards for deploy previews (not needed if you installed via the Deploy Button)\n\nIf you've deployed this template via the \"Deploy to Vercel\" button above, you can skip this step. The Supabase Vercel Integration will have set redirect wildcards for you. You can check this by going to your Supabase [auth settings](https://app.supabase.com/project/_/auth/url-configuration) and you should see a list of redirects under \"Redirect URLs\".\n\nOtherwise, 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).\n\nIf 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.\n\nOtherwise, 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.\n\n#### [Maybe Optional] - Set up Supabase environment variables (not needed if you installed via the Deploy Button)\n\nIf you've deployed this template via the \"Deploy to Vercel\" button above, you can skip this step. The Supabase Vercel Integration will have set your environment variables for you. You can check this by going to your Vercel project settings, and clicking on 'Environment variables', there will be a list of environment variables with the Supabase icon displayed next to them.\n\nOtherwise navigate to the [API settings](https://app.supabase.com/project/_/settings/api) and paste them into the Vercel deployment interface. Copy project API keys and paste into the `NEXT_PUBLIC_SUPABASE_ANON_KEY` and `SUPABASE_SERVICE_ROLE_KEY` fields, and copy the project URL and paste to Vercel as `NEXT_PUBLIC_SUPABASE_URL`.\n\nCongrats, this completes the Supabase setup, almost there!\n\n### Configure Stripe\n\nNext, we'll need to configure [Stripe](https://stripe.com/) to handle test payments. If you don't already have a Stripe account, create one now.\n\nFor the following steps, make sure you have the [\"Test Mode\" toggle](https://stripe.com/docs/testing) switched on.\n\n#### Create a Webhook\n\nWe 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.\n\n1. Click the \"Add Endpoint\" button on the [test Endpoints page](https://dashboard.stripe.com/test/webhooks).\n1. Enter your production deployment URL followed by `/api/webhooks` for the endpoint URL. (e.g. `https://your-deployment-url.vercel.app/api/webhooks`)\n1. Click `Select events` under the `Select events to listen to` heading.\n1. Click `Select all events` in the `Select events to send` section.\n1. 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).\n1. 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.\n\n#### Redeploy with new env vars\n\nFor the newly set environment variables to take effect and everything to work together correctly, we need to redeploy our app in Vercel. In your Vercel Dashboard, navigate to deployments, click the overflow menu button and select \"Redeploy\" (do NOT enable the \"Use existing Build Cache\" option). Once Vercel has rebuilt and redeployed your app, you're ready to set up your products and prices.\n\n#### Create product and pricing information\n\nYour application's webhook listens for product updates on Stripe and automatically propagates them to your Supabase database. So with your webhook listener running, you can now create your product and pricing information in the [Stripe Dashboard](https://dashboard.stripe.com/test/products).\n\nStripe Checkout currently supports pricing that bills a predefined amount at a specific interval. More complex plans (e.g., different pricing tiers or seats) are not yet supported.\n\nFor example, you can create business models with different pricing tiers, e.g.:\n\n- Product 1: Hobby\n  - Price 1: 10 USD per month\n  - Price 2: 100 USD per year\n- Product 2: Freelancer\n  - Price 1: 20 USD per month\n  - Price 2: 200 USD per year\n\nOptionally, to speed up the setup, we have added a [fixtures file](fixtures/stripe-fixtures.json) to bootstrap test product and pricing data in your Stripe account. The [Stripe CLI](https://stripe.com/docs/stripe-cli#install) `fixtures` command executes a series of API requests defined in this JSON file. Simply run `stripe fixtures fixtures/stripe-fixtures.json`.\n\n**Important:** Make sure that you've configured your Stripe webhook correctly and redeployed with all needed environment variables.\n\n#### Configure the Stripe customer portal\n\n1. Set your custom branding in the [settings](https://dashboard.stripe.com/settings/branding)\n1. Configure the Customer Portal [settings](https://dashboard.stripe.com/test/settings/billing/portal)\n1. Toggle on \"Allow customers to update their payment methods\"\n1. Toggle on \"Allow customers to update subscriptions\"\n1. Toggle on \"Allow customers to cancel subscriptions\"\n1. Add the products and prices that you want\n1. Set up the required business information and links\n\n### That's it\n\nI know, that was quite a lot to get through, but it's worth it. You're now ready to earn recurring revenue from your customers. 🥳\n\n## Develop locally\n\nIf you haven't already done so, clone your Github repository to your local machine.\n\n### Install dependencies\n\nEnsure you have [pnpm](https://pnpm.io/installation) installed and run:\n\n```bash\npnpm install\n```\n\nNext, use the [Vercel CLI](https://vercel.com/docs/cli) to link your project:\n\n```bash\npnpm dlx vercel login\npnpm dlx vercel link\n```\n\n`pnpm dlx` runs a package from the registry, without installing it as a dependency. Alternatively, you can install these packages globally, and drop the `pnpm dlx` part.\n\nIf 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:\n\n```bash\npnpm dlx vercel env pull .env.local\n```\n\nRunning 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 `--linked' or '--project-id <string>' in the `supabase:generate-types` script in `package.json`.(see -> [https://supabase.com/docs/reference/cli/supabase-gen-types-typescript])\n\n### Local development with Supabase\n\nIt'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`.\n\nFirst, you will need to install [Docker](https://www.docker.com/get-started/). You should also copy or rename:\n\n- `.env.local.example` -> `.env.local`\n- `.env.example` -> `.env`\n\nNext, run the following command to start a local Supabase instance and run the migrations to set up the database schema:\n\n```bash\npnpm supabase:start\n```\n\nThe terminal output will provide you with URLs to access the different services within the Supabase stack. The Supabase Studio is where you can make changes to your local database instance.\n\nCopy the value for the `service_role_key` and paste it as the value for the `SUPABASE_SERVICE_ROLE_KEY` in your `.env.local` file.\n\nYou can print out these URLs at any time with the following command:\n\n```bash\npnpm supabase:status\n```\n\nTo link your local Supabase instance to your project, run the following command, navigate to the Supabase project you created above, and enter your database password.\n\n```bash\npnpm supabase:link\n```\n\nIf you need to reset your database password, head over to [your database settings](https://supabase.com/dashboard/project/_/settings/database) and click \"Reset database password\", and this time copy it across to a password manager! 😄\n\n🚧 Warning: This links our Local Development instance to the project we are using for `production`. Currently, it only has test records, but once it has customer data, we recommend using [Branching](https://supabase.com/docs/guides/platform/branching) or manually creating a separate `preview` or `staging` environment, to ensure your customer's data is not used locally, and schema changes/migrations can be thoroughly tested before shipping to `production`.\n\nOnce you've linked your project, you can pull down any schema changes you made in your remote database with:\n\n```bash\npnpm supabase:pull\n```\n\nYou can seed your local database with any data you added in your remote database with:\n\n```bash\npnpm supabase:generate-seed\npnpm supabase:reset\n```\n\n🚧 Warning: this is seeding data from the `production` database. Currently, this only contains test data, but we recommend using [Branching](https://supabase.com/docs/guides/platform/branching) or manually setting up a `preview` or `staging` environment once this contains real customer data.\n\nYou 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:\n\n```bash\npnpm supabase:generate-types\n```\n\nYou can also automatically generate a migration file with all the changes you've made to your local database schema with the following command:\n\n```bash\npnpm supabase:generate-migration\n```\n\nAnd push those changes to your remote database with:\n\n```bash\npnpm supabase:push\n```\n\nRemember to test your changes thoroughly in your `local` and `staging` or `preview` environments before deploying them to `production`!\n\n### Use the Stripe CLI to test webhooks\n\nUse the [Stripe CLI](https://stripe.com/docs/stripe-cli) to [login to your Stripe account](https://stripe.com/docs/stripe-cli#login-account):\n\n```bash\npnpm stripe:login\n```\n\nThis will print a URL to navigate to in your browser and provide access to your Stripe account.\n\nNext, start local webhook forwarding:\n\n```bash\npnpm stripe:listen\n```\n\nRunning 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.\n\n### Run the Next.js client\n\nIn a separate terminal, run the following command to start the development server:\n\n```bash\npnpm dev\n```\n\nNote that webhook forwarding and the development server must be running concurrently in two separate terminals for the application to work correctly.\n\nFinally, navigate to [http://localhost:3000](http://localhost:3000) in your browser to see the application rendered.\n\n## Going live\n\n### Archive testing products\n\nArchive all test mode Stripe products before going live. Before creating your live mode products, make sure to follow the steps below to set up your live mode env vars and webhooks.\n\n### Configure production environment variables\n\nTo run the project in live mode and process payments with Stripe, switch Stripe from \"test mode\" to \"production mode.\" Your Stripe API keys will be different in production mode, and you will have to create a separate production mode webhook. Copy these values and paste them into Vercel, replacing the test mode values.\n\n### Redeploy\n\nAfterward, 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).\n\nTo 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.\n"
  },
  {
    "path": "app/account/page.tsx",
    "content": "import CustomerPortalForm from '@/components/ui/AccountForms/CustomerPortalForm';\nimport EmailForm from '@/components/ui/AccountForms/EmailForm';\nimport NameForm from '@/components/ui/AccountForms/NameForm';\nimport { redirect } from 'next/navigation';\nimport { createClient } from '@/utils/supabase/server';\nimport {\n  getUserDetails,\n  getSubscription,\n  getUser\n} from '@/utils/supabase/queries';\n\nexport default async function Account() {\n  const supabase = createClient();\n  const [user, userDetails, subscription] = await Promise.all([\n    getUser(supabase),\n    getUserDetails(supabase),\n    getSubscription(supabase)\n  ]);\n\n  if (!user) {\n    return redirect('/signin');\n  }\n\n  return (\n    <section className=\"mb-32 bg-black\">\n      <div className=\"max-w-6xl px-4 py-8 mx-auto sm:px-6 sm:pt-24 lg:px-8\">\n        <div className=\"sm:align-center sm:flex sm:flex-col\">\n          <h1 className=\"text-4xl font-extrabold text-white sm:text-center sm:text-6xl\">\n            Account\n          </h1>\n          <p className=\"max-w-2xl m-auto mt-5 text-xl text-zinc-200 sm:text-center sm:text-2xl\">\n            We partnered with Stripe for a simplified billing.\n          </p>\n        </div>\n      </div>\n      <div className=\"p-4\">\n        <CustomerPortalForm subscription={subscription} />\n        <NameForm userName={userDetails?.full_name ?? ''} />\n        <EmailForm userEmail={user.email} />\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "app/api/webhooks/route.ts",
    "content": "import Stripe from 'stripe';\nimport { stripe } from '@/utils/stripe/config';\nimport {\n  upsertProductRecord,\n  upsertPriceRecord,\n  manageSubscriptionStatusChange,\n  deleteProductRecord,\n  deletePriceRecord\n} from '@/utils/supabase/admin';\n\nconst relevantEvents = new Set([\n  'product.created',\n  'product.updated',\n  'product.deleted',\n  'price.created',\n  'price.updated',\n  'price.deleted',\n  'checkout.session.completed',\n  'customer.subscription.created',\n  'customer.subscription.updated',\n  'customer.subscription.deleted'\n]);\n\nexport async function POST(req: Request) {\n  const body = await req.text();\n  const sig = req.headers.get('stripe-signature') as string;\n  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;\n  let event: Stripe.Event;\n\n  try {\n    if (!sig || !webhookSecret)\n      return new Response('Webhook secret not found.', { status: 400 });\n    event = stripe.webhooks.constructEvent(body, sig, webhookSecret);\n    console.log(`🔔  Webhook received: ${event.type}`);\n  } catch (err: any) {\n    console.log(`❌ Error message: ${err.message}`);\n    return new Response(`Webhook Error: ${err.message}`, { status: 400 });\n  }\n\n  if (relevantEvents.has(event.type)) {\n    try {\n      switch (event.type) {\n        case 'product.created':\n        case 'product.updated':\n          await upsertProductRecord(event.data.object as Stripe.Product);\n          break;\n        case 'price.created':\n        case 'price.updated':\n          await upsertPriceRecord(event.data.object as Stripe.Price);\n          break;\n        case 'price.deleted':\n          await deletePriceRecord(event.data.object as Stripe.Price);\n          break;\n        case 'product.deleted':\n          await deleteProductRecord(event.data.object as Stripe.Product);\n          break;\n        case 'customer.subscription.created':\n        case 'customer.subscription.updated':\n        case 'customer.subscription.deleted':\n          const subscription = event.data.object as Stripe.Subscription;\n          await manageSubscriptionStatusChange(\n            subscription.id,\n            subscription.customer as string,\n            event.type === 'customer.subscription.created'\n          );\n          break;\n        case 'checkout.session.completed':\n          const checkoutSession = event.data.object as Stripe.Checkout.Session;\n          if (checkoutSession.mode === 'subscription') {\n            const subscriptionId = checkoutSession.subscription;\n            await manageSubscriptionStatusChange(\n              subscriptionId as string,\n              checkoutSession.customer as string,\n              true\n            );\n          }\n          break;\n        default:\n          throw new Error('Unhandled relevant event!');\n      }\n    } catch (error) {\n      console.log(error);\n      return new Response(\n        'Webhook handler failed. View your Next.js function logs.',\n        {\n          status: 400\n        }\n      );\n    }\n  } else {\n    return new Response(`Unsupported event type: ${event.type}`, {\n      status: 400\n    });\n  }\n  return new Response(JSON.stringify({ received: true }));\n}\n"
  },
  {
    "path": "app/auth/callback/route.ts",
    "content": "import { createClient } from '@/utils/supabase/server';\nimport { NextResponse } from 'next/server';\nimport { NextRequest } from 'next/server';\nimport { getErrorRedirect, getStatusRedirect } from '@/utils/helpers';\n\nexport async function GET(request: NextRequest) {\n  // The `/auth/callback` route is required for the server-side auth flow implemented\n  // by the `@supabase/ssr` package. It exchanges an auth code for the user's session.\n  const requestUrl = new URL(request.url);\n  const code = requestUrl.searchParams.get('code');\n\n  if (code) {\n    const supabase = createClient();\n\n    const { error } = await supabase.auth.exchangeCodeForSession(code);\n\n    if (error) {\n      return NextResponse.redirect(\n        getErrorRedirect(\n          `${requestUrl.origin}/signin`,\n          error.name,\n          \"Sorry, we weren't able to log you in. Please try again.\"\n        )\n      );\n    }\n  }\n\n  // URL to redirect to after sign in process completes\n  return NextResponse.redirect(\n    getStatusRedirect(\n      `${requestUrl.origin}/account`,\n      'Success!',\n      'You are now signed in.'\n    )\n  );\n}\n"
  },
  {
    "path": "app/auth/reset_password/route.ts",
    "content": "import { createClient } from '@/utils/supabase/server';\nimport { NextResponse } from 'next/server';\nimport { NextRequest } from 'next/server';\nimport { getErrorRedirect, getStatusRedirect } from '@/utils/helpers';\n\nexport async function GET(request: NextRequest) {\n  // The `/auth/callback` route is required for the server-side auth flow implemented\n  // by the `@supabase/ssr` package. It exchanges an auth code for the user's session.\n  const requestUrl = new URL(request.url);\n  const code = requestUrl.searchParams.get('code');\n\n  if (code) {\n    const supabase = createClient();\n\n    const { error } = await supabase.auth.exchangeCodeForSession(code);\n\n    if (error) {\n      return NextResponse.redirect(\n        getErrorRedirect(\n          `${requestUrl.origin}/signin/forgot_password`,\n          error.name,\n          \"Sorry, we weren't able to log you in. Please try again.\"\n        )\n      );\n    }\n  }\n\n  // URL to redirect to after sign in process completes\n  return NextResponse.redirect(\n    getStatusRedirect(\n      `${requestUrl.origin}/signin/update_password`,\n      'You are now signed in.',\n      'Please enter a new password for your account.'\n    )\n  );\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import { Metadata } from 'next';\nimport Footer from '@/components/ui/Footer';\nimport Navbar from '@/components/ui/Navbar';\nimport { Toaster } from '@/components/ui/Toasts/toaster';\nimport { PropsWithChildren, Suspense } from 'react';\nimport { getURL } from '@/utils/helpers';\nimport 'styles/main.css';\n\nconst title = 'Next.js Subscription Starter';\nconst description = 'Brought to you by Vercel, Stripe, and Supabase.';\n\nexport const metadata: Metadata = {\n  metadataBase: new URL(getURL()),\n  title: title,\n  description: description,\n  openGraph: {\n    title: title,\n    description: description\n  }\n};\n\nexport default async function RootLayout({ children }: PropsWithChildren) {\n  return (\n    <html lang=\"en\">\n      <body className=\"bg-black\">\n        <Navbar />\n        <main\n          id=\"skip\"\n          className=\"min-h-[calc(100dvh-4rem)] md:min-h[calc(100dvh-5rem)]\"\n        >\n          {children}\n        </main>\n        <Footer />\n        <Suspense>\n          <Toaster />\n        </Suspense>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/page.tsx",
    "content": "import Pricing from '@/components/ui/Pricing/Pricing';\nimport { createClient } from '@/utils/supabase/server';\nimport {\n  getProducts,\n  getSubscription,\n  getUser\n} from '@/utils/supabase/queries';\n\nexport default async function PricingPage() {\n  const supabase = createClient();\n  const [user, products, subscription] = await Promise.all([\n    getUser(supabase),\n    getProducts(supabase),\n    getSubscription(supabase)\n  ]);\n\n  return (\n    <Pricing\n      user={user}\n      products={products ?? []}\n      subscription={subscription}\n    />\n  );\n}\n"
  },
  {
    "path": "app/signin/[id]/page.tsx",
    "content": "import Logo from '@/components/icons/Logo';\nimport { createClient } from '@/utils/supabase/server';\nimport { cookies } from 'next/headers';\nimport { redirect } from 'next/navigation';\nimport {\n  getAuthTypes,\n  getViewTypes,\n  getDefaultSignInView,\n  getRedirectMethod\n} from '@/utils/auth-helpers/settings';\nimport Card from '@/components/ui/Card';\nimport PasswordSignIn from '@/components/ui/AuthForms/PasswordSignIn';\nimport EmailSignIn from '@/components/ui/AuthForms/EmailSignIn';\nimport Separator from '@/components/ui/AuthForms/Separator';\nimport OauthSignIn from '@/components/ui/AuthForms/OauthSignIn';\nimport ForgotPassword from '@/components/ui/AuthForms/ForgotPassword';\nimport UpdatePassword from '@/components/ui/AuthForms/UpdatePassword';\nimport SignUp from '@/components/ui/AuthForms/Signup';\n\nexport default async function SignIn({\n  params,\n  searchParams\n}: {\n  params: { id: string };\n  searchParams: { disable_button: boolean };\n}) {\n  const { allowOauth, allowEmail, allowPassword } = getAuthTypes();\n  const viewTypes = getViewTypes();\n  const redirectMethod = getRedirectMethod();\n\n  // Declare 'viewProp' and initialize with the default value\n  let viewProp: string;\n\n  // Assign url id to 'viewProp' if it's a valid string and ViewTypes includes it\n  if (typeof params.id === 'string' && viewTypes.includes(params.id)) {\n    viewProp = params.id;\n  } else {\n    const preferredSignInView =\n      cookies().get('preferredSignInView')?.value || null;\n    viewProp = getDefaultSignInView(preferredSignInView);\n    return redirect(`/signin/${viewProp}`);\n  }\n\n  // Check if the user is already logged in and redirect to the account page if so\n  const supabase = createClient();\n\n  const {\n    data: { user }\n  } = await supabase.auth.getUser();\n\n  if (user && viewProp !== 'update_password') {\n    return redirect('/');\n  } else if (!user && viewProp === 'update_password') {\n    return redirect('/signin');\n  }\n\n  return (\n    <div className=\"flex justify-center height-screen-helper\">\n      <div className=\"flex flex-col justify-between max-w-lg p-3 m-auto w-80 \">\n        <div className=\"flex justify-center pb-12 \">\n          <Logo width=\"64px\" height=\"64px\" />\n        </div>\n        <Card\n          title={\n            viewProp === 'forgot_password'\n              ? 'Reset Password'\n              : viewProp === 'update_password'\n                ? 'Update Password'\n                : viewProp === 'signup'\n                  ? 'Sign Up'\n                  : 'Sign In'\n          }\n        >\n          {viewProp === 'password_signin' && (\n            <PasswordSignIn\n              allowEmail={allowEmail}\n              redirectMethod={redirectMethod}\n            />\n          )}\n          {viewProp === 'email_signin' && (\n            <EmailSignIn\n              allowPassword={allowPassword}\n              redirectMethod={redirectMethod}\n              disableButton={searchParams.disable_button}\n            />\n          )}\n          {viewProp === 'forgot_password' && (\n            <ForgotPassword\n              allowEmail={allowEmail}\n              redirectMethod={redirectMethod}\n              disableButton={searchParams.disable_button}\n            />\n          )}\n          {viewProp === 'update_password' && (\n            <UpdatePassword redirectMethod={redirectMethod} />\n          )}\n          {viewProp === 'signup' && (\n            <SignUp allowEmail={allowEmail} redirectMethod={redirectMethod} />\n          )}\n          {viewProp !== 'update_password' &&\n            viewProp !== 'signup' &&\n            allowOauth && (\n              <>\n                <Separator text=\"Third-party sign-in\" />\n                <OauthSignIn />\n              </>\n            )}\n        </Card>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/signin/page.tsx",
    "content": "import { redirect } from 'next/navigation';\nimport { getDefaultSignInView } from '@/utils/auth-helpers/settings';\nimport { cookies } from 'next/headers';\n\nexport default function SignIn() {\n  const preferredSignInView =\n    cookies().get('preferredSignInView')?.value || null;\n  const defaultView = getDefaultSignInView(preferredSignInView);\n\n  return redirect(`/signin/${defaultView}`);\n}\n"
  },
  {
    "path": "components/icons/GitHub.tsx",
    "content": "const GitHub = ({ ...props }) => {\n  return (\n    <svg\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      {...props}\n    >\n      <path\n        fillRule=\"evenodd\"\n        clipRule=\"evenodd\"\n        d=\"M12 0C5.37 0 0 5.50583 0 12.3035C0 17.7478 3.435 22.3463 8.205 23.9765C8.805 24.0842 9.03 23.715 9.03 23.3921C9.03 23.0999 9.015 22.131 9.015 21.1005C6 21.6696 5.22 20.347 4.98 19.6549C4.845 19.3012 4.26 18.2092 3.75 17.917C3.33 17.6863 2.73 17.1173 3.735 17.1019C4.68 17.0865 5.355 17.9939 5.58 18.363C6.66 20.2239 8.385 19.701 9.075 19.3781C9.18 18.5783 9.495 18.04 9.84 17.7325C7.17 17.4249 4.38 16.3637 4.38 11.6576C4.38 10.3196 4.845 9.21227 5.61 8.35102C5.49 8.04343 5.07 6.78232 5.73 5.09058C5.73 5.09058 6.735 4.76762 9.03 6.3517C9.99 6.07487 11.01 5.93645 12.03 5.93645C13.05 5.93645 14.07 6.07487 15.03 6.3517C17.325 4.75224 18.33 5.09058 18.33 5.09058C18.99 6.78232 18.57 8.04343 18.45 8.35102C19.215 9.21227 19.68 10.3042 19.68 11.6576C19.68 16.3791 16.875 17.4249 14.205 17.7325C14.64 18.1169 15.015 18.8552 15.015 20.0086C15.015 21.6542 15 22.9768 15 23.3921C15 23.715 15.225 24.0995 15.825 23.9765C18.2072 23.1519 20.2773 21.5822 21.7438 19.4882C23.2103 17.3942 23.9994 14.8814 24 12.3035C24 5.50583 18.63 0 12 0Z\"\n        fill=\"currentColor\"\n      />\n    </svg>\n  );\n};\n\nexport default GitHub;\n"
  },
  {
    "path": "components/icons/Logo.tsx",
    "content": "const Logo = ({ ...props }) => (\n  <svg\n    width=\"32\"\n    height=\"32\"\n    viewBox=\"0 0 32 32\"\n    fill=\"none\"\n    xmlns=\"http://www.w3.org/2000/svg\"\n    {...props}\n  >\n    <rect width=\"100%\" height=\"100%\" rx=\"16\" fill=\"white\" />\n    <path\n      fillRule=\"evenodd\"\n      clipRule=\"evenodd\"\n      d=\"M17.6482 10.1305L15.8785 7.02583L7.02979 22.5499H10.5278L17.6482 10.1305ZM19.8798 14.0457L18.11 17.1983L19.394 19.4511H16.8453L15.1056 22.5499H24.7272L19.8798 14.0457Z\"\n      fill=\"black\"\n    />\n  </svg>\n);\n\nexport default Logo;\n"
  },
  {
    "path": "components/ui/AccountForms/CustomerPortalForm.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport { useRouter, usePathname } from 'next/navigation';\nimport { useState } from 'react';\nimport { createStripePortal } from '@/utils/stripe/server';\nimport Link from 'next/link';\nimport Card from '@/components/ui/Card';\nimport { Tables } from '@/types_db';\n\ntype Subscription = Tables<'subscriptions'>;\ntype Price = Tables<'prices'>;\ntype Product = Tables<'products'>;\n\ntype SubscriptionWithPriceAndProduct = Subscription & {\n  prices:\n    | (Price & {\n        products: Product | null;\n      })\n    | null;\n};\n\ninterface Props {\n  subscription: SubscriptionWithPriceAndProduct | null;\n}\n\nexport default function CustomerPortalForm({ subscription }: Props) {\n  const router = useRouter();\n  const currentPath = usePathname();\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const subscriptionPrice =\n    subscription &&\n    new Intl.NumberFormat('en-US', {\n      style: 'currency',\n      currency: subscription?.prices?.currency!,\n      minimumFractionDigits: 0\n    }).format((subscription?.prices?.unit_amount || 0) / 100);\n\n  const handleStripePortalRequest = async () => {\n    setIsSubmitting(true);\n    const redirectUrl = await createStripePortal(currentPath);\n    setIsSubmitting(false);\n    return router.push(redirectUrl);\n  };\n\n  return (\n    <Card\n      title=\"Your Plan\"\n      description={\n        subscription\n          ? `You are currently on the ${subscription?.prices?.products?.name} plan.`\n          : 'You are not currently subscribed to any plan.'\n      }\n      footer={\n        <div className=\"flex flex-col items-start justify-between sm:flex-row sm:items-center\">\n          <p className=\"pb-4 sm:pb-0\">Manage your subscription on Stripe.</p>\n          <Button\n            variant=\"slim\"\n            onClick={handleStripePortalRequest}\n            loading={isSubmitting}\n          >\n            Open customer portal\n          </Button>\n        </div>\n      }\n    >\n      <div className=\"mt-8 mb-4 text-xl font-semibold\">\n        {subscription ? (\n          `${subscriptionPrice}/${subscription?.prices?.interval}`\n        ) : (\n          <Link href=\"/\">Choose your plan</Link>\n        )}\n      </div>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "components/ui/AccountForms/EmailForm.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport Card from '@/components/ui/Card';\nimport { updateEmail } from '@/utils/auth-helpers/server';\nimport { handleRequest } from '@/utils/auth-helpers/client';\nimport { useRouter } from 'next/navigation';\nimport { useState } from 'react';\n\nexport default function EmailForm({\n  userEmail\n}: {\n  userEmail: string | undefined;\n}) {\n  const router = useRouter();\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n    setIsSubmitting(true);\n    // Check if the new email is the same as the old email\n    if (e.currentTarget.newEmail.value === userEmail) {\n      e.preventDefault();\n      setIsSubmitting(false);\n      return;\n    }\n    handleRequest(e, updateEmail, router);\n    setIsSubmitting(false);\n  };\n\n  return (\n    <Card\n      title=\"Your Email\"\n      description=\"Please enter the email address you want to use to login.\"\n      footer={\n        <div className=\"flex flex-col items-start justify-between sm:flex-row sm:items-center\">\n          <p className=\"pb-4 sm:pb-0\">\n            We will email you to verify the change.\n          </p>\n          <Button\n            variant=\"slim\"\n            type=\"submit\"\n            form=\"emailForm\"\n            loading={isSubmitting}\n          >\n            Update Email\n          </Button>\n        </div>\n      }\n    >\n      <div className=\"mt-8 mb-4 text-xl font-semibold\">\n        <form id=\"emailForm\" onSubmit={(e) => handleSubmit(e)}>\n          <input\n            type=\"text\"\n            name=\"newEmail\"\n            className=\"w-1/2 p-3 rounded-md bg-zinc-800\"\n            defaultValue={userEmail ?? ''}\n            placeholder=\"Your email\"\n            maxLength={64}\n          />\n        </form>\n      </div>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "components/ui/AccountForms/NameForm.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport Card from '@/components/ui/Card';\nimport { updateName } from '@/utils/auth-helpers/server';\nimport { handleRequest } from '@/utils/auth-helpers/client';\nimport { useRouter } from 'next/navigation';\nimport { useState } from 'react';\n\nexport default function NameForm({ userName }: { userName: string }) {\n  const router = useRouter();\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n    setIsSubmitting(true);\n    // Check if the new name is the same as the old name\n    if (e.currentTarget.fullName.value === userName) {\n      e.preventDefault();\n      setIsSubmitting(false);\n      return;\n    }\n    handleRequest(e, updateName, router);\n    setIsSubmitting(false);\n  };\n\n  return (\n    <Card\n      title=\"Your Name\"\n      description=\"Please enter your full name, or a display name you are comfortable with.\"\n      footer={\n        <div className=\"flex flex-col items-start justify-between sm:flex-row sm:items-center\">\n          <p className=\"pb-4 sm:pb-0\">64 characters maximum</p>\n          <Button\n            variant=\"slim\"\n            type=\"submit\"\n            form=\"nameForm\"\n            loading={isSubmitting}\n          >\n            Update Name\n          </Button>\n        </div>\n      }\n    >\n      <div className=\"mt-8 mb-4 text-xl font-semibold\">\n        <form id=\"nameForm\" onSubmit={(e) => handleSubmit(e)}>\n          <input\n            type=\"text\"\n            name=\"fullName\"\n            className=\"w-1/2 p-3 rounded-md bg-zinc-800\"\n            defaultValue={userName}\n            placeholder=\"Your name\"\n            maxLength={64}\n          />\n        </form>\n      </div>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "components/ui/AuthForms/EmailSignIn.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport Link from 'next/link';\nimport { signInWithEmail } from '@/utils/auth-helpers/server';\nimport { handleRequest } from '@/utils/auth-helpers/client';\nimport { useRouter } from 'next/navigation';\nimport { useState } from 'react';\n\n// Define prop type with allowPassword boolean\ninterface EmailSignInProps {\n  allowPassword: boolean;\n  redirectMethod: string;\n  disableButton?: boolean;\n}\n\nexport default function EmailSignIn({\n  allowPassword,\n  redirectMethod,\n  disableButton\n}: EmailSignInProps) {\n  const router = redirectMethod === 'client' ? useRouter() : null;\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n    setIsSubmitting(true); // Disable the button while the request is being handled\n    await handleRequest(e, signInWithEmail, router);\n    setIsSubmitting(false);\n  };\n\n  return (\n    <div className=\"my-8\">\n      <form\n        noValidate={true}\n        className=\"mb-4\"\n        onSubmit={(e) => handleSubmit(e)}\n      >\n        <div className=\"grid gap-2\">\n          <div className=\"grid gap-1\">\n            <label htmlFor=\"email\">Email</label>\n            <input\n              id=\"email\"\n              placeholder=\"name@example.com\"\n              type=\"email\"\n              name=\"email\"\n              autoCapitalize=\"none\"\n              autoComplete=\"email\"\n              autoCorrect=\"off\"\n              className=\"w-full p-3 rounded-md bg-zinc-800\"\n            />\n          </div>\n          <Button\n            variant=\"slim\"\n            type=\"submit\"\n            className=\"mt-1\"\n            loading={isSubmitting}\n            disabled={disableButton}\n          >\n            Sign in\n          </Button>\n        </div>\n      </form>\n      {allowPassword && (\n        <>\n          <p>\n            <Link href=\"/signin/password_signin\" className=\"font-light text-sm\">\n              Sign in with email and password\n            </Link>\n          </p>\n          <p>\n            <Link href=\"/signin/signup\" className=\"font-light text-sm\">\n              Don't have an account? Sign up\n            </Link>\n          </p>\n        </>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/AuthForms/ForgotPassword.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport Link from 'next/link';\nimport { requestPasswordUpdate } from '@/utils/auth-helpers/server';\nimport { handleRequest } from '@/utils/auth-helpers/client';\nimport { useRouter } from 'next/navigation';\nimport { useState } from 'react';\n\n// Define prop type with allowEmail boolean\ninterface ForgotPasswordProps {\n  allowEmail: boolean;\n  redirectMethod: string;\n  disableButton?: boolean;\n}\n\nexport default function ForgotPassword({\n  allowEmail,\n  redirectMethod,\n  disableButton\n}: ForgotPasswordProps) {\n  const router = redirectMethod === 'client' ? useRouter() : null;\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n    setIsSubmitting(true); // Disable the button while the request is being handled\n    await handleRequest(e, requestPasswordUpdate, router);\n    setIsSubmitting(false);\n  };\n\n  return (\n    <div className=\"my-8\">\n      <form\n        noValidate={true}\n        className=\"mb-4\"\n        onSubmit={(e) => handleSubmit(e)}\n      >\n        <div className=\"grid gap-2\">\n          <div className=\"grid gap-1\">\n            <label htmlFor=\"email\">Email</label>\n            <input\n              id=\"email\"\n              placeholder=\"name@example.com\"\n              type=\"email\"\n              name=\"email\"\n              autoCapitalize=\"none\"\n              autoComplete=\"email\"\n              autoCorrect=\"off\"\n              className=\"w-full p-3 rounded-md bg-zinc-800\"\n            />\n          </div>\n          <Button\n            variant=\"slim\"\n            type=\"submit\"\n            className=\"mt-1\"\n            loading={isSubmitting}\n            disabled={disableButton}\n          >\n            Send Email\n          </Button>\n        </div>\n      </form>\n      <p>\n        <Link href=\"/signin/password_signin\" className=\"font-light text-sm\">\n          Sign in with email and password\n        </Link>\n      </p>\n      {allowEmail && (\n        <p>\n          <Link href=\"/signin/email_signin\" className=\"font-light text-sm\">\n            Sign in via magic link\n          </Link>\n        </p>\n      )}\n      <p>\n        <Link href=\"/signin/signup\" className=\"font-light text-sm\">\n          Don't have an account? Sign up\n        </Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/AuthForms/OauthSignIn.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport { signInWithOAuth } from '@/utils/auth-helpers/client';\nimport { type Provider } from '@supabase/supabase-js';\nimport { Github } from 'lucide-react';\nimport { useState } from 'react';\n\ntype OAuthProviders = {\n  name: Provider;\n  displayName: string;\n  icon: JSX.Element;\n};\n\nexport default function OauthSignIn() {\n  const oAuthProviders: OAuthProviders[] = [\n    {\n      name: 'github',\n      displayName: 'GitHub',\n      icon: <Github className=\"h-5 w-5\" />\n    }\n    /* Add desired OAuth providers here */\n  ];\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n    setIsSubmitting(true); // Disable the button while the request is being handled\n    await signInWithOAuth(e);\n    setIsSubmitting(false);\n  };\n\n  return (\n    <div className=\"mt-8\">\n      {oAuthProviders.map((provider) => (\n        <form\n          key={provider.name}\n          className=\"pb-2\"\n          onSubmit={(e) => handleSubmit(e)}\n        >\n          <input type=\"hidden\" name=\"provider\" value={provider.name} />\n          <Button\n            variant=\"slim\"\n            type=\"submit\"\n            className=\"w-full\"\n            loading={isSubmitting}\n          >\n            <span className=\"mr-2\">{provider.icon}</span>\n            <span>{provider.displayName}</span>\n          </Button>\n        </form>\n      ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/AuthForms/PasswordSignIn.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport Link from 'next/link';\nimport { signInWithPassword } from '@/utils/auth-helpers/server';\nimport { handleRequest } from '@/utils/auth-helpers/client';\nimport { useRouter } from 'next/navigation';\nimport React, { useState } from 'react';\n\n// Define prop type with allowEmail boolean\ninterface PasswordSignInProps {\n  allowEmail: boolean;\n  redirectMethod: string;\n}\n\nexport default function PasswordSignIn({\n  allowEmail,\n  redirectMethod\n}: PasswordSignInProps) {\n  const router = redirectMethod === 'client' ? useRouter() : null;\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n    setIsSubmitting(true); // Disable the button while the request is being handled\n    await handleRequest(e, signInWithPassword, router);\n    setIsSubmitting(false);\n  };\n\n  return (\n    <div className=\"my-8\">\n      <form\n        noValidate={true}\n        className=\"mb-4\"\n        onSubmit={(e) => handleSubmit(e)}\n      >\n        <div className=\"grid gap-2\">\n          <div className=\"grid gap-1\">\n            <label htmlFor=\"email\">Email</label>\n            <input\n              id=\"email\"\n              placeholder=\"name@example.com\"\n              type=\"email\"\n              name=\"email\"\n              autoCapitalize=\"none\"\n              autoComplete=\"email\"\n              autoCorrect=\"off\"\n              className=\"w-full p-3 rounded-md bg-zinc-800\"\n            />\n            <label htmlFor=\"password\">Password</label>\n            <input\n              id=\"password\"\n              placeholder=\"Password\"\n              type=\"password\"\n              name=\"password\"\n              autoComplete=\"current-password\"\n              className=\"w-full p-3 rounded-md bg-zinc-800\"\n            />\n          </div>\n          <Button\n            variant=\"slim\"\n            type=\"submit\"\n            className=\"mt-1\"\n            loading={isSubmitting}\n          >\n            Sign in\n          </Button>\n        </div>\n      </form>\n      <p>\n        <Link href=\"/signin/forgot_password\" className=\"font-light text-sm\">\n          Forgot your password?\n        </Link>\n      </p>\n      {allowEmail && (\n        <p>\n          <Link href=\"/signin/email_signin\" className=\"font-light text-sm\">\n            Sign in via magic link\n          </Link>\n        </p>\n      )}\n      <p>\n        <Link href=\"/signin/signup\" className=\"font-light text-sm\">\n          Don't have an account? Sign up\n        </Link>\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/AuthForms/Separator.tsx",
    "content": "interface SeparatorProps {\n  text: string;\n}\n\nexport default function Separator({ text }: SeparatorProps) {\n  return (\n    <div className=\"relative\">\n      <div className=\"relative flex items-center py-1\">\n        <div className=\"grow border-t border-zinc-700\"></div>\n        <span className=\"mx-3 shrink text-sm leading-8 text-zinc-500\">\n          {text}\n        </span>\n        <div className=\"grow border-t border-zinc-700\"></div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/AuthForms/Signup.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport React from 'react';\nimport Link from 'next/link';\nimport { signUp } from '@/utils/auth-helpers/server';\nimport { handleRequest } from '@/utils/auth-helpers/client';\nimport { useRouter } from 'next/navigation';\nimport { useState } from 'react';\n\n// Define prop type with allowEmail boolean\ninterface SignUpProps {\n  allowEmail: boolean;\n  redirectMethod: string;\n}\n\nexport default function SignUp({ allowEmail, redirectMethod }: SignUpProps) {\n  const router = redirectMethod === 'client' ? useRouter() : null;\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n    setIsSubmitting(true); // Disable the button while the request is being handled\n    await handleRequest(e, signUp, router);\n    setIsSubmitting(false);\n  };\n\n  return (\n    <div className=\"my-8\">\n      <form\n        noValidate={true}\n        className=\"mb-4\"\n        onSubmit={(e) => handleSubmit(e)}\n      >\n        <div className=\"grid gap-2\">\n          <div className=\"grid gap-1\">\n            <label htmlFor=\"email\">Email</label>\n            <input\n              id=\"email\"\n              placeholder=\"name@example.com\"\n              type=\"email\"\n              name=\"email\"\n              autoCapitalize=\"none\"\n              autoComplete=\"email\"\n              autoCorrect=\"off\"\n              className=\"w-full p-3 rounded-md bg-zinc-800\"\n            />\n            <label htmlFor=\"password\">Password</label>\n            <input\n              id=\"password\"\n              placeholder=\"Password\"\n              type=\"password\"\n              name=\"password\"\n              autoComplete=\"current-password\"\n              className=\"w-full p-3 rounded-md bg-zinc-800\"\n            />\n          </div>\n          <Button\n            variant=\"slim\"\n            type=\"submit\"\n            className=\"mt-1\"\n            loading={isSubmitting}\n          >\n            Sign up\n          </Button>\n        </div>\n      </form>\n      <p>Already have an account?</p>\n      <p>\n        <Link href=\"/signin/password_signin\" className=\"font-light text-sm\">\n          Sign in with email and password\n        </Link>\n      </p>\n      {allowEmail && (\n        <p>\n          <Link href=\"/signin/email_signin\" className=\"font-light text-sm\">\n            Sign in via magic link\n          </Link>\n        </p>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/AuthForms/UpdatePassword.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport { updatePassword } from '@/utils/auth-helpers/server';\nimport { handleRequest } from '@/utils/auth-helpers/client';\nimport { useRouter } from 'next/navigation';\nimport React, { useState } from 'react';\n\ninterface UpdatePasswordProps {\n  redirectMethod: string;\n}\n\nexport default function UpdatePassword({\n  redirectMethod\n}: UpdatePasswordProps) {\n  const router = redirectMethod === 'client' ? useRouter() : null;\n  const [isSubmitting, setIsSubmitting] = useState(false);\n\n  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {\n    setIsSubmitting(true); // Disable the button while the request is being handled\n    await handleRequest(e, updatePassword, router);\n    setIsSubmitting(false);\n  };\n\n  return (\n    <div className=\"my-8\">\n      <form\n        noValidate={true}\n        className=\"mb-4\"\n        onSubmit={(e) => handleSubmit(e)}\n      >\n        <div className=\"grid gap-2\">\n          <div className=\"grid gap-1\">\n            <label htmlFor=\"password\">New Password</label>\n            <input\n              id=\"password\"\n              placeholder=\"Password\"\n              type=\"password\"\n              name=\"password\"\n              autoComplete=\"current-password\"\n              className=\"w-full p-3 rounded-md bg-zinc-800\"\n            />\n            <label htmlFor=\"passwordConfirm\">Confirm New Password</label>\n            <input\n              id=\"passwordConfirm\"\n              placeholder=\"Password\"\n              type=\"password\"\n              name=\"passwordConfirm\"\n              autoComplete=\"current-password\"\n              className=\"w-full p-3 rounded-md bg-zinc-800\"\n            />\n          </div>\n          <Button\n            variant=\"slim\"\n            type=\"submit\"\n            className=\"mt-1\"\n            loading={isSubmitting}\n          >\n            Update Password\n          </Button>\n        </div>\n      </form>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/Button/Button.module.css",
    "content": ".root {\n  @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;\n}\n\n.root:hover {\n  @apply bg-zinc-800 text-white border border-white;\n}\n\n.root:focus {\n  @apply outline-none ring-2 ring-pink-500 ring-opacity-50;\n}\n\n.root[data-active] {\n  @apply bg-zinc-600;\n}\n\n.loading {\n  @apply bg-zinc-700 text-zinc-500 border-zinc-600 cursor-not-allowed;\n}\n\n.slim {\n  @apply py-2 transform-none normal-case;\n}\n\n.disabled,\n.disabled:hover {\n  @apply text-zinc-400 border-zinc-600 bg-zinc-700 cursor-not-allowed;\n  filter: grayscale(1);\n  -webkit-transform: translateZ(0);\n  -webkit-perspective: 1000;\n  -webkit-backface-visibility: hidden;\n}\n"
  },
  {
    "path": "components/ui/Button/Button.tsx",
    "content": "'use client';\n\nimport cn from 'classnames';\nimport React, { forwardRef, useRef, ButtonHTMLAttributes } from 'react';\nimport { mergeRefs } from 'react-merge-refs';\n\nimport LoadingDots from '@/components/ui/LoadingDots';\n\nimport styles from './Button.module.css';\n\ninterface Props extends ButtonHTMLAttributes<HTMLButtonElement> {\n  variant?: 'slim' | 'flat';\n  active?: boolean;\n  width?: number;\n  loading?: boolean;\n  Component?: React.ComponentType;\n}\n\nconst Button = forwardRef<HTMLButtonElement, Props>((props, buttonRef) => {\n  const {\n    className,\n    variant = 'flat',\n    children,\n    active,\n    width,\n    loading = false,\n    disabled = false,\n    style = {},\n    Component = 'button',\n    ...rest\n  } = props;\n  const ref = useRef(null);\n  const rootClassName = cn(\n    styles.root,\n    {\n      [styles.slim]: variant === 'slim',\n      [styles.loading]: loading,\n      [styles.disabled]: disabled\n    },\n    className\n  );\n  return (\n    <Component\n      aria-pressed={active}\n      data-variant={variant}\n      ref={mergeRefs([ref, buttonRef])}\n      className={rootClassName}\n      disabled={disabled}\n      style={{\n        width,\n        ...style\n      }}\n      {...rest}\n    >\n      {children}\n      {loading && (\n        <i className=\"flex pl-2 m-0\">\n          <LoadingDots />\n        </i>\n      )}\n    </Component>\n  );\n});\nButton.displayName = 'Button';\n\nexport default Button;\n"
  },
  {
    "path": "components/ui/Button/index.ts",
    "content": "export { default } from './Button';\n"
  },
  {
    "path": "components/ui/Card/Card.tsx",
    "content": "import { ReactNode } from 'react';\n\ninterface Props {\n  title: string;\n  description?: string;\n  footer?: ReactNode;\n  children: ReactNode;\n}\n\nexport default function Card({ title, description, footer, children }: Props) {\n  return (\n    <div className=\"w-full max-w-3xl m-auto my-8 border rounded-md p border-zinc-700\">\n      <div className=\"px-5 py-4\">\n        <h3 className=\"mb-1 text-2xl font-medium\">{title}</h3>\n        <p className=\"text-zinc-300\">{description}</p>\n        {children}\n      </div>\n      {footer && (\n        <div className=\"p-4 border-t rounded-b-md border-zinc-700 bg-zinc-900 text-zinc-500\">\n          {footer}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/Card/index.ts",
    "content": "export { default } from './Card';\n"
  },
  {
    "path": "components/ui/Footer/Footer.tsx",
    "content": "import Link from 'next/link';\n\nimport Logo from '@/components/icons/Logo';\nimport GitHub from '@/components/icons/GitHub';\n\nexport default function Footer() {\n  return (\n    <footer className=\"mx-auto max-w-[1920px] px-6 bg-zinc-900\">\n      <div className=\"grid grid-cols-1 gap-8 py-12 text-white transition-colors duration-150 border-b lg:grid-cols-12 border-zinc-600 bg-zinc-900\">\n        <div className=\"col-span-1 lg:col-span-2\">\n          <Link\n            href=\"/\"\n            className=\"flex items-center flex-initial font-bold md:mr-24\"\n          >\n            <span className=\"mr-2 border rounded-full border-zinc-700\">\n              <Logo />\n            </span>\n            <span>ACME</span>\n          </Link>\n        </div>\n        <div className=\"col-span-1 lg:col-span-2\">\n          <ul className=\"flex flex-col flex-initial md:flex-1\">\n            <li className=\"py-3 md:py-0 md:pb-4\">\n              <Link\n                href=\"/\"\n                className=\"text-white transition duration-150 ease-in-out hover:text-zinc-200\"\n              >\n                Home\n              </Link>\n            </li>\n            <li className=\"py-3 md:py-0 md:pb-4\">\n              <Link\n                href=\"/\"\n                className=\"text-white transition duration-150 ease-in-out hover:text-zinc-200\"\n              >\n                About\n              </Link>\n            </li>\n            <li className=\"py-3 md:py-0 md:pb-4\">\n              <Link\n                href=\"/\"\n                className=\"text-white transition duration-150 ease-in-out hover:text-zinc-200\"\n              >\n                Careers\n              </Link>\n            </li>\n            <li className=\"py-3 md:py-0 md:pb-4\">\n              <Link\n                href=\"/\"\n                className=\"text-white transition duration-150 ease-in-out hover:text-zinc-200\"\n              >\n                Blog\n              </Link>\n            </li>\n          </ul>\n        </div>\n        <div className=\"col-span-1 lg:col-span-2\">\n          <ul className=\"flex flex-col flex-initial md:flex-1\">\n            <li className=\"py-3 md:py-0 md:pb-4\">\n              <p className=\"font-bold text-white transition duration-150 ease-in-out hover:text-zinc-200\">\n                LEGAL\n              </p>\n            </li>\n            <li className=\"py-3 md:py-0 md:pb-4\">\n              <Link\n                href=\"/\"\n                className=\"text-white transition duration-150 ease-in-out hover:text-zinc-200\"\n              >\n                Privacy Policy\n              </Link>\n            </li>\n            <li className=\"py-3 md:py-0 md:pb-4\">\n              <Link\n                href=\"/\"\n                className=\"text-white transition duration-150 ease-in-out hover:text-zinc-200\"\n              >\n                Terms of Use\n              </Link>\n            </li>\n          </ul>\n        </div>\n        <div className=\"flex items-start col-span-1 text-white lg:col-span-6 lg:justify-end\">\n          <div className=\"flex items-center h-10 space-x-6\">\n            <a\n              aria-label=\"Github Repository\"\n              href=\"https://github.com/vercel/nextjs-subscription-payments\"\n            >\n              <GitHub />\n            </a>\n          </div>\n        </div>\n      </div>\n      <div className=\"flex flex-col items-center justify-between py-12 space-y-4 md:flex-row bg-zinc-900\">\n        <div>\n          <span>\n            &copy; {new Date().getFullYear()} ACME, Inc. All rights reserved.\n          </span>\n        </div>\n        <div className=\"flex items-center\">\n          <span className=\"text-white\">Crafted by</span>\n          <a href=\"https://vercel.com\" aria-label=\"Vercel.com Link\">\n            <img\n              src=\"/vercel.svg\"\n              alt=\"Vercel.com Logo\"\n              className=\"inline-block h-6 ml-4 text-white\"\n            />\n          </a>\n        </div>\n      </div>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "components/ui/Footer/index.ts",
    "content": "export { default } from './Footer';\n"
  },
  {
    "path": "components/ui/Input/Input.module.css",
    "content": ".root {\n  @apply bg-black py-2 px-3 w-full appearance-none transition duration-150 ease-in-out border border-zinc-500 text-zinc-200;\n}\n\n.root:focus {\n  @apply outline-none;\n}\n"
  },
  {
    "path": "components/ui/Input/Input.tsx",
    "content": "import React, { InputHTMLAttributes, ChangeEvent } from 'react';\nimport cn from 'classnames';\n\nimport s from './Input.module.css';\n\ninterface Props extends Omit<InputHTMLAttributes<any>, 'onChange'> {\n  className?: string;\n  onChange: (value: string) => void;\n}\nconst Input = (props: Props) => {\n  const { className, children, onChange, ...rest } = props;\n\n  const rootClassName = cn(s.root, {}, className);\n\n  const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {\n    if (onChange) {\n      onChange(e.target.value);\n    }\n    return null;\n  };\n\n  return (\n    <label>\n      <input\n        className={rootClassName}\n        onChange={handleOnChange}\n        autoComplete=\"off\"\n        autoCorrect=\"off\"\n        autoCapitalize=\"off\"\n        spellCheck=\"false\"\n        {...rest}\n      />\n    </label>\n  );\n};\n\nexport default Input;\n"
  },
  {
    "path": "components/ui/Input/index.ts",
    "content": "export { default } from './Input';\n"
  },
  {
    "path": "components/ui/LoadingDots/LoadingDots.module.css",
    "content": ".root {\n  @apply inline-flex text-center items-center leading-7;\n}\n\n.root span {\n  @apply bg-zinc-200 rounded-full h-2 w-2;\n  animation-name: blink;\n  animation-duration: 1.4s;\n  animation-iteration-count: infinite;\n  animation-fill-mode: both;\n  margin: 0 2px;\n}\n\n.root span:nth-of-type(2) {\n  animation-delay: 0.2s;\n}\n\n.root span:nth-of-type(3) {\n  animation-delay: 0.4s;\n}\n\n@keyframes blink {\n  0% {\n    opacity: 0.2;\n  }\n  20% {\n    opacity: 1;\n  }\n  100% {\n    opacity: 0.2;\n  }\n}\n"
  },
  {
    "path": "components/ui/LoadingDots/LoadingDots.tsx",
    "content": "import s from './LoadingDots.module.css';\n\nconst LoadingDots = () => {\n  return (\n    <span className={s.root}>\n      <span />\n      <span />\n      <span />\n    </span>\n  );\n};\n\nexport default LoadingDots;\n"
  },
  {
    "path": "components/ui/LoadingDots/index.ts",
    "content": "export { default } from './LoadingDots';\n"
  },
  {
    "path": "components/ui/LogoCloud/LogoCloud.tsx",
    "content": "export default function LogoCloud() {\n  return (\n    <div>\n      <p className=\"mt-24 text-xs uppercase text-zinc-400 text-center font-bold tracking-[0.3em]\">\n        Brought to you by\n      </p>\n      <div className=\"grid grid-cols-1 place-items-center\tmy-12 space-y-4 sm:mt-8 sm:space-y-0 md:mx-auto md:max-w-2xl sm:grid sm:gap-6 sm:grid-cols-5\">\n        <div className=\"flex items-center justify-start h-12\">\n          <a href=\"https://nextjs.org\" aria-label=\"Next.js Link\">\n            <img\n              src=\"/nextjs.svg\"\n              alt=\"Next.js Logo\"\n              className=\"h-6 sm:h-12 text-white\"\n            />\n          </a>\n        </div>\n        <div className=\"flex items-center justify-start h-12\">\n          <a href=\"https://vercel.com\" aria-label=\"Vercel.com Link\">\n            <img\n              src=\"/vercel.svg\"\n              alt=\"Vercel.com Logo\"\n              className=\"h-6 text-white\"\n            />\n          </a>\n        </div>\n        <div className=\"flex items-center justify-start h-12\">\n          <a href=\"https://stripe.com\" aria-label=\"stripe.com Link\">\n            <img\n              src=\"/stripe.svg\"\n              alt=\"stripe.com Logo\"\n              className=\"h-12 text-white\"\n            />\n          </a>\n        </div>\n        <div className=\"flex items-center justify-start h-12\">\n          <a href=\"https://supabase.io\" aria-label=\"supabase.io Link\">\n            <img\n              src=\"/supabase.svg\"\n              alt=\"supabase.io Logo\"\n              className=\"h-10 text-white\"\n            />\n          </a>\n        </div>\n        <div className=\"flex items-center justify-start h-12\">\n          <a href=\"https://github.com\" aria-label=\"github.com Link\">\n            <img\n              src=\"/github.svg\"\n              alt=\"github.com Logo\"\n              className=\"h-8 text-white\"\n            />\n          </a>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/LogoCloud/index.ts",
    "content": "export { default } from './LogoCloud';\n"
  },
  {
    "path": "components/ui/Navbar/Navbar.module.css",
    "content": ".root {\n  @apply sticky top-0 bg-black z-40 transition-all duration-150 h-16 md:h-20;\n}\n\n.link {\n  @apply inline-flex items-center leading-6 font-medium transition ease-in-out duration-75 cursor-pointer text-zinc-200 rounded-md p-1;\n}\n\n.link:hover {\n  @apply text-zinc-100;\n}\n\n.link:focus {\n  @apply outline-none text-zinc-100 ring-2 ring-pink-500 ring-opacity-50;\n}\n\n.logo {\n  @apply cursor-pointer rounded-full transform duration-100 ease-in-out;\n}\n"
  },
  {
    "path": "components/ui/Navbar/Navbar.tsx",
    "content": "import { createClient } from '@/utils/supabase/server';\nimport s from './Navbar.module.css';\nimport Navlinks from './Navlinks';\n\nexport default async function Navbar() {\n  const supabase = createClient();\n\n  const {\n    data: { user }\n  } = await supabase.auth.getUser();\n\n  return (\n    <nav className={s.root}>\n      <a href=\"#skip\" className=\"sr-only focus:not-sr-only\">\n        Skip to content\n      </a>\n      <div className=\"max-w-6xl px-6 mx-auto\">\n        <Navlinks user={user} />\n      </div>\n    </nav>\n  );\n}\n"
  },
  {
    "path": "components/ui/Navbar/Navlinks.tsx",
    "content": "'use client';\n\nimport Link from 'next/link';\nimport { SignOut } from '@/utils/auth-helpers/server';\nimport { handleRequest } from '@/utils/auth-helpers/client';\nimport Logo from '@/components/icons/Logo';\nimport { usePathname, useRouter } from 'next/navigation';\nimport { getRedirectMethod } from '@/utils/auth-helpers/settings';\nimport s from './Navbar.module.css';\n\ninterface NavlinksProps {\n  user?: any;\n}\n\nexport default function Navlinks({ user }: NavlinksProps) {\n  const router = getRedirectMethod() === 'client' ? useRouter() : null;\n\n  return (\n    <div className=\"relative flex flex-row justify-between py-4 align-center md:py-6\">\n      <div className=\"flex items-center flex-1\">\n        <Link href=\"/\" className={s.logo} aria-label=\"Logo\">\n          <Logo />\n        </Link>\n        <nav className=\"ml-6 space-x-2 lg:block\">\n          <Link href=\"/\" className={s.link}>\n            Pricing\n          </Link>\n          {user && (\n            <Link href=\"/account\" className={s.link}>\n              Account\n            </Link>\n          )}\n        </nav>\n      </div>\n      <div className=\"flex justify-end space-x-8\">\n        {user ? (\n          <form onSubmit={(e) => handleRequest(e, SignOut, router)}>\n            <input type=\"hidden\" name=\"pathName\" value={usePathname()} />\n            <button type=\"submit\" className={s.link}>\n              Sign out\n            </button>\n          </form>\n        ) : (\n          <Link href=\"/signin\" className={s.link}>\n            Sign In\n          </Link>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/ui/Navbar/index.ts",
    "content": "export { default } from './Navbar';\n"
  },
  {
    "path": "components/ui/Pricing/Pricing.tsx",
    "content": "'use client';\n\nimport Button from '@/components/ui/Button';\nimport LogoCloud from '@/components/ui/LogoCloud';\nimport type { Tables } from '@/types_db';\nimport { getStripe } from '@/utils/stripe/client';\nimport { checkoutWithStripe } from '@/utils/stripe/server';\nimport { getErrorRedirect } from '@/utils/helpers';\nimport { User } from '@supabase/supabase-js';\nimport cn from 'classnames';\nimport { useRouter, usePathname } from 'next/navigation';\nimport { useState } from 'react';\n\ntype Subscription = Tables<'subscriptions'>;\ntype Product = Tables<'products'>;\ntype Price = Tables<'prices'>;\ninterface ProductWithPrices extends Product {\n  prices: Price[];\n}\ninterface PriceWithProduct extends Price {\n  products: Product | null;\n}\ninterface SubscriptionWithProduct extends Subscription {\n  prices: PriceWithProduct | null;\n}\n\ninterface Props {\n  user: User | null | undefined;\n  products: ProductWithPrices[];\n  subscription: SubscriptionWithProduct | null;\n}\n\ntype BillingInterval = 'lifetime' | 'year' | 'month';\n\nexport default function Pricing({ user, products, subscription }: Props) {\n  const intervals = Array.from(\n    new Set(\n      products.flatMap((product) =>\n        product?.prices?.map((price) => price?.interval)\n      )\n    )\n  );\n  const router = useRouter();\n  const [billingInterval, setBillingInterval] =\n    useState<BillingInterval>('month');\n  const [priceIdLoading, setPriceIdLoading] = useState<string>();\n  const currentPath = usePathname();\n\n  const handleStripeCheckout = async (price: Price) => {\n    setPriceIdLoading(price.id);\n\n    if (!user) {\n      setPriceIdLoading(undefined);\n      return router.push('/signin/signup');\n    }\n\n    const { errorRedirect, sessionId } = await checkoutWithStripe(\n      price,\n      currentPath\n    );\n\n    if (errorRedirect) {\n      setPriceIdLoading(undefined);\n      return router.push(errorRedirect);\n    }\n\n    if (!sessionId) {\n      setPriceIdLoading(undefined);\n      return router.push(\n        getErrorRedirect(\n          currentPath,\n          'An unknown error occurred.',\n          'Please try again later or contact a system administrator.'\n        )\n      );\n    }\n\n    const stripe = await getStripe();\n    stripe?.redirectToCheckout({ sessionId });\n\n    setPriceIdLoading(undefined);\n  };\n\n  if (!products.length) {\n    return (\n      <section className=\"bg-black\">\n        <div className=\"max-w-6xl px-4 py-8 mx-auto sm:py-24 sm:px-6 lg:px-8\">\n          <div className=\"sm:flex sm:flex-col sm:align-center\"></div>\n          <p className=\"text-4xl font-extrabold text-white sm:text-center sm:text-6xl\">\n            No subscription pricing plans found. Create them in your{' '}\n            <a\n              className=\"text-pink-500 underline\"\n              href=\"https://dashboard.stripe.com/products\"\n              rel=\"noopener noreferrer\"\n              target=\"_blank\"\n            >\n              Stripe Dashboard\n            </a>\n            .\n          </p>\n        </div>\n        <LogoCloud />\n      </section>\n    );\n  } else {\n    return (\n      <section className=\"bg-black\">\n        <div className=\"max-w-6xl px-4 py-8 mx-auto sm:py-24 sm:px-6 lg:px-8\">\n          <div className=\"sm:flex sm:flex-col sm:align-center\">\n            <h1 className=\"text-4xl font-extrabold text-white sm:text-center sm:text-6xl\">\n              Pricing Plans\n            </h1>\n            <p className=\"max-w-2xl m-auto mt-5 text-xl text-zinc-200 sm:text-center sm:text-2xl\">\n              Start building for free, then add a site plan to go live. Account\n              plans unlock additional features.\n            </p>\n            <div className=\"relative self-center mt-6 bg-zinc-900 rounded-lg p-0.5 flex sm:mt-8 border border-zinc-800\">\n              {intervals.includes('month') && (\n                <button\n                  onClick={() => setBillingInterval('month')}\n                  type=\"button\"\n                  className={`${\n                    billingInterval === 'month'\n                      ? 'relative w-1/2 bg-zinc-700 border-zinc-800 shadow-sm text-white'\n                      : 'ml-0.5 relative w-1/2 border border-transparent text-zinc-400'\n                  } rounded-md m-1 py-2 text-sm font-medium whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-pink-500 focus:ring-opacity-50 focus:z-10 sm:w-auto sm:px-8`}\n                >\n                  Monthly billing\n                </button>\n              )}\n              {intervals.includes('year') && (\n                <button\n                  onClick={() => setBillingInterval('year')}\n                  type=\"button\"\n                  className={`${\n                    billingInterval === 'year'\n                      ? 'relative w-1/2 bg-zinc-700 border-zinc-800 shadow-sm text-white'\n                      : 'ml-0.5 relative w-1/2 border border-transparent text-zinc-400'\n                  } rounded-md m-1 py-2 text-sm font-medium whitespace-nowrap focus:outline-none focus:ring-2 focus:ring-pink-500 focus:ring-opacity-50 focus:z-10 sm:w-auto sm:px-8`}\n                >\n                  Yearly billing\n                </button>\n              )}\n            </div>\n          </div>\n          <div className=\"mt-12 space-y-0 sm:mt-16 flex flex-wrap justify-center gap-6 lg:max-w-4xl lg:mx-auto xl:max-w-none xl:mx-0\">\n            {products.map((product) => {\n              const price = product?.prices?.find(\n                (price) => price.interval === billingInterval\n              );\n              if (!price) return null;\n              const priceString = new Intl.NumberFormat('en-US', {\n                style: 'currency',\n                currency: price.currency!,\n                minimumFractionDigits: 0\n              }).format((price?.unit_amount || 0) / 100);\n              return (\n                <div\n                  key={product.id}\n                  className={cn(\n                    'flex flex-col rounded-lg shadow-sm divide-y divide-zinc-600 bg-zinc-900',\n                    {\n                      'border border-pink-500': subscription\n                        ? product.name === subscription?.prices?.products?.name\n                        : product.name === 'Freelancer'\n                    },\n                    'flex-1', // This makes the flex item grow to fill the space\n                    'basis-1/3', // Assuming you want each card to take up roughly a third of the container's width\n                    'max-w-xs' // Sets a maximum width to the cards to prevent them from getting too large\n                  )}\n                >\n                  <div className=\"p-6\">\n                    <h2 className=\"text-2xl font-semibold leading-6 text-white\">\n                      {product.name}\n                    </h2>\n                    <p className=\"mt-4 text-zinc-300\">{product.description}</p>\n                    <p className=\"mt-8\">\n                      <span className=\"text-5xl font-extrabold white\">\n                        {priceString}\n                      </span>\n                      <span className=\"text-base font-medium text-zinc-100\">\n                        /{billingInterval}\n                      </span>\n                    </p>\n                    <Button\n                      variant=\"slim\"\n                      type=\"button\"\n                      loading={priceIdLoading === price.id}\n                      onClick={() => handleStripeCheckout(price)}\n                      className=\"block w-full py-2 mt-8 text-sm font-semibold text-center text-white rounded-md hover:bg-zinc-900\"\n                    >\n                      {subscription ? 'Manage' : 'Subscribe'}\n                    </Button>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n          <LogoCloud />\n        </div>\n      </section>\n    );\n  }\n}\n"
  },
  {
    "path": "components/ui/Toasts/toast.tsx",
    "content": "import * as React from 'react';\nimport * as ToastPrimitives from '@radix-ui/react-toast';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { X } from 'lucide-react';\n\nimport { cn } from '@/utils/cn';\n\nconst ToastProvider = ToastPrimitives.Provider;\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',\n      className\n    )}\n    {...props}\n  />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n  'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border border-zinc-200 p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-zinc-800',\n  {\n    variants: {\n      variant: {\n        default:\n          'border bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50',\n        destructive:\n          'destructive group border-red-500 bg-red-500 text-zinc-50 dark:border-red-900 dark:bg-red-900 dark:text-zinc-50'\n      }\n    },\n    defaultVariants: {\n      variant: 'default'\n    }\n  }\n);\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  );\n});\nToast.displayName = ToastPrimitives.Root.displayName;\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      'inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-zinc-200 bg-transparent px-3 text-sm font-medium ring-offset-white transition-colors hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-zinc-100/40 group-[.destructive]:hover:border-red-500/30 group-[.destructive]:hover:bg-red-500 group-[.destructive]:hover:text-zinc-50 group-[.destructive]:focus:ring-red-500 dark:border-zinc-800 dark:ring-offset-zinc-950 dark:hover:bg-zinc-800 dark:focus:ring-zinc-300 dark:group-[.destructive]:border-zinc-800/40 dark:group-[.destructive]:hover:border-red-900/30 dark:group-[.destructive]:hover:bg-red-900 dark:group-[.destructive]:hover:text-zinc-50 dark:group-[.destructive]:focus:ring-red-900',\n      className\n    )}\n    {...props}\n  />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      'absolute right-2 top-2 rounded-md p-1 text-zinc-950/50 opacity-0 transition-opacity hover:text-zinc-950 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:text-zinc-50/50 dark:hover:text-zinc-50',\n      className\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <X className=\"h-4 w-4\" />\n  </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn('text-sm font-semibold', className)}\n    {...props}\n  />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn('text-sm opacity-90', className)}\n    {...props}\n  />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>;\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction\n};\n"
  },
  {
    "path": "components/ui/Toasts/toaster.tsx",
    "content": "'use client';\n\nimport {\n  Toast,\n  ToastClose,\n  ToastDescription,\n  ToastProvider,\n  ToastTitle,\n  ToastViewport\n} from '@/components/ui/Toasts/toast';\nimport { useToast } from '@/components/ui/Toasts/use-toast';\nimport { usePathname, useRouter, useSearchParams } from 'next/navigation';\nimport { useEffect } from 'react';\n\nexport function Toaster() {\n  const { toast, toasts } = useToast();\n  const searchParams = useSearchParams();\n  const pathname = usePathname();\n  const router = useRouter();\n\n  useEffect(() => {\n    const status = searchParams.get('status');\n    const status_description = searchParams.get('status_description');\n    const error = searchParams.get('error');\n    const error_description = searchParams.get('error_description');\n    if (error || status) {\n      toast({\n        title: error\n          ? error ?? 'Hmm... Something went wrong.'\n          : status ?? 'Alright!',\n        description: error ? error_description : status_description,\n        variant: error ? 'destructive' : undefined\n      });\n      // Clear any 'error', 'status', 'status_description', and 'error_description' search params\n      // so that the toast doesn't show up again on refresh, but leave any other search params\n      // intact.\n      const newSearchParams = new URLSearchParams(searchParams.toString());\n      const paramsToRemove = [\n        'error',\n        'status',\n        'status_description',\n        'error_description'\n      ];\n      paramsToRemove.forEach((param) => newSearchParams.delete(param));\n      const redirectPath = `${pathname}?${newSearchParams.toString()}`;\n      router.replace(redirectPath, { scroll: false });\n    }\n  }, [searchParams]);\n\n  return (\n    <ToastProvider>\n      {toasts.map(function ({ id, title, description, action, ...props }) {\n        return (\n          <Toast key={id} {...props}>\n            <div className=\"grid gap-1\">\n              {title && <ToastTitle>{title}</ToastTitle>}\n              {description && (\n                <ToastDescription>{description}</ToastDescription>\n              )}\n            </div>\n            {action}\n            <ToastClose />\n          </Toast>\n        );\n      })}\n      <ToastViewport />\n    </ToastProvider>\n  );\n}\n"
  },
  {
    "path": "components/ui/Toasts/use-toast.ts",
    "content": "// Inspired by react-hot-toast library\nimport * as React from 'react';\n\nimport type {\n  ToastActionElement,\n  ToastProps\n} from '@/components/ui/Toasts/toast';\n\nconst TOAST_LIMIT = 1;\nconst TOAST_REMOVE_DELAY = 1000000;\n\ntype ToasterToast = ToastProps & {\n  id: string;\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  action?: ToastActionElement;\n};\n\nconst actionTypes = {\n  ADD_TOAST: 'ADD_TOAST',\n  UPDATE_TOAST: 'UPDATE_TOAST',\n  DISMISS_TOAST: 'DISMISS_TOAST',\n  REMOVE_TOAST: 'REMOVE_TOAST'\n} as const;\n\nlet count = 0;\n\nfunction genId() {\n  count = (count + 1) % Number.MAX_SAFE_INTEGER;\n  return count.toString();\n}\n\ntype ActionType = typeof actionTypes;\n\ntype Action =\n  | {\n      type: ActionType['ADD_TOAST'];\n      toast: ToasterToast;\n    }\n  | {\n      type: ActionType['UPDATE_TOAST'];\n      toast: Partial<ToasterToast>;\n    }\n  | {\n      type: ActionType['DISMISS_TOAST'];\n      toastId?: ToasterToast['id'];\n    }\n  | {\n      type: ActionType['REMOVE_TOAST'];\n      toastId?: ToasterToast['id'];\n    };\n\ninterface State {\n  toasts: ToasterToast[];\n}\n\nconst toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst addToRemoveQueue = (toastId: string) => {\n  if (toastTimeouts.has(toastId)) {\n    return;\n  }\n\n  const timeout = setTimeout(() => {\n    toastTimeouts.delete(toastId);\n    dispatch({\n      type: 'REMOVE_TOAST',\n      toastId: toastId\n    });\n  }, TOAST_REMOVE_DELAY);\n\n  toastTimeouts.set(toastId, timeout);\n};\n\nexport const reducer = (state: State, action: Action): State => {\n  switch (action.type) {\n    case 'ADD_TOAST':\n      return {\n        ...state,\n        toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT)\n      };\n\n    case 'UPDATE_TOAST':\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === action.toast.id ? { ...t, ...action.toast } : t\n        )\n      };\n\n    case 'DISMISS_TOAST': {\n      const { toastId } = action;\n\n      // ! Side effects ! - This could be extracted into a dismissToast() action,\n      // but I'll keep it here for simplicity\n      if (toastId) {\n        addToRemoveQueue(toastId);\n      } else {\n        state.toasts.forEach((toast) => {\n          addToRemoveQueue(toast.id);\n        });\n      }\n\n      return {\n        ...state,\n        toasts: state.toasts.map((t) =>\n          t.id === toastId || toastId === undefined\n            ? {\n                ...t,\n                open: false\n              }\n            : t\n        )\n      };\n    }\n    case 'REMOVE_TOAST':\n      if (action.toastId === undefined) {\n        return {\n          ...state,\n          toasts: []\n        };\n      }\n      return {\n        ...state,\n        toasts: state.toasts.filter((t) => t.id !== action.toastId)\n      };\n  }\n};\n\nconst listeners: Array<(state: State) => void> = [];\n\nlet memoryState: State = { toasts: [] };\n\nfunction dispatch(action: Action) {\n  memoryState = reducer(memoryState, action);\n  listeners.forEach((listener) => {\n    listener(memoryState);\n  });\n}\n\ntype Toast = Omit<ToasterToast, 'id'>;\n\nfunction toast({ ...props }: Toast) {\n  const id = genId();\n\n  const update = (props: ToasterToast) =>\n    dispatch({\n      type: 'UPDATE_TOAST',\n      toast: { ...props, id }\n    });\n  const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id });\n\n  dispatch({\n    type: 'ADD_TOAST',\n    toast: {\n      ...props,\n      id,\n      open: true,\n      onOpenChange: (open) => {\n        if (!open) dismiss();\n      }\n    }\n  });\n\n  return {\n    id: id,\n    dismiss,\n    update\n  };\n}\n\nfunction useToast() {\n  const [state, setState] = React.useState<State>(memoryState);\n\n  React.useEffect(() => {\n    listeners.push(setState);\n    return () => {\n      const index = listeners.indexOf(setState);\n      if (index > -1) {\n        listeners.splice(index, 1);\n      }\n    };\n  }, [state]);\n\n  return {\n    ...state,\n    toast,\n    dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId })\n  };\n}\n\nexport { useToast, toast };\n"
  },
  {
    "path": "components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"styles/main.css\",\n    \"baseColor\": \"zinc\",\n    \"cssVariables\": false\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/utils/cn\"\n  }\n}\n"
  },
  {
    "path": "fixtures/stripe-fixtures.json",
    "content": "{\n  \"_meta\": {\n    \"template_version\": 0\n  },\n  \"fixtures\": [\n    {\n      \"name\": \"prod_hobby\",\n      \"path\": \"/v1/products\",\n      \"method\": \"post\",\n      \"params\": {\n        \"name\": \"Hobby\",\n        \"description\": \"Hobby product description\",\n        \"metadata\": {\n          \"index\": 0\n        }\n      }\n    },\n    {\n      \"name\": \"price_hobby_month\",\n      \"path\": \"/v1/prices\",\n      \"method\": \"post\",\n      \"params\": {\n        \"product\": \"${prod_hobby:id}\",\n        \"currency\": \"usd\",\n        \"billing_scheme\": \"per_unit\",\n        \"unit_amount\": 1000,\n        \"recurring\": {\n          \"interval\": \"month\",\n          \"interval_count\": 1\n        }\n      }\n    },\n    {\n      \"name\": \"price_hobby_year\",\n      \"path\": \"/v1/prices\",\n      \"method\": \"post\",\n      \"params\": {\n        \"product\": \"${prod_hobby:id}\",\n        \"currency\": \"usd\",\n        \"billing_scheme\": \"per_unit\",\n        \"unit_amount\": 10000,\n        \"recurring\": {\n          \"interval\": \"year\",\n          \"interval_count\": 1\n        }\n      }\n    },\n    {\n      \"name\": \"prod_freelancer\",\n      \"path\": \"/v1/products\",\n      \"method\": \"post\",\n      \"params\": {\n        \"name\": \"Freelancer\",\n        \"description\": \"Freelancer product description\",\n        \"metadata\": {\n          \"index\": 1\n        }\n      }\n    },\n    {\n      \"name\": \"price_freelancer_month\",\n      \"path\": \"/v1/prices\",\n      \"method\": \"post\",\n      \"params\": {\n        \"product\": \"${prod_freelancer:id}\",\n        \"currency\": \"usd\",\n        \"billing_scheme\": \"per_unit\",\n        \"unit_amount\": 2000,\n        \"recurring\": {\n          \"interval\": \"month\",\n          \"interval_count\": 1\n        }\n      }\n    },\n    {\n      \"name\": \"price_freelancer_year\",\n      \"path\": \"/v1/prices\",\n      \"method\": \"post\",\n      \"params\": {\n        \"product\": \"${prod_freelancer:id}\",\n        \"currency\": \"usd\",\n        \"billing_scheme\": \"per_unit\",\n        \"unit_amount\": 20000,\n        \"recurring\": {\n          \"interval\": \"year\",\n          \"interval_count\": 1\n        }\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "middleware.ts",
    "content": "import { type NextRequest } from 'next/server';\nimport { updateSession } from '@/utils/supabase/middleware';\n\nexport async function middleware(request: NextRequest) {\n  return await updateSession(request);\n}\n\nexport const config = {\n  matcher: [\n    /*\n     * Match all request paths except:\n     * - _next/static (static files)\n     * - _next/image (image optimization files)\n     * - favicon.ico (favicon file)\n     * - images - .svg, .png, .jpg, .jpeg, .gif, .webp\n     * Feel free to modify this pattern to include more paths.\n     */\n    '/((?!_next/static|_next/image|favicon.ico|.*\\\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)'\n  ]\n};\n"
  },
  {
    "path": "next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/basic-features/typescript for more information.\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev --turbo\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"lint\": \"next lint\",\n    \"prettier-fix\": \"prettier --write .\",\n    \"stripe:login\": \"stripe login\",\n    \"stripe:listen\": \"stripe listen --forward-to=localhost:3000/api/webhooks\",\n    \"stripe:fixtures\": \"stripe fixtures fixtures/stripe-fixtures.json\",\n    \"supabase:start\": \"npx supabase start\",\n    \"supabase:stop\": \"npx supabase stop\",\n    \"supabase:status\": \"npx supabase status\",\n    \"supabase:restart\": \"npm run supabase:stop && npm run supabase:start\",\n    \"supabase:reset\": \"npx supabase db reset\",\n    \"supabase:link\": \"npx supabase link\",\n    \"supabase:generate-types\": \"npx supabase gen types typescript --local --schema public > types_db.ts\",\n    \"supabase:generate-migration\": \"npx supabase db diff | npx supabase migration new\",\n    \"supabase:generate-seed\": \"npx supabase db dump --data-only -f supabase/seed.sql\",\n    \"supabase:push\": \"npx supabase db push\",\n    \"supabase:pull\": \"npx supabase db pull\"\n  },\n  \"dependencies\": {\n    \"@radix-ui/react-toast\": \"^1.1.5\",\n    \"@stripe/stripe-js\": \"2.4.0\",\n    \"@supabase/ssr\": \"^0.1.0\",\n    \"@supabase/supabase-js\": \"^2.43.4\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"classnames\": \"^2.5.1\",\n    \"clsx\": \"^2.1.1\",\n    \"lucide-react\": \"0.330.0\",\n    \"next\": \"14.2.3\",\n    \"react\": \"^18.3.1\",\n    \"react-dom\": \"^18.3.1\",\n    \"react-merge-refs\": \"^2.1.1\",\n    \"stripe\": \"^14.25.0\",\n    \"tailwind-merge\": \"^2.3.0\",\n    \"tailwindcss\": \"^3.4.4\",\n    \"tailwindcss-animate\": \"^1.0.7\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^20.14.2\",\n    \"@types/react\": \"^18.3.3\",\n    \"@types/react-dom\": \"^18.3.0\",\n    \"autoprefixer\": \"^10.4.19\",\n    \"eslint\": \"^8.57.0\",\n    \"eslint-config-next\": \"14.1.0\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-plugin-react\": \"^7.34.2\",\n    \"eslint-plugin-tailwindcss\": \"^3.17.3\",\n    \"postcss\": \"^8.4.38\",\n    \"prettier\": \"^3.3.1\",\n    \"prettier-plugin-tailwindcss\": \"^0.5.14\",\n    \"supabase\": \"^1.172.2\",\n    \"typescript\": \"^5.4.5\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {}\n  }\n};\n"
  },
  {
    "path": "schema.sql",
    "content": "/** \n* USERS\n* Note: This table contains user data. Users should only be able to view and update their own data.\n*/\ncreate table users (\n  -- UUID from auth.users\n  id uuid references auth.users not null primary key,\n  full_name text,\n  avatar_url text,\n  -- The customer's billing address, stored in JSON format.\n  billing_address jsonb,\n  -- Stores your customer's payment instruments.\n  payment_method jsonb\n);\nalter table users enable row level security;\ncreate policy \"Can view own user data.\" on users for select using (auth.uid() = id);\ncreate policy \"Can update own user data.\" on users for update using (auth.uid() = id);\n\n/**\n* This trigger automatically creates a user entry when a new user signs up via Supabase Auth.\n*/ \ncreate function public.handle_new_user() \nreturns trigger as $$\nbegin\n  insert into public.users (id, full_name, avatar_url)\n  values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');\n  return new;\nend;\n$$ language plpgsql security definer;\ncreate trigger on_auth_user_created\n  after insert on auth.users\n  for each row execute procedure public.handle_new_user();\n\n/**\n* CUSTOMERS\n* Note: this is a private table that contains a mapping of user IDs to Stripe customer IDs.\n*/\ncreate table customers (\n  -- UUID from auth.users\n  id uuid references auth.users not null primary key,\n  -- The user's customer ID in Stripe. User must not be able to update this.\n  stripe_customer_id text\n);\nalter table customers enable row level security;\n-- No policies as this is a private table that the user must not have access to.\n\n/** \n* PRODUCTS\n* Note: products are created and managed in Stripe and synced to our DB via Stripe webhooks.\n*/\ncreate table products (\n  -- Product ID from Stripe, e.g. prod_1234.\n  id text primary key,\n  -- Whether the product is currently available for purchase.\n  active boolean,\n  -- The product's name, meant to be displayable to the customer. Whenever this product is sold via a subscription, name will show up on associated invoice line item descriptions.\n  name text,\n  -- The product's description, meant to be displayable to the customer. Use this field to optionally store a long form explanation of the product being sold for your own rendering purposes.\n  description text,\n  -- A URL of the product image in Stripe, meant to be displayable to the customer.\n  image text,\n  -- Set of key-value pairs, used to store additional information about the object in a structured format.\n  metadata jsonb\n);\nalter table products enable row level security;\ncreate policy \"Allow public read-only access.\" on products for select using (true);\n\n/**\n* PRICES\n* Note: prices are created and managed in Stripe and synced to our DB via Stripe webhooks.\n*/\ncreate type pricing_type as enum ('one_time', 'recurring');\ncreate type pricing_plan_interval as enum ('day', 'week', 'month', 'year');\ncreate table prices (\n  -- Price ID from Stripe, e.g. price_1234.\n  id text primary key,\n  -- The ID of the prduct that this price belongs to.\n  product_id text references products, \n  -- Whether the price can be used for new purchases.\n  active boolean,\n  -- A brief description of the price.\n  description text,\n  -- The unit amount as a positive integer in the smallest currency unit (e.g., 100 cents for US$1.00 or 100 for ¥100, a zero-decimal currency).\n  unit_amount bigint,\n  -- Three-letter ISO currency code, in lowercase.\n  currency text check (char_length(currency) = 3),\n  -- One of `one_time` or `recurring` depending on whether the price is for a one-time purchase or a recurring (subscription) purchase.\n  type pricing_type,\n  -- The frequency at which a subscription is billed. One of `day`, `week`, `month` or `year`.\n  interval pricing_plan_interval,\n  -- The number of intervals (specified in the `interval` attribute) between subscription billings. For example, `interval=month` and `interval_count=3` bills every 3 months.\n  interval_count integer,\n  -- Default number of trial days when subscribing a customer to this price using [`trial_from_plan=true`](https://stripe.com/docs/api#create_subscription-trial_from_plan).\n  trial_period_days integer,\n  -- Set of key-value pairs, used to store additional information about the object in a structured format.\n  metadata jsonb\n);\nalter table prices enable row level security;\ncreate policy \"Allow public read-only access.\" on prices for select using (true);\n\n/**\n* SUBSCRIPTIONS\n* Note: subscriptions are created and managed in Stripe and synced to our DB via Stripe webhooks.\n*/\ncreate type subscription_status as enum ('trialing', 'active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid', 'paused');\ncreate table subscriptions (\n  -- Subscription ID from Stripe, e.g. sub_1234.\n  id text primary key,\n  user_id uuid references auth.users not null,\n  -- The status of the subscription object, one of subscription_status type above.\n  status subscription_status,\n  -- Set of key-value pairs, used to store additional information about the object in a structured format.\n  metadata jsonb,\n  -- ID of the price that created this subscription.\n  price_id text references prices,\n  -- Quantity multiplied by the unit amount of the price creates the amount of the subscription. Can be used to charge multiple seats.\n  quantity integer,\n  -- If true the subscription has been canceled by the user and will be deleted at the end of the billing period.\n  cancel_at_period_end boolean,\n  -- Time at which the subscription was created.\n  created timestamp with time zone default timezone('utc'::text, now()) not null,\n  -- Start of the current period that the subscription has been invoiced for.\n  current_period_start timestamp with time zone default timezone('utc'::text, now()) not null,\n  -- End of the current period that the subscription has been invoiced for. At the end of this period, a new invoice will be created.\n  current_period_end timestamp with time zone default timezone('utc'::text, now()) not null,\n  -- If the subscription has ended, the timestamp of the date the subscription ended.\n  ended_at timestamp with time zone default timezone('utc'::text, now()),\n  -- A date in the future at which the subscription will automatically get canceled.\n  cancel_at timestamp with time zone default timezone('utc'::text, now()),\n  -- If the subscription has been canceled, the date of that cancellation. If the subscription was canceled with `cancel_at_period_end`, `canceled_at` will still reflect the date of the initial cancellation request, not the end of the subscription period when the subscription is automatically moved to a canceled state.\n  canceled_at timestamp with time zone default timezone('utc'::text, now()),\n  -- If the subscription has a trial, the beginning of that trial.\n  trial_start timestamp with time zone default timezone('utc'::text, now()),\n  -- If the subscription has a trial, the end of that trial.\n  trial_end timestamp with time zone default timezone('utc'::text, now())\n);\nalter table subscriptions enable row level security;\ncreate policy \"Can only view own subs data.\" on subscriptions for select using (auth.uid() = user_id);\n\n/**\n * REALTIME SUBSCRIPTIONS\n * Only allow realtime listening on public tables.\n */\ndrop publication if exists supabase_realtime;\ncreate publication supabase_realtime for table products, prices;"
  },
  {
    "path": "styles/main.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n*,\n*:before,\n*:after {\n  box-sizing: inherit;\n}\n\n*:focus:not(ol) {\n  @apply outline-none ring-2 ring-pink-500 ring-opacity-50;\n}\n\nhtml {\n  height: 100%;\n  box-sizing: border-box;\n  touch-action: manipulation;\n  font-feature-settings:\n    'case' 1,\n    'rlig' 1,\n    'calt' 0;\n}\n\nhtml,\nbody {\n  font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Helvetica Neue',\n    'Helvetica', sans-serif;\n  text-rendering: optimizeLegibility;\n  -moz-osx-font-smoothing: grayscale;\n  @apply text-white bg-zinc-800 antialiased;\n}\n\nbody {\n  position: relative;\n  min-height: 100%;\n  margin: 0;\n}\n\na {\n  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\n\np a {\n  @apply hover:underline;\n}\n\n.animated {\n  -webkit-animation-duration: 1s;\n  animation-duration: 1s;\n  -webkit-animation-duration: 1s;\n  animation-duration: 1s;\n  -webkit-animation-fill-mode: both;\n  animation-fill-mode: both;\n}\n\n.height-screen-helper {\n  min-height: calc(100vh - 80px);\n}\n"
  },
  {
    "path": "supabase/.gitignore",
    "content": "# Supabase\n.branches\n.temp\n.env\n"
  },
  {
    "path": "supabase/config.toml",
    "content": "# A string used to distinguish different Supabase projects on the same host. Defaults to the\n# working directory name when running `supabase init`.\nproject_id = \"nextjs-subscription-payments\"\n\n[api]\nenabled = true\n# Port to use for the API URL.\nport = 54321\n# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API\n# endpoints. public and storage are always included.\nschemas = [\"public\", \"storage\", \"graphql_public\"]\n# Extra schemas to add to the search_path of every request. public is always included.\nextra_search_path = [\"public\", \"extensions\"]\n# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size\n# for accidental or malicious requests.\nmax_rows = 1000\n\n[db]\n# Port to use for the local database URL.\nport = 54322\n# Port used by db diff command to initialize the shadow database.\nshadow_port = 54320\n# The database major version to use. This has to be the same as your remote database's. Run `SHOW\n# server_version;` on the remote database to check.\nmajor_version = 15\n\n[db.pooler]\nenabled = false\n# Port to use for the local connection pooler.\nport = 54329\n# Specifies when a server connection can be reused by other clients.\n# Configure one of the supported pooler modes: `transaction`, `session`.\npool_mode = \"transaction\"\n# How many server connections to allow per user/database pair.\ndefault_pool_size = 20\n# Maximum number of client connections allowed.\nmax_client_conn = 100\n\n[realtime]\nenabled = true\n# Bind realtime via either IPv4 or IPv6. (default: IPv6)\n# ip_version = \"IPv6\"\n# The maximum length in bytes of HTTP request headers. (default: 4096)\n# max_header_length = 4096\n\n[studio]\nenabled = true\n# Port to use for Supabase Studio.\nport = 54323\n# External URL of the API server that frontend connects to.\napi_url = \"http://127.0.0.1\"\n\n# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they\n# are monitored, and you can view the emails that would have been sent from the web interface.\n[inbucket]\nenabled = true\n# Port to use for the email testing server web interface.\nport = 54324\n# Uncomment to expose additional ports for testing user applications that send emails.\n# smtp_port = 54325\n# pop3_port = 54326\n\n[storage]\nenabled = true\n# The maximum file size allowed (e.g. \"5MB\", \"500KB\").\nfile_size_limit = \"50MiB\"\n\n[auth]\nenabled = true\n# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used\n# in emails.\nsite_url = \"http://localhost:3000\"\n# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.\nadditional_redirect_urls = [\"https://127.0.0.1:3000\"]\n# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).\njwt_expiry = 3600\n# If disabled, the refresh token will never expire.\nenable_refresh_token_rotation = true\n# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.\n# Requires enable_refresh_token_rotation = true.\nrefresh_token_reuse_interval = 10\n# Allow/disallow new user signups to your project.\nenable_signup = true\n# Allow/disallow testing manual linking of accounts\nenable_manual_linking = false\n\n[auth.email]\n# Allow/disallow new user signups via email to your project.\nenable_signup = true\n# If enabled, a user will be required to confirm any email change on both the old, and new email\n# addresses. If disabled, only the new email is required to confirm.\ndouble_confirm_changes = true\n# If enabled, users need to confirm their email address before signing in.\nenable_confirmations = false\n\n# Uncomment to customize email template\n# [auth.email.template.invite]\n# subject = \"You have been invited\"\n# content_path = \"./supabase/templates/invite.html\"\n\n[auth.sms]\n# Allow/disallow new user signups via SMS to your project.\nenable_signup = true\n# If enabled, users need to confirm their phone number before signing in.\nenable_confirmations = false\n# Template for sending OTP to users\ntemplate = \"Your code is {{ .Code }} .\"\n\n# Use pre-defined map of phone number to OTP for testing.\n[auth.sms.test_otp]\n# 4152127777 = \"123456\"\n\n# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.\n[auth.hook.custom_access_token]\n# enabled = true\n# uri = \"pg-functions://<database>/<schema>/<hook_name>\"\n\n\n# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.\n[auth.sms.twilio]\nenabled = false\naccount_sid = \"\"\nmessage_service_sid = \"\"\n# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:\nauth_token = \"env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)\"\n\n# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,\n# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,\n# `twitter`, `slack`, `spotify`, `workos`, `zoom`.\n[auth.external.github]\nenabled = true\nclient_id = \"env(SUPABASE_AUTH_EXTERNAL_GITHUB_CLIENT_ID)\"\n# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:\nsecret = \"env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)\"\n# Overrides the default auth redirectUrl.\nredirect_uri = \"env(SUPABASE_AUTH_EXTERNAL_GITHUB_REDIRECT_URI)\"\n# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,\n# or any other third-party OIDC providers.\nurl = \"\"\n\n[analytics]\nenabled = false\nport = 54327\nvector_port = 54328\n# Configure one of the supported backends: `postgres`, `bigquery`.\nbackend = \"postgres\"\n\n# Experimental features may be deprecated any time\n[experimental]\n# Configures Postgres storage engine to use OrioleDB (S3)\norioledb_version = \"\"\n# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com\ns3_host = \"env(S3_HOST)\"\n# Configures S3 bucket region, eg. us-east-1\ns3_region = \"env(S3_REGION)\"\n# Configures AWS_ACCESS_KEY_ID for S3 bucket\ns3_access_key = \"env(S3_ACCESS_KEY)\"\n# Configures AWS_SECRET_ACCESS_KEY for S3 bucket\ns3_secret_key = \"env(S3_SECRET_KEY)\"\n"
  },
  {
    "path": "supabase/migrations/20230530034630_init.sql",
    "content": "/** \n* USERS\n* Note: This table contains user data. Users should only be able to view and update their own data.\n*/\ncreate table users (\n  -- UUID from auth.users\n  id uuid references auth.users not null primary key,\n  full_name text,\n  avatar_url text,\n  -- The customer's billing address, stored in JSON format.\n  billing_address jsonb,\n  -- Stores your customer's payment instruments.\n  payment_method jsonb\n);\nalter table users enable row level security;\ncreate policy \"Can view own user data.\" on users for select using (auth.uid() = id);\ncreate policy \"Can update own user data.\" on users for update using (auth.uid() = id);\n\n/**\n* This trigger automatically creates a user entry when a new user signs up via Supabase Auth.\n*/ \ncreate function public.handle_new_user() \nreturns trigger as $$\nbegin\n  insert into public.users (id, full_name, avatar_url)\n  values (new.id, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url');\n  return new;\nend;\n$$ language plpgsql security definer;\ncreate trigger on_auth_user_created\n  after insert on auth.users\n  for each row execute procedure public.handle_new_user();\n\n/**\n* CUSTOMERS\n* Note: this is a private table that contains a mapping of user IDs to Stripe customer IDs.\n*/\ncreate table customers (\n  -- UUID from auth.users\n  id uuid references auth.users not null primary key,\n  -- The user's customer ID in Stripe. User must not be able to update this.\n  stripe_customer_id text\n);\nalter table customers enable row level security;\n-- No policies as this is a private table that the user must not have access to.\n\n/** \n* PRODUCTS\n* Note: products are created and managed in Stripe and synced to our DB via Stripe webhooks.\n*/\ncreate table products (\n  -- Product ID from Stripe, e.g. prod_1234.\n  id text primary key,\n  -- Whether the product is currently available for purchase.\n  active boolean,\n  -- The product's name, meant to be displayable to the customer. Whenever this product is sold via a subscription, name will show up on associated invoice line item descriptions.\n  name text,\n  -- The product's description, meant to be displayable to the customer. Use this field to optionally store a long form explanation of the product being sold for your own rendering purposes.\n  description text,\n  -- A URL of the product image in Stripe, meant to be displayable to the customer.\n  image text,\n  -- Set of key-value pairs, used to store additional information about the object in a structured format.\n  metadata jsonb\n);\nalter table products enable row level security;\ncreate policy \"Allow public read-only access.\" on products for select using (true);\n\n/**\n* PRICES\n* Note: prices are created and managed in Stripe and synced to our DB via Stripe webhooks.\n*/\ncreate type pricing_type as enum ('one_time', 'recurring');\ncreate type pricing_plan_interval as enum ('day', 'week', 'month', 'year');\ncreate table prices (\n  -- Price ID from Stripe, e.g. price_1234.\n  id text primary key,\n  -- The ID of the prduct that this price belongs to.\n  product_id text references products, \n  -- Whether the price can be used for new purchases.\n  active boolean,\n  -- A brief description of the price.\n  description text,\n  -- The unit amount as a positive integer in the smallest currency unit (e.g., 100 cents for US$1.00 or 100 for ¥100, a zero-decimal currency).\n  unit_amount bigint,\n  -- Three-letter ISO currency code, in lowercase.\n  currency text check (char_length(currency) = 3),\n  -- One of `one_time` or `recurring` depending on whether the price is for a one-time purchase or a recurring (subscription) purchase.\n  type pricing_type,\n  -- The frequency at which a subscription is billed. One of `day`, `week`, `month` or `year`.\n  interval pricing_plan_interval,\n  -- The number of intervals (specified in the `interval` attribute) between subscription billings. For example, `interval=month` and `interval_count=3` bills every 3 months.\n  interval_count integer,\n  -- Default number of trial days when subscribing a customer to this price using [`trial_from_plan=true`](https://stripe.com/docs/api#create_subscription-trial_from_plan).\n  trial_period_days integer,\n  -- Set of key-value pairs, used to store additional information about the object in a structured format.\n  metadata jsonb\n);\nalter table prices enable row level security;\ncreate policy \"Allow public read-only access.\" on prices for select using (true);\n\n/**\n* SUBSCRIPTIONS\n* Note: subscriptions are created and managed in Stripe and synced to our DB via Stripe webhooks.\n*/\ncreate type subscription_status as enum ('trialing', 'active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'unpaid', 'paused');\ncreate table subscriptions (\n  -- Subscription ID from Stripe, e.g. sub_1234.\n  id text primary key,\n  user_id uuid references auth.users not null,\n  -- The status of the subscription object, one of subscription_status type above.\n  status subscription_status,\n  -- Set of key-value pairs, used to store additional information about the object in a structured format.\n  metadata jsonb,\n  -- ID of the price that created this subscription.\n  price_id text references prices,\n  -- Quantity multiplied by the unit amount of the price creates the amount of the subscription. Can be used to charge multiple seats.\n  quantity integer,\n  -- If true the subscription has been canceled by the user and will be deleted at the end of the billing period.\n  cancel_at_period_end boolean,\n  -- Time at which the subscription was created.\n  created timestamp with time zone default timezone('utc'::text, now()) not null,\n  -- Start of the current period that the subscription has been invoiced for.\n  current_period_start timestamp with time zone default timezone('utc'::text, now()) not null,\n  -- End of the current period that the subscription has been invoiced for. At the end of this period, a new invoice will be created.\n  current_period_end timestamp with time zone default timezone('utc'::text, now()) not null,\n  -- If the subscription has ended, the timestamp of the date the subscription ended.\n  ended_at timestamp with time zone default timezone('utc'::text, now()),\n  -- A date in the future at which the subscription will automatically get canceled.\n  cancel_at timestamp with time zone default timezone('utc'::text, now()),\n  -- If the subscription has been canceled, the date of that cancellation. If the subscription was canceled with `cancel_at_period_end`, `canceled_at` will still reflect the date of the initial cancellation request, not the end of the subscription period when the subscription is automatically moved to a canceled state.\n  canceled_at timestamp with time zone default timezone('utc'::text, now()),\n  -- If the subscription has a trial, the beginning of that trial.\n  trial_start timestamp with time zone default timezone('utc'::text, now()),\n  -- If the subscription has a trial, the end of that trial.\n  trial_end timestamp with time zone default timezone('utc'::text, now())\n);\nalter table subscriptions enable row level security;\ncreate policy \"Can only view own subs data.\" on subscriptions for select using (auth.uid() = user_id);\n\n/**\n * REALTIME SUBSCRIPTIONS\n * Only allow realtime listening on public tables.\n */\ndrop publication if exists supabase_realtime;\ncreate publication supabase_realtime for table products, prices;"
  },
  {
    "path": "supabase/seed.sql",
    "content": ""
  },
  {
    "path": "tailwind.config.js",
    "content": "const { fontFamily } = require('tailwindcss/defaultTheme');\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: ['class', '[data-theme=\"dark\"]'],\n  content: [\n    'app/**/*.{ts,tsx}',\n    'components/**/*.{ts,tsx}',\n    'pages/**/*.{ts,tsx}'\n  ],\n  theme: {\n    container: {\n      center: true,\n      padding: '2rem',\n      screens: {\n        '2xl': '1400px'\n      }\n    },\n    extend: {\n      fontFamily: {\n        sans: ['var(--font-sans)', ...fontFamily.sans]\n      },\n      keyframes: {\n        'accordion-down': {\n          from: { height: 0 },\n          to: { height: 'var(--radix-accordion-content-height)' }\n        },\n        'accordion-up': {\n          from: { height: 'var(--radix-accordion-content-height)' },\n          to: { height: 0 }\n        }\n      },\n      animation: {\n        'accordion-down': 'accordion-down 0.2s ease-out',\n        'accordion-up': 'accordion-up 0.2s ease-out'\n      }\n    }\n  },\n  plugins: [require('tailwindcss-animate')]\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    },\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "utils/auth-helpers/client.ts",
    "content": "'use client';\n\nimport { createClient } from '@/utils/supabase/client';\nimport { type Provider } from '@supabase/supabase-js';\nimport { getURL } from '@/utils/helpers';\nimport { redirectToPath } from './server';\nimport { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';\n\nexport async function handleRequest(\n  e: React.FormEvent<HTMLFormElement>,\n  requestFunc: (formData: FormData) => Promise<string>,\n  router: AppRouterInstance | null = null\n): Promise<boolean | void> {\n  // Prevent default form submission refresh\n  e.preventDefault();\n\n  const formData = new FormData(e.currentTarget);\n  const redirectUrl: string = await requestFunc(formData);\n\n  if (router) {\n    // If client-side router is provided, use it to redirect\n    return router.push(redirectUrl);\n  } else {\n    // Otherwise, redirect server-side\n    return await redirectToPath(redirectUrl);\n  }\n}\n\nexport async function signInWithOAuth(e: React.FormEvent<HTMLFormElement>) {\n  // Prevent default form submission refresh\n  e.preventDefault();\n  const formData = new FormData(e.currentTarget);\n  const provider = String(formData.get('provider')).trim() as Provider;\n\n  // Create client-side supabase client and call signInWithOAuth\n  const supabase = createClient();\n  const redirectURL = getURL('/auth/callback');\n  await supabase.auth.signInWithOAuth({\n    provider: provider,\n    options: {\n      redirectTo: redirectURL\n    }\n  });\n}\n"
  },
  {
    "path": "utils/auth-helpers/server.ts",
    "content": "'use server';\n\nimport { createClient } from '@/utils/supabase/server';\nimport { cookies } from 'next/headers';\nimport { redirect } from 'next/navigation';\nimport { getURL, getErrorRedirect, getStatusRedirect } from 'utils/helpers';\nimport { getAuthTypes } from 'utils/auth-helpers/settings';\n\nfunction isValidEmail(email: string) {\n  var regex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}$/;\n  return regex.test(email);\n}\n\nexport async function redirectToPath(path: string) {\n  return redirect(path);\n}\n\nexport async function SignOut(formData: FormData) {\n  const pathName = String(formData.get('pathName')).trim();\n\n  const supabase = createClient();\n  const { error } = await supabase.auth.signOut();\n\n  if (error) {\n    return getErrorRedirect(\n      pathName,\n      'Hmm... Something went wrong.',\n      'You could not be signed out.'\n    );\n  }\n\n  return '/signin';\n}\n\nexport async function signInWithEmail(formData: FormData) {\n  const cookieStore = cookies();\n  const callbackURL = getURL('/auth/callback');\n\n  const email = String(formData.get('email')).trim();\n  let redirectPath: string;\n\n  if (!isValidEmail(email)) {\n    redirectPath = getErrorRedirect(\n      '/signin/email_signin',\n      'Invalid email address.',\n      'Please try again.'\n    );\n  }\n\n  const supabase = createClient();\n  let options = {\n    emailRedirectTo: callbackURL,\n    shouldCreateUser: true\n  };\n\n  // If allowPassword is false, do not create a new user\n  const { allowPassword } = getAuthTypes();\n  if (allowPassword) options.shouldCreateUser = false;\n  const { data, error } = await supabase.auth.signInWithOtp({\n    email,\n    options: options\n  });\n\n  if (error) {\n    redirectPath = getErrorRedirect(\n      '/signin/email_signin',\n      'You could not be signed in.',\n      error.message\n    );\n  } else if (data) {\n    cookieStore.set('preferredSignInView', 'email_signin', { path: '/' });\n    redirectPath = getStatusRedirect(\n      '/signin/email_signin',\n      'Success!',\n      'Please check your email for a magic link. You may now close this tab.',\n      true\n    );\n  } else {\n    redirectPath = getErrorRedirect(\n      '/signin/email_signin',\n      'Hmm... Something went wrong.',\n      'You could not be signed in.'\n    );\n  }\n\n  return redirectPath;\n}\n\nexport async function requestPasswordUpdate(formData: FormData) {\n  const callbackURL = getURL('/auth/reset_password');\n\n  // Get form data\n  const email = String(formData.get('email')).trim();\n  let redirectPath: string;\n\n  if (!isValidEmail(email)) {\n    redirectPath = getErrorRedirect(\n      '/signin/forgot_password',\n      'Invalid email address.',\n      'Please try again.'\n    );\n  }\n\n  const supabase = createClient();\n\n  const { data, error } = await supabase.auth.resetPasswordForEmail(email, {\n    redirectTo: callbackURL\n  });\n\n  if (error) {\n    redirectPath = getErrorRedirect(\n      '/signin/forgot_password',\n      error.message,\n      'Please try again.'\n    );\n  } else if (data) {\n    redirectPath = getStatusRedirect(\n      '/signin/forgot_password',\n      'Success!',\n      'Please check your email for a password reset link. You may now close this tab.',\n      true\n    );\n  } else {\n    redirectPath = getErrorRedirect(\n      '/signin/forgot_password',\n      'Hmm... Something went wrong.',\n      'Password reset email could not be sent.'\n    );\n  }\n\n  return redirectPath;\n}\n\nexport async function signInWithPassword(formData: FormData) {\n  const cookieStore = cookies();\n  const email = String(formData.get('email')).trim();\n  const password = String(formData.get('password')).trim();\n  let redirectPath: string;\n\n  const supabase = createClient();\n  const { error, data } = await supabase.auth.signInWithPassword({\n    email,\n    password\n  });\n\n  if (error) {\n    redirectPath = getErrorRedirect(\n      '/signin/password_signin',\n      'Sign in failed.',\n      error.message\n    );\n  } else if (data.user) {\n    cookieStore.set('preferredSignInView', 'password_signin', { path: '/' });\n    redirectPath = getStatusRedirect('/', 'Success!', 'You are now signed in.');\n  } else {\n    redirectPath = getErrorRedirect(\n      '/signin/password_signin',\n      'Hmm... Something went wrong.',\n      'You could not be signed in.'\n    );\n  }\n\n  return redirectPath;\n}\n\nexport async function signUp(formData: FormData) {\n  const callbackURL = getURL('/auth/callback');\n\n  const email = String(formData.get('email')).trim();\n  const password = String(formData.get('password')).trim();\n  let redirectPath: string;\n\n  if (!isValidEmail(email)) {\n    redirectPath = getErrorRedirect(\n      '/signin/signup',\n      'Invalid email address.',\n      'Please try again.'\n    );\n  }\n\n  const supabase = createClient();\n  const { error, data } = await supabase.auth.signUp({\n    email,\n    password,\n    options: {\n      emailRedirectTo: callbackURL\n    }\n  });\n\n  if (error) {\n    redirectPath = getErrorRedirect(\n      '/signin/signup',\n      'Sign up failed.',\n      error.message\n    );\n  } else if (data.session) {\n    redirectPath = getStatusRedirect('/', 'Success!', 'You are now signed in.');\n  } else if (\n    data.user &&\n    data.user.identities &&\n    data.user.identities.length == 0\n  ) {\n    redirectPath = getErrorRedirect(\n      '/signin/signup',\n      'Sign up failed.',\n      'There is already an account associated with this email address. Try resetting your password.'\n    );\n  } else if (data.user) {\n    redirectPath = getStatusRedirect(\n      '/',\n      'Success!',\n      'Please check your email for a confirmation link. You may now close this tab.'\n    );\n  } else {\n    redirectPath = getErrorRedirect(\n      '/signin/signup',\n      'Hmm... Something went wrong.',\n      'You could not be signed up.'\n    );\n  }\n\n  return redirectPath;\n}\n\nexport async function updatePassword(formData: FormData) {\n  const password = String(formData.get('password')).trim();\n  const passwordConfirm = String(formData.get('passwordConfirm')).trim();\n  let redirectPath: string;\n\n  // Check that the password and confirmation match\n  if (password !== passwordConfirm) {\n    redirectPath = getErrorRedirect(\n      '/signin/update_password',\n      'Your password could not be updated.',\n      'Passwords do not match.'\n    );\n  }\n\n  const supabase = createClient();\n  const { error, data } = await supabase.auth.updateUser({\n    password\n  });\n\n  if (error) {\n    redirectPath = getErrorRedirect(\n      '/signin/update_password',\n      'Your password could not be updated.',\n      error.message\n    );\n  } else if (data.user) {\n    redirectPath = getStatusRedirect(\n      '/',\n      'Success!',\n      'Your password has been updated.'\n    );\n  } else {\n    redirectPath = getErrorRedirect(\n      '/signin/update_password',\n      'Hmm... Something went wrong.',\n      'Your password could not be updated.'\n    );\n  }\n\n  return redirectPath;\n}\n\nexport async function updateEmail(formData: FormData) {\n  // Get form data\n  const newEmail = String(formData.get('newEmail')).trim();\n\n  // Check that the email is valid\n  if (!isValidEmail(newEmail)) {\n    return getErrorRedirect(\n      '/account',\n      'Your email could not be updated.',\n      'Invalid email address.'\n    );\n  }\n\n  const supabase = createClient();\n\n  const callbackUrl = getURL(\n    getStatusRedirect('/account', 'Success!', `Your email has been updated.`)\n  );\n\n  const { error } = await supabase.auth.updateUser(\n    { email: newEmail },\n    {\n      emailRedirectTo: callbackUrl\n    }\n  );\n\n  if (error) {\n    return getErrorRedirect(\n      '/account',\n      'Your email could not be updated.',\n      error.message\n    );\n  } else {\n    return getStatusRedirect(\n      '/account',\n      'Confirmation emails sent.',\n      `You will need to confirm the update by clicking the links sent to both the old and new email addresses.`\n    );\n  }\n}\n\nexport async function updateName(formData: FormData) {\n  // Get form data\n  const fullName = String(formData.get('fullName')).trim();\n\n  const supabase = createClient();\n  const { error, data } = await supabase.auth.updateUser({\n    data: { full_name: fullName }\n  });\n\n  if (error) {\n    return getErrorRedirect(\n      '/account',\n      'Your name could not be updated.',\n      error.message\n    );\n  } else if (data.user) {\n    return getStatusRedirect(\n      '/account',\n      'Success!',\n      'Your name has been updated.'\n    );\n  } else {\n    return getErrorRedirect(\n      '/account',\n      'Hmm... Something went wrong.',\n      'Your name could not be updated.'\n    );\n  }\n}\n"
  },
  {
    "path": "utils/auth-helpers/settings.ts",
    "content": "// Boolean toggles to determine which auth types are allowed\nconst allowOauth = true;\nconst allowEmail = true;\nconst allowPassword = true;\n\n// Boolean toggle to determine whether auth interface should route through server or client\n// (Currently set to false because screen sometimes flickers with server redirects)\nconst allowServerRedirect = false;\n\n// Check that at least one of allowPassword and allowEmail is true\nif (!allowPassword && !allowEmail)\n  throw new Error('At least one of allowPassword and allowEmail must be true');\n\nexport const getAuthTypes = () => {\n  return { allowOauth, allowEmail, allowPassword };\n};\n\nexport const getViewTypes = () => {\n  // Define the valid view types\n  let viewTypes: string[] = [];\n  if (allowEmail) {\n    viewTypes = [...viewTypes, 'email_signin'];\n  }\n  if (allowPassword) {\n    viewTypes = [\n      ...viewTypes,\n      'password_signin',\n      'forgot_password',\n      'update_password',\n      'signup'\n    ];\n  }\n\n  return viewTypes;\n};\n\nexport const getDefaultSignInView = (preferredSignInView: string | null) => {\n  // Define the default sign in view\n  let defaultView = allowPassword ? 'password_signin' : 'email_signin';\n  if (preferredSignInView && getViewTypes().includes(preferredSignInView)) {\n    defaultView = preferredSignInView;\n  }\n\n  return defaultView;\n};\n\nexport const getRedirectMethod = () => {\n  return allowServerRedirect ? 'server' : 'client';\n};\n"
  },
  {
    "path": "utils/cn.ts",
    "content": "import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "utils/helpers.ts",
    "content": "import type { Tables } from '@/types_db';\n\ntype Price = Tables<'prices'>;\n\nexport const getURL = (path: string = '') => {\n  // Check if NEXT_PUBLIC_SITE_URL is set and non-empty. Set this to your site URL in production env.\n  let url =\n    process?.env?.NEXT_PUBLIC_SITE_URL &&\n    process.env.NEXT_PUBLIC_SITE_URL.trim() !== ''\n      ? process.env.NEXT_PUBLIC_SITE_URL\n      : // If not set, check for NEXT_PUBLIC_VERCEL_URL, which is automatically set by Vercel.\n        process?.env?.NEXT_PUBLIC_VERCEL_URL &&\n          process.env.NEXT_PUBLIC_VERCEL_URL.trim() !== ''\n        ? process.env.NEXT_PUBLIC_VERCEL_URL\n        : // If neither is set, default to localhost for local development.\n          'http://localhost:3000/';\n\n  // Trim the URL and remove trailing slash if exists.\n  url = url.replace(/\\/+$/, '');\n  // Make sure to include `https://` when not localhost.\n  url = url.includes('http') ? url : `https://${url}`;\n  // Ensure path starts without a slash to avoid double slashes in the final URL.\n  path = path.replace(/^\\/+/, '');\n\n  // Concatenate the URL and the path.\n  return path ? `${url}/${path}` : url;\n};\n\nexport const postData = async ({\n  url,\n  data\n}: {\n  url: string;\n  data?: { price: Price };\n}) => {\n  const res = await fetch(url, {\n    method: 'POST',\n    headers: new Headers({ 'Content-Type': 'application/json' }),\n    credentials: 'same-origin',\n    body: JSON.stringify(data)\n  });\n\n  return res.json();\n};\n\nexport const toDateTime = (secs: number) => {\n  var t = new Date(+0); // Unix epoch start.\n  t.setSeconds(secs);\n  return t;\n};\n\nexport const calculateTrialEndUnixTimestamp = (\n  trialPeriodDays: number | null | undefined\n) => {\n  // Check if trialPeriodDays is null, undefined, or less than 2 days\n  if (\n    trialPeriodDays === null ||\n    trialPeriodDays === undefined ||\n    trialPeriodDays < 2\n  ) {\n    return undefined;\n  }\n\n  const currentDate = new Date(); // Current date and time\n  const trialEnd = new Date(\n    currentDate.getTime() + (trialPeriodDays + 1) * 24 * 60 * 60 * 1000\n  ); // Add trial days\n  return Math.floor(trialEnd.getTime() / 1000); // Convert to Unix timestamp in seconds\n};\n\nconst toastKeyMap: { [key: string]: string[] } = {\n  status: ['status', 'status_description'],\n  error: ['error', 'error_description']\n};\n\nconst getToastRedirect = (\n  path: string,\n  toastType: string,\n  toastName: string,\n  toastDescription: string = '',\n  disableButton: boolean = false,\n  arbitraryParams: string = ''\n): string => {\n  const [nameKey, descriptionKey] = toastKeyMap[toastType];\n\n  let redirectPath = `${path}?${nameKey}=${encodeURIComponent(toastName)}`;\n\n  if (toastDescription) {\n    redirectPath += `&${descriptionKey}=${encodeURIComponent(toastDescription)}`;\n  }\n\n  if (disableButton) {\n    redirectPath += `&disable_button=true`;\n  }\n\n  if (arbitraryParams) {\n    redirectPath += `&${arbitraryParams}`;\n  }\n\n  return redirectPath;\n};\n\nexport const getStatusRedirect = (\n  path: string,\n  statusName: string,\n  statusDescription: string = '',\n  disableButton: boolean = false,\n  arbitraryParams: string = ''\n) =>\n  getToastRedirect(\n    path,\n    'status',\n    statusName,\n    statusDescription,\n    disableButton,\n    arbitraryParams\n  );\n\nexport const getErrorRedirect = (\n  path: string,\n  errorName: string,\n  errorDescription: string = '',\n  disableButton: boolean = false,\n  arbitraryParams: string = ''\n) =>\n  getToastRedirect(\n    path,\n    'error',\n    errorName,\n    errorDescription,\n    disableButton,\n    arbitraryParams\n  );\n"
  },
  {
    "path": "utils/stripe/client.ts",
    "content": "import { loadStripe, Stripe } from '@stripe/stripe-js';\n\nlet stripePromise: Promise<Stripe | null>;\n\nexport const getStripe = () => {\n  if (!stripePromise) {\n    stripePromise = loadStripe(\n      process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_LIVE ??\n        process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY ??\n        ''\n    );\n  }\n\n  return stripePromise;\n};\n"
  },
  {
    "path": "utils/stripe/config.ts",
    "content": "import Stripe from 'stripe';\n\nexport const stripe = new Stripe(\n  process.env.STRIPE_SECRET_KEY_LIVE ?? process.env.STRIPE_SECRET_KEY ?? '',\n  {\n    // https://github.com/stripe/stripe-node#configuration\n    // https://stripe.com/docs/api/versioning\n    // @ts-ignore\n    apiVersion: null,\n    // Register this as an official Stripe plugin.\n    // https://stripe.com/docs/building-plugins#setappinfo\n    appInfo: {\n      name: 'Next.js Subscription Starter',\n      version: '0.0.0',\n      url: 'https://github.com/vercel/nextjs-subscription-payments'\n    }\n  }\n);\n"
  },
  {
    "path": "utils/stripe/server.ts",
    "content": "'use server';\n\nimport Stripe from 'stripe';\nimport { stripe } from '@/utils/stripe/config';\nimport { createClient } from '@/utils/supabase/server';\nimport { createOrRetrieveCustomer } from '@/utils/supabase/admin';\nimport {\n  getURL,\n  getErrorRedirect,\n  calculateTrialEndUnixTimestamp\n} from '@/utils/helpers';\nimport { Tables } from '@/types_db';\n\ntype Price = Tables<'prices'>;\n\ntype CheckoutResponse = {\n  errorRedirect?: string;\n  sessionId?: string;\n};\n\nexport async function checkoutWithStripe(\n  price: Price,\n  redirectPath: string = '/account'\n): Promise<CheckoutResponse> {\n  try {\n    // Get the user from Supabase auth\n    const supabase = createClient();\n    const {\n      error,\n      data: { user }\n    } = await supabase.auth.getUser();\n\n    if (error || !user) {\n      console.error(error);\n      throw new Error('Could not get user session.');\n    }\n\n    // Retrieve or create the customer in Stripe\n    let customer: string;\n    try {\n      customer = await createOrRetrieveCustomer({\n        uuid: user?.id || '',\n        email: user?.email || ''\n      });\n    } catch (err) {\n      console.error(err);\n      throw new Error('Unable to access customer record.');\n    }\n\n    let params: Stripe.Checkout.SessionCreateParams = {\n      allow_promotion_codes: true,\n      billing_address_collection: 'required',\n      customer,\n      customer_update: {\n        address: 'auto'\n      },\n      line_items: [\n        {\n          price: price.id,\n          quantity: 1\n        }\n      ],\n      cancel_url: getURL(),\n      success_url: getURL(redirectPath)\n    };\n\n    console.log(\n      'Trial end:',\n      calculateTrialEndUnixTimestamp(price.trial_period_days)\n    );\n    if (price.type === 'recurring') {\n      params = {\n        ...params,\n        mode: 'subscription',\n        subscription_data: {\n          trial_end: calculateTrialEndUnixTimestamp(price.trial_period_days)\n        }\n      };\n    } else if (price.type === 'one_time') {\n      params = {\n        ...params,\n        mode: 'payment'\n      };\n    }\n\n    // Create a checkout session in Stripe\n    let session;\n    try {\n      session = await stripe.checkout.sessions.create(params);\n    } catch (err) {\n      console.error(err);\n      throw new Error('Unable to create checkout session.');\n    }\n\n    // Instead of returning a Response, just return the data or error.\n    if (session) {\n      return { sessionId: session.id };\n    } else {\n      throw new Error('Unable to create checkout session.');\n    }\n  } catch (error) {\n    if (error instanceof Error) {\n      return {\n        errorRedirect: getErrorRedirect(\n          redirectPath,\n          error.message,\n          'Please try again later or contact a system administrator.'\n        )\n      };\n    } else {\n      return {\n        errorRedirect: getErrorRedirect(\n          redirectPath,\n          'An unknown error occurred.',\n          'Please try again later or contact a system administrator.'\n        )\n      };\n    }\n  }\n}\n\nexport async function createStripePortal(currentPath: string) {\n  try {\n    const supabase = createClient();\n    const {\n      error,\n      data: { user }\n    } = await supabase.auth.getUser();\n\n    if (!user) {\n      if (error) {\n        console.error(error);\n      }\n      throw new Error('Could not get user session.');\n    }\n\n    let customer;\n    try {\n      customer = await createOrRetrieveCustomer({\n        uuid: user.id || '',\n        email: user.email || ''\n      });\n    } catch (err) {\n      console.error(err);\n      throw new Error('Unable to access customer record.');\n    }\n\n    if (!customer) {\n      throw new Error('Could not get customer.');\n    }\n\n    try {\n      const { url } = await stripe.billingPortal.sessions.create({\n        customer,\n        return_url: getURL('/account')\n      });\n      if (!url) {\n        throw new Error('Could not create billing portal');\n      }\n      return url;\n    } catch (err) {\n      console.error(err);\n      throw new Error('Could not create billing portal');\n    }\n  } catch (error) {\n    if (error instanceof Error) {\n      console.error(error);\n      return getErrorRedirect(\n        currentPath,\n        error.message,\n        'Please try again later or contact a system administrator.'\n      );\n    } else {\n      return getErrorRedirect(\n        currentPath,\n        'An unknown error occurred.',\n        'Please try again later or contact a system administrator.'\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "utils/supabase/admin.ts",
    "content": "import { toDateTime } from '@/utils/helpers';\nimport { stripe } from '@/utils/stripe/config';\nimport { createClient } from '@supabase/supabase-js';\nimport Stripe from 'stripe';\nimport type { Database, Tables, TablesInsert } from 'types_db';\n\ntype Product = Tables<'products'>;\ntype Price = Tables<'prices'>;\n\n// Change to control trial period length\nconst TRIAL_PERIOD_DAYS = 0;\n\n// Note: supabaseAdmin uses the SERVICE_ROLE_KEY which you must only use in a secure server-side context\n// as it has admin privileges and overwrites RLS policies!\nconst supabaseAdmin = createClient<Database>(\n  process.env.NEXT_PUBLIC_SUPABASE_URL || '',\n  process.env.SUPABASE_SERVICE_ROLE_KEY || ''\n);\n\nconst upsertProductRecord = async (product: Stripe.Product) => {\n  const productData: Product = {\n    id: product.id,\n    active: product.active,\n    name: product.name,\n    description: product.description ?? null,\n    image: product.images?.[0] ?? null,\n    metadata: product.metadata\n  };\n\n  const { error: upsertError } = await supabaseAdmin\n    .from('products')\n    .upsert([productData]);\n  if (upsertError)\n    throw new Error(`Product insert/update failed: ${upsertError.message}`);\n  console.log(`Product inserted/updated: ${product.id}`);\n};\n\nconst upsertPriceRecord = async (\n  price: Stripe.Price,\n  retryCount = 0,\n  maxRetries = 3\n) => {\n  const priceData: Price = {\n    id: price.id,\n    product_id: typeof price.product === 'string' ? price.product : '',\n    active: price.active,\n    currency: price.currency,\n    type: price.type,\n    unit_amount: price.unit_amount ?? null,\n    interval: price.recurring?.interval ?? null,\n    interval_count: price.recurring?.interval_count ?? null,\n    trial_period_days: price.recurring?.trial_period_days ?? TRIAL_PERIOD_DAYS\n  };\n\n  const { error: upsertError } = await supabaseAdmin\n    .from('prices')\n    .upsert([priceData]);\n\n  if (upsertError?.message.includes('foreign key constraint')) {\n    if (retryCount < maxRetries) {\n      console.log(`Retry attempt ${retryCount + 1} for price ID: ${price.id}`);\n      await new Promise((resolve) => setTimeout(resolve, 2000));\n      await upsertPriceRecord(price, retryCount + 1, maxRetries);\n    } else {\n      throw new Error(\n        `Price insert/update failed after ${maxRetries} retries: ${upsertError.message}`\n      );\n    }\n  } else if (upsertError) {\n    throw new Error(`Price insert/update failed: ${upsertError.message}`);\n  } else {\n    console.log(`Price inserted/updated: ${price.id}`);\n  }\n};\n\nconst deleteProductRecord = async (product: Stripe.Product) => {\n  const { error: deletionError } = await supabaseAdmin\n    .from('products')\n    .delete()\n    .eq('id', product.id);\n  if (deletionError)\n    throw new Error(`Product deletion failed: ${deletionError.message}`);\n  console.log(`Product deleted: ${product.id}`);\n};\n\nconst deletePriceRecord = async (price: Stripe.Price) => {\n  const { error: deletionError } = await supabaseAdmin\n    .from('prices')\n    .delete()\n    .eq('id', price.id);\n  if (deletionError) throw new Error(`Price deletion failed: ${deletionError.message}`);\n  console.log(`Price deleted: ${price.id}`);\n};\n\nconst upsertCustomerToSupabase = async (uuid: string, customerId: string) => {\n  const { error: upsertError } = await supabaseAdmin\n    .from('customers')\n    .upsert([{ id: uuid, stripe_customer_id: customerId }]);\n\n  if (upsertError)\n    throw new Error(`Supabase customer record creation failed: ${upsertError.message}`);\n\n  return customerId;\n};\n\nconst createCustomerInStripe = async (uuid: string, email: string) => {\n  const customerData = { metadata: { supabaseUUID: uuid }, email: email };\n  const newCustomer = await stripe.customers.create(customerData);\n  if (!newCustomer) throw new Error('Stripe customer creation failed.');\n\n  return newCustomer.id;\n};\n\nconst createOrRetrieveCustomer = async ({\n  email,\n  uuid\n}: {\n  email: string;\n  uuid: string;\n}) => {\n  // Check if the customer already exists in Supabase\n  const { data: existingSupabaseCustomer, error: queryError } =\n    await supabaseAdmin\n      .from('customers')\n      .select('*')\n      .eq('id', uuid)\n      .maybeSingle();\n\n  if (queryError) {\n    throw new Error(`Supabase customer lookup failed: ${queryError.message}`);\n  }\n\n  // Retrieve the Stripe customer ID using the Supabase customer ID, with email fallback\n  let stripeCustomerId: string | undefined;\n  if (existingSupabaseCustomer?.stripe_customer_id) {\n    const existingStripeCustomer = await stripe.customers.retrieve(\n      existingSupabaseCustomer.stripe_customer_id\n    );\n    stripeCustomerId = existingStripeCustomer.id;\n  } else {\n    // If Stripe ID is missing from Supabase, try to retrieve Stripe customer ID by email\n    const stripeCustomers = await stripe.customers.list({ email: email });\n    stripeCustomerId =\n      stripeCustomers.data.length > 0 ? stripeCustomers.data[0].id : undefined;\n  }\n\n  // If still no stripeCustomerId, create a new customer in Stripe\n  const stripeIdToInsert = stripeCustomerId\n    ? stripeCustomerId\n    : await createCustomerInStripe(uuid, email);\n  if (!stripeIdToInsert) throw new Error('Stripe customer creation failed.');\n\n  if (existingSupabaseCustomer && stripeCustomerId) {\n    // If Supabase has a record but doesn't match Stripe, update Supabase record\n    if (existingSupabaseCustomer.stripe_customer_id !== stripeCustomerId) {\n      const { error: updateError } = await supabaseAdmin\n        .from('customers')\n        .update({ stripe_customer_id: stripeCustomerId })\n        .eq('id', uuid);\n\n      if (updateError)\n        throw new Error(\n          `Supabase customer record update failed: ${updateError.message}`\n        );\n      console.warn(\n        `Supabase customer record mismatched Stripe ID. Supabase record updated.`\n      );\n    }\n    // If Supabase has a record and matches Stripe, return Stripe customer ID\n    return stripeCustomerId;\n  } else {\n    console.warn(\n      `Supabase customer record was missing. A new record was created.`\n    );\n\n    // If Supabase has no record, create a new record and return Stripe customer ID\n    const upsertedStripeCustomer = await upsertCustomerToSupabase(\n      uuid,\n      stripeIdToInsert\n    );\n    if (!upsertedStripeCustomer)\n      throw new Error('Supabase customer record creation failed.');\n\n    return upsertedStripeCustomer;\n  }\n};\n\n/**\n * Copies the billing details from the payment method to the customer object.\n */\nconst copyBillingDetailsToCustomer = async (\n  uuid: string,\n  payment_method: Stripe.PaymentMethod\n) => {\n  //Todo: check this assertion\n  const customer = payment_method.customer as string;\n  const { name, phone, address } = payment_method.billing_details;\n  if (!name || !phone || !address) return;\n  //@ts-ignore\n  await stripe.customers.update(customer, { name, phone, address });\n  const { error: updateError } = await supabaseAdmin\n    .from('users')\n    .update({\n      billing_address: { ...address },\n      payment_method: { ...payment_method[payment_method.type] }\n    })\n    .eq('id', uuid);\n  if (updateError) throw new Error(`Customer update failed: ${updateError.message}`);\n};\n\nconst manageSubscriptionStatusChange = async (\n  subscriptionId: string,\n  customerId: string,\n  createAction = false\n) => {\n  // Get customer's UUID from mapping table.\n  const { data: customerData, error: noCustomerError } = await supabaseAdmin\n    .from('customers')\n    .select('id')\n    .eq('stripe_customer_id', customerId)\n    .single();\n\n  if (noCustomerError)\n    throw new Error(`Customer lookup failed: ${noCustomerError.message}`);\n\n  const { id: uuid } = customerData!;\n\n  const subscription = await stripe.subscriptions.retrieve(subscriptionId, {\n    expand: ['default_payment_method']\n  });\n  // Upsert the latest status of the subscription object.\n  const subscriptionData: TablesInsert<'subscriptions'> = {\n    id: subscription.id,\n    user_id: uuid,\n    metadata: subscription.metadata,\n    status: subscription.status,\n    price_id: subscription.items.data[0].price.id,\n    //TODO check quantity on subscription\n    // @ts-ignore\n    quantity: subscription.quantity,\n    cancel_at_period_end: subscription.cancel_at_period_end,\n    cancel_at: subscription.cancel_at\n      ? toDateTime(subscription.cancel_at).toISOString()\n      : null,\n    canceled_at: subscription.canceled_at\n      ? toDateTime(subscription.canceled_at).toISOString()\n      : null,\n    current_period_start: toDateTime(\n      subscription.current_period_start\n    ).toISOString(),\n    current_period_end: toDateTime(\n      subscription.current_period_end\n    ).toISOString(),\n    created: toDateTime(subscription.created).toISOString(),\n    ended_at: subscription.ended_at\n      ? toDateTime(subscription.ended_at).toISOString()\n      : null,\n    trial_start: subscription.trial_start\n      ? toDateTime(subscription.trial_start).toISOString()\n      : null,\n    trial_end: subscription.trial_end\n      ? toDateTime(subscription.trial_end).toISOString()\n      : null\n  };\n\n  const { error: upsertError } = await supabaseAdmin\n    .from('subscriptions')\n    .upsert([subscriptionData]);\n  if (upsertError)\n    throw new Error(`Subscription insert/update failed: ${upsertError.message}`);\n  console.log(\n    `Inserted/updated subscription [${subscription.id}] for user [${uuid}]`\n  );\n\n  // For a new subscription copy the billing details to the customer object.\n  // NOTE: This is a costly operation and should happen at the very end.\n  if (createAction && subscription.default_payment_method && uuid)\n    //@ts-ignore\n    await copyBillingDetailsToCustomer(\n      uuid,\n      subscription.default_payment_method as Stripe.PaymentMethod\n    );\n};\n\nexport {\n  upsertProductRecord,\n  upsertPriceRecord,\n  deleteProductRecord,\n  deletePriceRecord,\n  createOrRetrieveCustomer,\n  manageSubscriptionStatusChange\n};\n"
  },
  {
    "path": "utils/supabase/client.ts",
    "content": "import { createBrowserClient } from '@supabase/ssr';\nimport { Database } from '@/types_db';\n\n// Define a function to create a Supabase client for client-side operations\nexport const createClient = () =>\n  createBrowserClient<Database>(\n    // Pass Supabase URL and anonymous key from the environment to the client\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!\n  );\n"
  },
  {
    "path": "utils/supabase/middleware.ts",
    "content": "import { createServerClient, type CookieOptions } from '@supabase/ssr';\nimport { type NextRequest, NextResponse } from 'next/server';\n\nexport const createClient = (request: NextRequest) => {\n  // Create an unmodified response\n  let response = NextResponse.next({\n    request: {\n      headers: request.headers\n    }\n  });\n\n  const supabase = createServerClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n    {\n      cookies: {\n        get(name: string) {\n          return request.cookies.get(name)?.value;\n        },\n        set(name: string, value: string, options: CookieOptions) {\n          // If the cookie is updated, update the cookies for the request and response\n          request.cookies.set({\n            name,\n            value,\n            ...options\n          });\n          response = NextResponse.next({\n            request: {\n              headers: request.headers\n            }\n          });\n          response.cookies.set({\n            name,\n            value,\n            ...options\n          });\n        },\n        remove(name: string, options: CookieOptions) {\n          // If the cookie is removed, update the cookies for the request and response\n          request.cookies.set({\n            name,\n            value: '',\n            ...options\n          });\n          response = NextResponse.next({\n            request: {\n              headers: request.headers\n            }\n          });\n          response.cookies.set({\n            name,\n            value: '',\n            ...options\n          });\n        }\n      }\n    }\n  );\n\n  return { supabase, response };\n};\n\nexport const updateSession = async (request: NextRequest) => {\n  try {\n    // This `try/catch` block is only here for the interactive tutorial.\n    // Feel free to remove once you have Supabase connected.\n    const { supabase, response } = createClient(request);\n\n    // This will refresh session if expired - required for Server Components\n    // https://supabase.com/docs/guides/auth/server-side/nextjs\n    await supabase.auth.getUser();\n\n    return response;\n  } catch (e) {\n    // If you are here, a Supabase client could not be created!\n    // This is likely because you have not set up environment variables.\n    // Check out http://localhost:3000 for Next Steps.\n    return NextResponse.next({\n      request: {\n        headers: request.headers\n      }\n    });\n  }\n};\n"
  },
  {
    "path": "utils/supabase/queries.ts",
    "content": "import { SupabaseClient } from '@supabase/supabase-js';\nimport { cache } from 'react';\n\nexport const getUser = cache(async (supabase: SupabaseClient) => {\n  const {\n    data: { user }\n  } = await supabase.auth.getUser();\n  return user;\n});\n\nexport const getSubscription = cache(async (supabase: SupabaseClient) => {\n  const { data: subscription, error } = await supabase\n    .from('subscriptions')\n    .select('*, prices(*, products(*))')\n    .in('status', ['trialing', 'active'])\n    .maybeSingle();\n\n  return subscription;\n});\n\nexport const getProducts = cache(async (supabase: SupabaseClient) => {\n  const { data: products, error } = await supabase\n    .from('products')\n    .select('*, prices(*)')\n    .eq('active', true)\n    .eq('prices.active', true)\n    .order('metadata->index')\n    .order('unit_amount', { referencedTable: 'prices' });\n\n  return products;\n});\n\nexport const getUserDetails = cache(async (supabase: SupabaseClient) => {\n  const { data: userDetails } = await supabase\n    .from('users')\n    .select('*')\n    .single();\n  return userDetails;\n});\n"
  },
  {
    "path": "utils/supabase/server.ts",
    "content": "import { createServerClient, type CookieOptions } from '@supabase/ssr';\nimport { cookies } from 'next/headers';\nimport { Database } from '@/types_db';\n\n// Define a function to create a Supabase client for server-side operations\n// The function takes a cookie store created with next/headers cookies as an argument\nexport const createClient = () => {\n  const cookieStore = cookies();\n\n  return createServerClient<Database>(\n    // Pass Supabase URL and anonymous key from the environment to the client\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n\n    // Define a cookies object with methods for interacting with the cookie store and pass it to the client\n    {\n      cookies: {\n        // The get method is used to retrieve a cookie by its name\n        get(name: string) {\n          return cookieStore.get(name)?.value;\n        },\n        // The set method is used to set a cookie with a given name, value, and options\n        set(name: string, value: string, options: CookieOptions) {\n          try {\n            cookieStore.set({ name, value, ...options });\n          } catch (error) {\n            // If the set method is called from a Server Component, an error may occur\n            // This can be ignored if there is middleware refreshing user sessions\n          }\n        },\n        // The remove method is used to delete a cookie by its name\n        remove(name: string, options: CookieOptions) {\n          try {\n            cookieStore.set({ name, value: '', ...options });\n          } catch (error) {\n            // If the remove method is called from a Server Component, an error may occur\n            // This can be ignored if there is middleware refreshing user sessions\n          }\n        }\n      }\n    }\n  );\n};\n"
  }
]