├── .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 |