├── .eslintrc.json
├── app
├── favicon.ico
├── api
│ ├── auth
│ │ ├── [...nextauth]
│ │ │ └── route.ts
│ │ └── authOptions.ts
│ ├── order
│ │ └── route.ts
│ └── verify
│ │ └── route.ts
├── provider.tsx
├── page.tsx
├── layout.tsx
├── success
│ └── page.tsx
├── globals.css
├── pricing
│ └── page.tsx
├── components
│ └── Appbar.tsx
└── checkout
│ └── page.tsx
├── next.config.mjs
├── prisma
├── migrations
│ ├── migration_lock.toml
│ └── 20240830110859_init
│ │ └── migration.sql
└── schema.prisma
├── postcss.config.mjs
├── lib
├── utils.ts
└── prisma.ts
├── docker-compose.yml
├── components.json
├── .env.example
├── .gitignore
├── public
├── vercel.svg
└── next.svg
├── tsconfig.json
├── package.json
├── LICENSE
├── README.md
├── components
└── ui
│ ├── button.tsx
│ └── card.tsx
└── tailwind.config.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sinisterchilll/razorpay-nextauth-saas-stack/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from "@/app/api/auth/authOptions";
2 | import NextAuth from "next-auth";
3 |
4 | const handler = NextAuth(authOptions);
5 | export { handler as GET, handler as POST };
--------------------------------------------------------------------------------
/app/provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { SessionProvider } from "next-auth/react";
4 |
5 | export function Provider({ children }: { children: React.ReactNode }) {
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 | services:
3 | db:
4 | image: postgres:14
5 | restart: always
6 | environment:
7 | - POSTGRES_USER=myuser
8 | - POSTGRES_PASSWORD=mypassword
9 | - POSTGRES_DB=mydb
10 | ports:
11 | - '5432:5432'
12 | volumes:
13 | - postgres_data:/var/lib/postgresql/data
14 |
15 | volumes:
16 | postgres_data:
17 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { Appbar } from "./components/Appbar";
2 | import Link from "next/link";
3 |
4 | export default function Home() {
5 | return (
6 |
7 |
8 |
9 | Happy Shipping!
10 |
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/lib/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client'
2 |
3 | // Prevent multiple instances of Prisma Client in development
4 | const globalForPrisma = global as unknown as { prisma: PrismaClient }
5 |
6 | export const prisma =
7 | globalForPrisma.prisma ||
8 | new PrismaClient({
9 | log: ['query', 'error', 'warn'],
10 | })
11 |
12 | if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
13 |
14 | export type { PrismaClient }
15 |
16 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Database
2 | DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/mydb"
3 |
4 | # NextAuth
5 | NEXTAUTH_SECRET=your_nextauth_secret
6 |
7 |
8 | # Razorpay
9 | RAZORPAY_KEY_ID=your_razorpay_key_id
10 | RAZORPAY_KEY_SECRET=your_razorpay_key_secret
11 |
12 | #Google Auth
13 | GOOGLE_CLIENT_ID=example
14 | GOOGLE_CLIENT_SECRET=example
15 |
16 | # Github Auth
17 | GITHUB_CLIENT_ID=
18 | GITHUB_CLIENT_SECRET=
19 |
20 | #Lemonsqueezy
21 | LEMONSQUEEZY_API_KEY=
22 | LEMONSQUEEZY_STORE_ID=
23 | LEMON_SQUEEZY_WEBHOOK_SIGNATURE=
24 |
25 |
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/prisma/migrations/20240830110859_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "Provider" AS ENUM ('GOOGLE');
3 |
4 | -- CreateTable
5 | CREATE TABLE "User" (
6 | "id" TEXT NOT NULL,
7 | "email" TEXT NOT NULL,
8 | "name" TEXT,
9 | "image" TEXT,
10 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
11 | "updatedAt" TIMESTAMP(3) NOT NULL,
12 | "provider" "Provider" NOT NULL,
13 |
14 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
15 | );
16 |
17 | -- CreateIndex
18 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
19 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/api/order/route.ts:
--------------------------------------------------------------------------------
1 | import Razorpay from 'razorpay';
2 |
3 | const razorpay = new Razorpay({
4 | key_id: process.env.key_id!,
5 | key_secret: process.env.key_secret,
6 | });
7 |
8 | import { NextRequest, NextResponse } from 'next/server';
9 |
10 | export async function POST(request: NextRequest) {
11 | const { amount, currency } = (await request.json()) as {
12 | amount: string;
13 | currency: string;
14 | };
15 |
16 | var options = {
17 | amount: amount,
18 | currency: currency,
19 | receipt: 'rcp1',
20 | };
21 | const order = await razorpay.orders.create(options);
22 | console.log(order);
23 | return NextResponse.json({ orderId: order.id }, { status: 200 });
24 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { Provider } from "./provider";
5 |
6 | const inter = Inter({ subsets: ["latin"] });
7 |
8 | export const metadata: Metadata = {
9 | title: "Create Next App",
10 | description: "Generated by create next app",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: Readonly<{
16 | children: React.ReactNode;
17 | }>) {
18 | return (
19 |
20 |
21 |
22 | {children}
23 |
24 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/app/success/page.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import Link from 'next/link';
3 | import { CheckCircle2 } from 'lucide-react';
4 | export default function PaymentVerifyPage() {
5 | return (
6 |
7 |
8 | Thank You for Purchasing our product
9 |
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | datasource db {
12 | provider = "postgresql"
13 | url = env("DATABASE_URL")
14 | }
15 |
16 | model User {
17 | id String @id @default(cuid())
18 | email String @unique
19 | name String?
20 | image String?
21 | createdAt DateTime @default(now())
22 | updatedAt DateTime @updatedAt
23 | provider Provider
24 | }
25 |
26 | enum Provider {
27 | GOOGLE
28 | GITHUB
29 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "template_saas_next",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@prisma/client": "^5.19.0",
14 | "@radix-ui/react-slot": "^1.1.0",
15 | "class-variance-authority": "^0.7.0",
16 | "clsx": "^2.1.1",
17 | "lucide-react": "^0.436.0",
18 | "next": "14.2.7",
19 | "next-auth": "^4.24.7",
20 | "prisma": "^5.19.0",
21 | "razorpay": "^2.9.4",
22 | "react": "^18",
23 | "react-dom": "^18",
24 | "tailwind-merge": "^2.5.2",
25 | "tailwindcss-animate": "^1.0.7",
26 | "zod": "^3.23.8"
27 | },
28 | "devDependencies": {
29 | "@types/node": "^20",
30 | "@types/react": "^18",
31 | "@types/react-dom": "^18",
32 | "eslint": "^8",
33 | "eslint-config-next": "14.2.7",
34 | "postcss": "^8",
35 | "tailwindcss": "^3.4.1",
36 | "typescript": "^5"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/api/verify/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import crypto from 'crypto';
3 |
4 | const generatedSignature = (
5 | razorpayOrderId: string,
6 | razorpayPaymentId: string
7 | ) => {
8 | const keySecret = process.env.key_secret;
9 | if (!keySecret) {
10 | throw new Error(
11 | 'Razorpay key secret is not defined in environment variables.'
12 | );
13 | }
14 | const sig = crypto
15 | .createHmac('sha256', keySecret)
16 | .update(razorpayOrderId + '|' + razorpayPaymentId)
17 | .digest('hex');
18 | return sig;
19 | };
20 |
21 |
22 | export async function POST(request: NextRequest) {
23 | const { orderCreationId, razorpayPaymentId, razorpaySignature } =
24 | await request.json();
25 |
26 | const signature = generatedSignature(orderCreationId, razorpayPaymentId);
27 | if (signature !== razorpaySignature) {
28 | return NextResponse.json(
29 | { message: 'payment verification failed', isOk: false },
30 | { status: 400 }
31 | );
32 | }
33 | return NextResponse.json(
34 | { message: 'payment verified successfully', isOk: true },
35 | { status: 200 }
36 | );
37 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2024] [Ayush Singh]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SaaS Starter with Nextauth, Razorpay, Prisma, and PostgreSQL
2 |
3 | This is a starter template for Next.js projects with built-in authentication, Razorpay integration, Prisma ORM, and PostgreSQL (via Docker).
4 |
5 | ## Features
6 |
7 | - Next.js 13+ with App Router
8 | - NextAuth for authentication
9 | - Razorpay integration for payments
10 | - Prisma ORM for database management
11 | - PostgreSQL database (containerized with Docker)
12 | - Tailwind CSS for styling
13 | - TypeScript support
14 |
15 | ## Prerequisites
16 |
17 | - Node.js 14+ and npm
18 | - Docker and Docker Compose
19 |
20 | ## Getting Started
21 |
22 | 1. Clone the repository:
23 | ```bash
24 | git clone https://github.com/yourusername/your-repo-name.git
25 | ```
26 |
27 | 2. Install dependencies:
28 | ```bash
29 | cd your-repo-name
30 | npm install
31 | ```
32 |
33 | 3. Set up environment variables:
34 | Copy `.env.example` to `.env` and fill in your details.
35 |
36 | 4. Start the PostgreSQL database:
37 | ```bash
38 | docker-compose up -d
39 | ```
40 | 5. Make .env file same as example env file, if usind docker for database kepp the DATABASE_URL same as example
41 |
42 | 6. Run Prisma migrations:
43 | ```bash
44 | npx prisma migrate dev
45 | ```
46 |
47 | 7. Start the development server:
48 | ```bash
49 | npm run dev
50 | ```
51 |
52 |
53 | ## License
54 |
55 | This project is open source and available under the [MIT License](LICENSE).
56 |
--------------------------------------------------------------------------------
/app/api/auth/authOptions.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { AuthOptions, DefaultSession } from "next-auth"
2 | import GoogleProvider from "next-auth/providers/google";
3 | import GitHubProvider from "next-auth/providers/github";
4 | import {z} from "zod"
5 | import { prisma } from "@/lib/prisma"
6 | import { Provider } from '@prisma/client'
7 |
8 | const userSchema = z.object({
9 | name: z.string(),
10 | email: z.string().email(),
11 | image: z.string(),
12 | })
13 |
14 |
15 | export const authOptions: AuthOptions = {
16 | providers: [
17 | GoogleProvider({
18 | clientId: process.env.GOOGLE_CLIENT_ID ?? '',
19 | clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? ''
20 | }),
21 | GitHubProvider({
22 | clientId: process.env.GITHUB_CLIENT_ID ?? '',
23 | clientSecret: process.env.GITHUB_CLIENT_SECRET ?? ''
24 | })
25 | ],
26 | callbacks: {
27 | async signIn(params) {
28 | try {
29 | const provider = params.account?.provider?.toUpperCase() as Provider;
30 | await prisma.user.upsert({
31 | where: { email: params.user.email ?? '' },
32 | update: {
33 | name: params.user.name,
34 | image: params.user.image,
35 | provider: provider
36 | },
37 | create: {
38 | email: params.user.email ?? '',
39 | name: params.user.name,
40 | image: params.user.image,
41 | provider: provider
42 | }
43 | });
44 | } catch (error) {
45 | console.log(error)
46 | }
47 | return true
48 | }
49 | }
50 | }
51 |
52 | const handler = NextAuth(authOptions)
53 |
54 | export { handler as GET, handler as POST }
55 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 222.2 84% 4.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 222.2 84% 4.9%;
13 | --primary: 222.2 47.4% 11.2%;
14 | --primary-foreground: 210 40% 98%;
15 | --secondary: 210 40% 96.1%;
16 | --secondary-foreground: 222.2 47.4% 11.2%;
17 | --muted: 210 40% 96.1%;
18 | --muted-foreground: 215.4 16.3% 46.9%;
19 | --accent: 210 40% 96.1%;
20 | --accent-foreground: 222.2 47.4% 11.2%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 214.3 31.8% 91.4%;
24 | --input: 214.3 31.8% 91.4%;
25 | --ring: 222.2 84% 4.9%;
26 | --radius: 0.5rem;
27 | --chart-1: 12 76% 61%;
28 | --chart-2: 173 58% 39%;
29 | --chart-3: 197 37% 24%;
30 | --chart-4: 43 74% 66%;
31 | --chart-5: 27 87% 67%;
32 | }
33 |
34 | .dark {
35 | --background: 222.2 84% 4.9%;
36 | --foreground: 210 40% 98%;
37 | --card: 222.2 84% 4.9%;
38 | --card-foreground: 210 40% 98%;
39 | --popover: 222.2 84% 4.9%;
40 | --popover-foreground: 210 40% 98%;
41 | --primary: 210 40% 98%;
42 | --primary-foreground: 222.2 47.4% 11.2%;
43 | --secondary: 217.2 32.6% 17.5%;
44 | --secondary-foreground: 210 40% 98%;
45 | --muted: 217.2 32.6% 17.5%;
46 | --muted-foreground: 215 20.2% 65.1%;
47 | --accent: 217.2 32.6% 17.5%;
48 | --accent-foreground: 210 40% 98%;
49 | --destructive: 0 62.8% 30.6%;
50 | --destructive-foreground: 210 40% 98%;
51 | --border: 217.2 32.6% 17.5%;
52 | --input: 217.2 32.6% 17.5%;
53 | --ring: 212.7 26.8% 83.9%;
54 | --chart-1: 220 70% 50%;
55 | --chart-2: 160 60% 45%;
56 | --chart-3: 30 80% 55%;
57 | --chart-4: 280 65% 60%;
58 | --chart-5: 340 75% 55%;
59 | }
60 | }
61 |
62 | @layer base {
63 | * {
64 | @apply border-border;
65 | }
66 | body {
67 | @apply bg-background text-foreground;
68 | }
69 | }
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/app/pricing/page.tsx:
--------------------------------------------------------------------------------
1 |
2 | "use client";
3 |
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import { CheckCircle2 } from "lucide-react";
13 | import { Button } from "@/components/ui/button";
14 | import Link from "next/link";
15 | import { Appbar } from "../components/Appbar";
16 |
17 | type PricingCardProps = {
18 | title: string;
19 | price: number;
20 | description: string;
21 | features: string[];
22 | actionLabel: string;
23 | };
24 |
25 | const PricingCard = ({title,price,description, features, actionLabel}:PricingCardProps) => (
26 |
27 |
28 |
29 |
30 | {title}
31 |
32 |
33 | Rs {price}
34 |
35 |
36 |
37 | {description}
38 | {features.map((f,i)=>{
39 | return {f}
40 | })}
41 |
42 |
43 |
48 |
49 |
50 | );
51 |
52 | export default function Page() {
53 | const plans = [
54 | {
55 | title: "Basic",
56 | price:999,
57 | description: "Essential features you need to get started",
58 | features: [
59 | "Example Feature Number 1",
60 | "Example Feature Number 2",
61 | "Example Feature Number 3",
62 | ],
63 | actionLabel: "Get Basic",
64 | },
65 | {
66 | title: "Pro",
67 | price:5999,
68 | description: "Perfect for owners of small & medium businessess",
69 | features: [
70 | "Example Feature Number 1",
71 | "Example Feature Number 2",
72 | "Example Feature Number 3",
73 | ],
74 | actionLabel: "Get Pro",
75 | }
76 | ];
77 | return (
78 |
79 |
80 |
81 |
Pricing Plans
82 |
Choose the plan that's right for you
83 |
84 | {plans.map((plan) => {
85 | return ;
86 | })}
87 |
88 |
89 |
90 | );
91 | }
--------------------------------------------------------------------------------
/app/components/Appbar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { signIn, signOut, useSession } from "next-auth/react";
4 | import Link from 'next/link';
5 |
6 | export function Appbar() {
7 | const session = useSession()
8 |
9 | const handleGoogleSignIn = () => {
10 | signIn('google', { callbackUrl: '/' });
11 | }
12 |
13 | const handleGithubSignIn = () => {
14 | signIn('github', { callbackUrl: '/' });
15 | }
16 |
17 | return (
18 |
75 | )
76 | }
--------------------------------------------------------------------------------
/app/checkout/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import * as React from "react";
3 | import { Suspense } from "react";
4 |
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | Card,
8 | CardContent,
9 | CardDescription,
10 | CardFooter,
11 | CardHeader,
12 | CardTitle,
13 | } from "@/components/ui/card";
14 | import { useRouter, useSearchParams } from "next/navigation";
15 | import Script from "next/script";
16 | import { LoaderCircle } from "lucide-react";
17 |
18 | function CheckoutContent() {
19 | const router = useRouter();
20 | const params = useSearchParams();
21 | const amount = params.get("amount");
22 | const [loading1, setLoading1] = React.useState(true);
23 | const [loading, setLoading] = React.useState(false);
24 | const [orderId, setOrderId] = React.useState(null);
25 |
26 | const createOrderId = React.useCallback(async () => {
27 | try {
28 | const response = await fetch("/api/order", {
29 | method: "POST",
30 | headers: {
31 | "Content-Type": "application/json",
32 | },
33 | body: JSON.stringify({
34 | amount: parseFloat(amount!) * 100,
35 | plan: params.get("plan"),
36 | }),
37 | });
38 |
39 | if (!response.ok) {
40 | throw new Error("Network response was not ok");
41 | }
42 |
43 | const data = await response.json();
44 | setOrderId(data.orderId);
45 | setLoading1(false);
46 | } catch (error) {
47 | console.error("There was a problem with your fetch operation:", error);
48 | setLoading1(false);
49 | }
50 | }, [amount, params]);
51 |
52 | React.useEffect(() => {
53 | if (!amount) {
54 | router.replace("/");
55 | }
56 | createOrderId();
57 | }, [amount, router, createOrderId]);
58 |
59 | const processPayment = async (e: React.FormEvent) => {
60 | e.preventDefault();
61 | setLoading(true);
62 |
63 | // Create a new order ID for each payment attempt
64 | await createOrderId();
65 |
66 | if (!orderId) {
67 | alert("Failed to create order. Please try again.");
68 | setLoading(false);
69 | return;
70 | }
71 |
72 | try {
73 | const options = {
74 | key: process.env.key_id,
75 | amount: parseFloat(amount!) * 100,
76 | currency: "INR",
77 | name: "Your Business Name",
78 | description: `Subscription - ${params.get("plan")} Plan`,
79 | order_id: orderId,
80 | handler: async function (response: any) {
81 | const verifyData = {
82 | orderCreationId: orderId,
83 | razorpayPaymentId: response.razorpay_payment_id,
84 | razorpayOrderId: response.razorpay_order_id,
85 | razorpaySignature: response.razorpay_signature,
86 | plan: params.get("plan"),
87 | };
88 |
89 | const result = await fetch("/api/verify", {
90 | method: "POST",
91 | body: JSON.stringify(verifyData),
92 | headers: { "Content-Type": "application/json" },
93 | });
94 | const res = await result.json();
95 | if (res.success) {
96 | alert("Payment successful! Your subscription has been updated.");
97 | router.push("/dashboard"); // Redirect to dashboard or confirmation page
98 | } else {
99 | alert("Payment verification failed. Please contact support.");
100 | }
101 | },
102 | prefill: {
103 | name: "Customer Name",
104 | email: "customer@example.com",
105 | },
106 | theme: { color: "#3399cc" },
107 | // Add UPI payment method
108 | method: {
109 | upi: true,
110 | netbanking: true,
111 | card: true,
112 | },
113 | };
114 | const paymentObject = new (window as any).Razorpay(options);
115 | paymentObject.on("payment.failed", function (response: any) {
116 | alert("Payment failed. " + response.error.description);
117 | });
118 | setLoading(false);
119 | paymentObject.open();
120 | } catch (error) {
121 | console.error("Payment error:", error);
122 | alert("An error occurred while processing your payment. Please try again.");
123 | } finally {
124 | setLoading(false);
125 | }
126 | };
127 |
128 | if (loading1) return
129 |
130 |
131 | return (
132 | <>
133 |
137 |
138 |
139 |
140 | Checkout
141 |
142 |
143 |
144 | Choose Payment Method
145 |
146 | Select your preferred payment method to complete the subscription.
147 |
148 |
149 |
150 |
155 |
156 |
157 |
158 | Please read the terms and conditions.
159 |
160 |
161 |
162 |
163 | >
164 | );
165 | }
166 |
167 | export default function Checkout() {
168 | return (
169 | }>
170 |
171 |
172 | );
173 | }
174 |
175 | function LoadingFallback() {
176 | return (
177 |
178 |
179 |
180 | );
181 | }
--------------------------------------------------------------------------------