Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
18 changes: 18 additions & 0 deletions with-nuxt/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# https://docs.polar.sh/integrate/oat
POLAR_ACCESS_TOKEN=

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

# URL to redirect to after successful order
POLAR_SUCCESS_URL=

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

SANDBOX_POLAR_WEBHOOK_SECRET=

SANDBOX_POLAR_SUCCESS_URL=

# Polar server mode (production or sandbox)
POLAR_MODE=
24 changes: 24 additions & 0 deletions with-nuxt/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist

# Node dependencies
node_modules

# Logs
logs
*.log

# Misc
.DS_Store
.fleet
.idea

# Local env files
.env
.env.*
!.env.example
3 changes: 3 additions & 0 deletions with-nuxt/.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-nuxt/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"printWidth": 180,
"singleQuote": true
}
109 changes: 109 additions & 0 deletions with-nuxt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
![](../logo.svg)

# Getting Started with Polar and Nuxt

This repo is a demonstration of the integration of Polar features such as Webhooks, Customer Portal and Checkout creation organization in Nuxt.

## Prerequisites

- Node.js installed on your system
- Your POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, SUCCESS_URL and SANDBOX_POLAR_ACCESS_TOKEN, SANDBOX_POLAR_WEBHOOK_SECRET, SANDBOX_POLAR_SUCCESS_URL, and POLAR_MODE
> this is an optional configuration, adjust based on your needs



## 1. Clone the repository

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

## 2. Install dependencies:

```bash
npm install
```

## 3. Configure environment variables:

Create a `.env` file in the project root with your Polar credentials:

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

Add your Polar API credentials to the `.env` file:

```env
POLAR_ACCESS_TOKEN=

POLAR_WEBHOOK_SECRET=

POLAR_SUCCESS_URL=

SANDBOX_POLAR_ACCESS_TOKEN=

SANDBOX_POLAR_WEBHOOK_SECRET=

SANDBOX_POLAR_SUCCESS_URL=

POLAR_MODE=
```

You can find your POLAR_ACCESS_TOKEN and POLAR_WEBHOOK_SECRET variables in your Polar dashboard settings. see `.env.example`

## 4. Start Development Server

```bash
npm run dev
```

Visit `http://localhost:4321` to see the demo interface.

## Configuration

### Polar Dashboard Setup

1. **Create Products**: Set up products in your Polar dashboard
2. **Configure Webhooks**: Add webhook endpoint `https://your-domain.com/api/webhooks/polar`
3. **Get Credentials**: Copy your access token and webhook secret

## Deployment

### Vercel (Recommended)

1. Connect your repository to Vercel
1. Add environment variables in Vercel dashboard
1. Deploy automatically on push

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/polarsource/examples/tree/main/with-nuxt&env=POLAR_ACCESS_TOKEN,POLAR_WEBHOOK_SECRET,POLAR_SUCCESS_URL,SANDBOX_POLAR_ACCESS_TOKEN,SANDBOX_POLAR_WEBHOOK_SECRET,SANDBOX_POLAR_SUCCESS_URL,POLAR_MODE&envDescription=Configure%20your%20Polar%20API%20credentials%20and%20mode.&envLink=https://docs.polar.sh/integrate/webhooks/endpoints#setup-webhooks)

### Other Platforms

The project works with any platform that supports Nuxt:


- Cloudflare
- Netlify
- Node etc.

## 5. Testing

### Local Testing

1. Use Polar's sandbox environment
2. Test with sandbox product IDs
3. Monitor webhook events payloads in your console and
4. Verify your access tokens by running the `validateAccessToken.ts` test in `./scripts`

```bash
npm run validate-token
```

### Webhook Testing

1. Use tools like ngrok for local webhook testing
2. Configure webhook URL in Polar dashboard
3. Configure `vite.server.allowedhosts` in `nuxt.config.ts` to allow it.
4. Trigger test events from Polar dashboard

12 changes: 12 additions & 0 deletions with-nuxt/app/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<template>
<div>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>

<script setup lang="ts">
// You can leave this script block empty.
// It's a good place for app-wide logic if you need it later.
</script>
1 change: 1 addition & 0 deletions with-nuxt/app/assets/css/main.css
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import 'tailwindcss';
54 changes: 54 additions & 0 deletions with-nuxt/app/components/ProductCard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
<div class="bg-card border border-border overflow-hidden hover:shadow-lg transition-shadow">
<NuxtImg :src="image || '/placeholder.svg'" :alt="name" class="w-full h-48 object-cover" />
<div class="p-6">
<h3 class="text-xl font-heading font-semibold text-card-foreground mb-2">
{{ name }}
</h3>
<p class="text-muted-foreground mb-4">{{ description }}</p>
<div class="flex items-center justify-between">
<span class="text-2xl font-medium text-primary">{{ formattedPrice }}</span>
<button @click="buyNow" class="px-4 py-2 bg-gray-200 text-primary-foreground font-medium rounded-lg hover:bg-primary/90 transition-colors cursor-pointer">Buy Now</button>
</div>
</div>
</div>
</template>

<script setup lang="ts">
const props = defineProps<{
id: string
name: string
description?: string | null
image?: string | null
priceAmount?: number
priceCurrency?: string
}>()

const formattedPrice = computed(() =>
props.priceAmount
? new Intl.NumberFormat('en-US', {
style: 'currency',
currency: props.priceCurrency || 'USD',
}).format(props.priceAmount / 100)
: '—',
)

const buyNow = () => {
const customerId = sessionStorage.getItem('customerId')
const customerEmail = sessionStorage.getItem('customerEmail')
const customerName = sessionStorage.getItem('customerName')
const url = new URL('/api/checkout', window.location.origin)

url.searchParams.append('products', props.id)
if (customerId) {
url.searchParams.append('customerId', customerId)
}
if (customerEmail) {
url.searchParams.append('customerEmail', customerEmail)
}
if (customerName) {
url.searchParams.append('customerName', customerName)
}
window.location.href = url.toString()
}
</script>
30 changes: 30 additions & 0 deletions with-nuxt/app/components/ProductsGrid.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<template>
<section id="products" class="py-16">
<div class="container mx-auto px-4">
<div v-if="!products || products.length === 0" class="text-center">
<p class="text-muted-foreground">No products available.</p>
</div>
<template v-else>
<h2 class="text-3xl font-heading font-medium text-center mb-12">Products</h2>
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
<ProductCard
v-for="product in products"
:key="product.id"
:id="product.id"
:name="product.name"
:description="product.description"
:image="product.medias?.[0]?.publicUrl"
:priceAmount="product.prices?.[0]?.priceAmount"
:priceCurrency="product.prices?.[0]?.priceCurrency"
/>
</div>
</template>
</div>
</section>
</template>

<script setup lang="ts">
defineProps({
products: Array,
})
</script>
20 changes: 20 additions & 0 deletions with-nuxt/app/layouts/default.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template>
<div class="bg-background text-foreground font-sans">
<header class="border-b border-border bg-card">
<div class="container mx-auto px-4 py-4">
<nav class="flex items-center justify-between">
<NuxtLink to="/" class="text-2xl font-heading font-bold text-primary"> Polar with Nuxt Example </NuxtLink>
<div class="flex items-center gap-4">
<NuxtLink to="/" class="text-muted-foreground hover:text-foreground transition-colors border p-3"> Products </NuxtLink>
<NuxtLink to="/customer-portal" class="text-muted-foreground hover:text-foreground transition-colors border p-3"> Customer Portal </NuxtLink>
</div>
</nav>
</div>
</header>
<main class="min-h-screen">
<slot />
</main>
</div>
</template>

<script setup lang="ts"></script>
67 changes: 67 additions & 0 deletions with-nuxt/app/pages/customer-portal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<template>
<section class="py-16">
<div class="container mx-auto px-4">
<div class="max-w-2xl mx-auto text-center">
<h1 class="text-4xl font-heading font-bold text-primary mb-6">Customer Portal</h1>
<p class="text-xl text-muted-foreground mb-8">Access your purchases and manage your account</p>

<div class="bg-card border border-border rounded-lg p-6">
<form @submit.prevent="accessPortal" class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-card-foreground mb-2"> Your Name </label>
<input
type="text"
id="name"
v-model="name"
class="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-ring bg-input text-foreground"
placeholder="Your Name"
/>
</div>
<div>
<label for="email" class="block text-sm font-medium text-card-foreground mb-2"> Enter your email address </label>
<input
type="email"
id="email"
v-model="email"
class="w-full px-3 py-2 border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-ring bg-input text-foreground"
placeholder="your@email.com"
/>
</div>
<button type="submit" class="w-full px-6 py-3 bg-gray-200 text-primary-foreground font-medium rounded-lg hover:bg-primary/90 transition-colors cursor-pointer">
Access Portal
</button>
</form>

<p class="text-sm text-muted-foreground mt-4">You'll be redirected to your secure customer portal</p>
</div>
</div>
</div>
</section>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const name = ref('')
const email = ref('')

onMounted(() => {
name.value = sessionStorage.getItem('customerName') || ''
email.value = sessionStorage.getItem('customerEmail') || ''
})

const accessPortal = () => {
const customerId = sessionStorage.getItem('customerId')

sessionStorage.setItem('customerName', name.value)
sessionStorage.setItem('customerEmail', email.value)

if (customerId) {
window.location.href = `/api/customer-portal?customerId=${customerId}`
} else if (email.value) {
window.location.href = `/api/customer-portal?customerEmail=${email.value}`
} else {
alert('Please provide an email address to access the portal.')
}
}
</script>
9 changes: 9 additions & 0 deletions with-nuxt/app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<div>
<ProductsGrid :products="products" />
</div>
</template>

<script setup lang="ts">
const { data: products } = await useAsyncData('products', () => $fetch('/api/products'))
</script>
Loading