Skip to content
Closed
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
14 changes: 14 additions & 0 deletions with-nextjs/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# create an organization access token from your organization settings in Polar.
# SANDBOX
NEXT_PUBLIC_SANDBOX_POLAR_ACCESS_TOKEN=
POLAR_SANDBOX_WEBHOOK_SECRET=

# PRODUCTION
POLAR_ACCESS_TOKEN=
POLAR_WEBHOOK_SECRET=

# SERVER
NODE_ENV=

# client url
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
git clone https://github.com/polarsource/examples.git
cd with-nextjs
```
# How to use

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

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

Get your Polar credentials
```bash
Polar_ACCESS_TOKEN
POLAR_WEBHOOK_SECRET
```

Run the command below to install project dependencies:
```bash
npm install
# install the dependencies

```
Run the Next.js application using the following command:
```bash
npm run dev
# run the project
```
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 } 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: process.env.SUCCESS_URL,
})

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

import { polar } from '@/app/polar'
import { NextRequest, NextResponse } from 'next/server'

async function getOrCreateCustomer(email: string) {
try {
const existingCustomers = await polar.customers.list({ email: email, limit: 1 })

if (existingCustomers.result.items.length > 0) {
const customerId = existingCustomers.result.items[0].id
return { customer_id: customerId, message: 'Customer found.' }
}

const newCustomer = await polar.customers.create({ email })
return {
customer_id: newCustomer?.id,
message: 'Customer created successfully.',
}
} catch (error) {
console.error('Failed to get or create customer', error)
return {
customer_id: null,
message: 'Customer operation was unsuccessful.',
}
}
}

export async function POST(req: NextRequest) {
try {
let email: string | null = null

try {
const body = await req.json()
email = body.email
} catch (error) { }

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

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

const { customer_id, message }: any = await getOrCreateCustomer(email)
if (!customer_id) {
return NextResponse.json(
{ message, customerId: customer_id },
{ status: 400 },
)
}

const customerSession = await polar.customerSessions.create({
customerId: customer_id,
})

const portalUrl = customerSession.customerPortalUrl

if (!portalUrl || typeof portalUrl !== 'string') {
console.error(
'Error: customerPortalUrl from Polar SDK was not a string.',
portalUrl,
)
return NextResponse.json(
{ message: 'Internal server error: Could not retrieve a valid portal URL.' },
{ status: 500 },
)
}

return NextResponse.json(
{ customerPortalUrl: portalUrl, 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 },
)
}
}

19 changes: 19 additions & 0 deletions with-nextjs/app/api/products/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// app/api/products/route.ts

import { polar } from '@/app/polar'
import { NextResponse } from 'next/server'

// This is a server-side-only function. It can safely access all your secrets.
export async function GET() {
try {
const products = await polar.products.list({ isArchived: false })
return NextResponse.json(products?.result?.items ?? [])
} catch (error) {
// If something goes wrong on the server, we'll see it in the Vercel logs.
console.error('API Error: Failed to fetch Polar products:', error)
return NextResponse.json(
{ message: 'Internal Server Error' },
{ status: 500 },
)
}
}
20 changes: 20 additions & 0 deletions with-nextjs/app/api/webhooks/polar/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Webhooks } from '@polar-sh/nextjs'
import { NextResponse } from 'next/server'

export const POST = async () => {
const token = process.env.NODE_ENV === 'production' ? process.env.POLAR_WEBHOOK_SECRET : process.env.POLAR_SANDBOX_WEBHOOK_SECRET
if (!token) {
return NextResponse.json({ message: 'Polar web secret is not found.' }, { status: 400 })
}
Webhooks({
webhookSecret: process.env.POLAR_WEBHOOK_SECRET!,

// onPayload: async (payload) => {
// Handle the payload
// },
// onOrderPaid: async (event) => { },

})

return NextResponse.json({}, { status: 200 })
}
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;
}
}
Loading