Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions with-nextjs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# https://docs.polar.sh/integrate/oat
POLAR_ACCESS_TOKEN=

# https://docs.polar.sh/integrate/webhooks/endpoints#setup-webhooks
POLAR_WEBHOOK_SECRET=

# use the above same approach to get the sandbox credentials.
SANDBOX_POLAR_ACCESS_TOKEN=

SANDBOX_POLAR_WEBHOOK_SECRET=

# Polar server mode - production or sandbox
POLAR_MODE=production

# client url - this is the URL the customer would be led to if they purchase something.
DEV_SUCCESS_URL=
PROD_SUCCESS_URL=
41 changes: 41 additions & 0 deletions with-nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
3 changes: 3 additions & 0 deletions with-nextjs/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
README.md
5 changes: 5 additions & 0 deletions with-nextjs/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"printWidth": 180,
"singleQuote": true
}
36 changes: 36 additions & 0 deletions with-nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<img width="255" height="75" alt="image" src="https://github.com/user-attachments/assets/5f6f176d-661a-45ed-b661-b4d8383e63c6" />

# Getting started with Polar and Next.js

## Clone the repository

```bash
npx degit polarsource/examples/with-nextjs ./with-nextjs
```

## How to use

Run the command below to copy the .env.example file :

```bash
cp .env.example .env
```

Checkout env.example & doc to get the Polar credentials

```bash
POLAR_ACCESS_TOKEN
POLAR_WEBHOOK_SECRET
```

Run the command below to install project dependencies :

```bash
npm install
```

Run the Next.js application using the following command :

```bash
npm run dev
```
29 changes: 29 additions & 0 deletions with-nextjs/app/api/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { polar, successUrl } from '@/app/polar'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(req: NextRequest) {
try {
const body = await req.json()
const { productId } = body

if (!productId) {
return NextResponse.json({ message: 'productId is required.' }, { status: 400 })
}

const checkout = await polar.checkouts.create({
products: [productId],
successUrl: successUrl,
})

return NextResponse.json(
{
checkout: checkout,
message: 'Checkout successful.',
},
{ status: 200 },
)
} catch (error) {
console.error('❌ Error in checkout API:', error)
return NextResponse.json({ message: 'Internal server error' }, { status: 500 })
}
}
48 changes: 48 additions & 0 deletions with-nextjs/app/api/customer-portal/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { polar } from '@/app/polar'
import { NextRequest, NextResponse } from 'next/server'

export async function createCustomer(email: string) {
try {
// existing user fetch logic
// in this example we are not using database, so you can only get the portal-once because 2nd time we dont have any logic to fecth customerId.

const customer = await polar.customers.create({
email: email,
})
return { customer_id: customer?.id, message: 'customer created succesfully.' }
} catch (error) {
return { customer_id: null, message: 'customer creation unsuccesfull.' }
}
}

export async function POST(req: NextRequest) {
try {
const { searchParams } = req.nextUrl

let email: string | null | undefined

const body = await req.json().catch(() => ({}))
email = body?.email

if (!email) {
email = searchParams.get('email')
}

if (!email) {
return NextResponse.json({ message: 'Email is required' }, { status: 400 })
}

const { customer_id, message }: any = await createCustomer(email)
if (!customer_id) {
return NextResponse.json({ message, customerId: customer_id }, { status: 400 })
}
const { customerPortalUrl } = await polar.customerSessions.create({
customerId: customer_id,
})

return NextResponse.json({ customerPortalUrl, customerId: customer_id, message }, { status: 201 })
} catch (error) {
console.log('Error in customer portal.', error)
return NextResponse.json({ message: 'Error in customer portal.' }, { status: 500 })
}
}
11 changes: 11 additions & 0 deletions with-nextjs/app/api/products/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { NextResponse } from 'next/server'
import { polar } from '@/app/polar'

export async function GET() {
try {
const products = await polar.products.list({})
return NextResponse.json(products.result.items ?? [], { status: 200 })
} catch (error: any) {
return NextResponse.json({ error: error.message ?? 'Failed to fetch products' }, { status: 500 })
}
}
24 changes: 24 additions & 0 deletions with-nextjs/app/api/webhooks/polar/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Webhooks } from '@polar-sh/nextjs'
import { NextResponse } from 'next/server'

export const POST = async () => {
const webhookSecret = process.env.POLAR_MODE === 'production' ? process.env.POLAR_WEBHOOK_SECRET : process.env.SANDBOX_POLAR_WEBHOOK_SECRET
if (!webhookSecret) {
return NextResponse.json(
{
message: `${process.env.POLAR_MODE === 'production' ? 'POLAR_WEBHOOK_SECRET' : 'SANDBOX_POLAR_WEBHOOK_SECRET'} token is not found.`,
},
{ status: 400 },
)
}
Webhooks({
webhookSecret: webhookSecret,
onPayload: async (payload) => {
// Handle the payload
// No need to return an acknowledge response
},
onOrderPaid: async (event) => {},
})

return NextResponse.json({}, { status: 200 })
}
16 changes: 16 additions & 0 deletions with-nextjs/app/confirmation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use client'

import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export default function ConfirmationPage() {
const router = useRouter()
useEffect(() => {
setTimeout(() => router.replace('/'), 3000)
}, [])
return (
<div className="p-6 w-[40%] m-auto">
<h1 className="text-2xl font-bold">Thank you for your purchase 🎉</h1>
</div>
)
}
Binary file added with-nextjs/app/favicon.ico
Binary file not shown.
122 changes: 122 additions & 0 deletions with-nextjs/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
@import 'tailwindcss';
@import 'tw-animate-css';

@custom-variant dark (&:is(.dark *));

@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}

:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}

.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}

@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
30 changes: 30 additions & 0 deletions with-nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'

const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
})

const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
})

export const metadata: Metadata = {
title: 'Polar with Next.js',
description: 'Example of Polar with Next.js',
}

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
</html>
)
}
Loading