Skip to content

Commit f31b0c0

Browse files
Merge pull request #134 from ivancernja/add-encore-example
Add Encore example
2 parents 56c4994 + 34ad150 commit f31b0c0

10 files changed

Lines changed: 194 additions & 0 deletions

File tree

with-encore/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
dist
2+
node_modules
3+
encore.gen

with-encore/.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
dist
3+
encore.gen

with-encore/.prettierrc.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"printWidth": 180,
3+
"singleQuote": true,
4+
"semi": false
5+
}

with-encore/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
![](../logo.svg)
2+
3+
# Getting started with Polar and Encore
4+
5+
## Clone the repository
6+
7+
```bash
8+
npx degit polarsource/examples/with-encore ./with-encore
9+
```
10+
11+
## Prerequisites
12+
13+
- [Encore CLI](https://encore.dev/docs/ts/install) installed
14+
- A [Polar](https://polar.sh) account with an access token and a product created
15+
16+
## How to use
17+
18+
1. Run the command below to install project dependencies:
19+
20+
```
21+
npm install
22+
```
23+
24+
2. Set up your Polar secrets:
25+
26+
```
27+
encore secret set --type local POLAR_ACCESS_TOKEN
28+
encore secret set --type local POLAR_WEBHOOK_SECRET
29+
```
30+
31+
3. Run the Encore application using the following command:
32+
33+
```
34+
encore run
35+
```
36+
37+
4. To receive webhooks locally, use the [Polar CLI](https://polar.sh/docs/integrate/webhooks/locally):
38+
39+
```
40+
polar listen http://localhost:4000/polar/webhooks
41+
```

with-encore/encore.app

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"id": ""
3+
}

with-encore/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"type": "module",
3+
"name": "with-encore",
4+
"scripts": {
5+
"dev": "encore run",
6+
"format": "prettier --write '**/*' --ignore-unknown"
7+
},
8+
"dependencies": {
9+
"@polar-sh/sdk": "^0.41.5",
10+
"standardwebhooks": "^1.0.0",
11+
"encore.dev": "^1.46.0"
12+
},
13+
"devDependencies": {
14+
"prettier": "^3.6.2"
15+
}
16+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Service } from 'encore.dev/service'
2+
3+
export default new Service('payments')

with-encore/payments/payments.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { api } from 'encore.dev/api'
2+
import { Webhook } from 'standardwebhooks'
3+
import { polar, POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET } from './polar'
4+
5+
// Home route - list products
6+
export const home = api.raw({ expose: true, path: '/', method: 'GET' }, async (req, resp) => {
7+
const products = await polar.products.list({ isArchived: false })
8+
resp.writeHead(200, { 'Content-Type': 'text/html' })
9+
resp.end(`<html><body>
10+
<form action="/portal" method="get">
11+
<input type="email" name="email" placeholder="Email" required />
12+
<button type="submit">Open Customer Portal</button>
13+
</form>
14+
${products.result.items.map((product) => `<div><a target="_blank" href="/checkout?products=${product.id}">${product.name}</a></div>`).join('')}
15+
</body></html>`)
16+
})
17+
18+
// Checkout route - create a checkout session and redirect
19+
export const checkout = api.raw({ expose: true, path: '/checkout', method: 'GET' }, async (req, resp) => {
20+
const url = new URL(req.url!, `http://${req.headers.host}`)
21+
const productIds = url.searchParams.get('products')
22+
23+
if (!productIds) {
24+
resp.writeHead(400)
25+
resp.end('Missing products parameter')
26+
return
27+
}
28+
29+
const checkoutSession = await polar.checkouts.create({
30+
products: typeof productIds === 'string' ? [productIds] : productIds,
31+
successUrl: `http://${req.headers.host}/`,
32+
})
33+
34+
resp.writeHead(302, { Location: checkoutSession.url })
35+
resp.end()
36+
})
37+
38+
// Customer portal route - redirect to Polar customer portal
39+
export const portal = api.raw({ expose: true, path: '/portal', method: 'GET' }, async (req, resp) => {
40+
const url = new URL(req.url!, `http://${req.headers.host}`)
41+
const email = url.searchParams.get('email')
42+
43+
if (!email) {
44+
resp.writeHead(400)
45+
resp.end('Missing email parameter')
46+
return
47+
}
48+
49+
const customer = await polar.customers.list({ email })
50+
51+
if (!customer.result.items.length) {
52+
resp.writeHead(404)
53+
resp.end('Customer not found')
54+
return
55+
}
56+
57+
const session = await polar.customerSessions.create({
58+
customerId: customer.result.items[0].id,
59+
})
60+
61+
resp.writeHead(302, { Location: session.customerPortalUrl })
62+
resp.end()
63+
})
64+
65+
// Webhook route - verify and handle Polar webhook events
66+
export const webhooks = api.raw({ expose: true, path: '/polar/webhooks', method: 'POST' }, async (req, resp) => {
67+
const chunks: Buffer[] = []
68+
for await (const chunk of req) {
69+
chunks.push(chunk)
70+
}
71+
const body = Buffer.concat(chunks).toString('utf-8')
72+
73+
const headers: Record<string, string> = {}
74+
for (const [key, value] of Object.entries(req.headers)) {
75+
headers[key] = Array.isArray(value) ? value[0] : (value || '')
76+
}
77+
78+
try {
79+
const base64Secret = Buffer.from(POLAR_WEBHOOK_SECRET().trim(), 'utf-8').toString('base64')
80+
const wh = new Webhook(base64Secret)
81+
const payload = wh.verify(body, headers) as any
82+
83+
console.log(`[Polar] Received event: ${payload.type}`, payload.data.id)
84+
85+
resp.writeHead(200, { 'Content-Type': 'application/json' })
86+
resp.end(JSON.stringify({ received: true }))
87+
} catch (error: any) {
88+
console.error('[Polar] Invalid webhook signature:', error?.message)
89+
resp.writeHead(403)
90+
resp.end(JSON.stringify({ error: 'Invalid signature' }))
91+
}
92+
})

with-encore/payments/polar.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { Polar } from '@polar-sh/sdk'
2+
import { secret } from 'encore.dev/config'
3+
4+
export const POLAR_ACCESS_TOKEN = secret('POLAR_ACCESS_TOKEN')
5+
export const POLAR_WEBHOOK_SECRET = secret('POLAR_WEBHOOK_SECRET')
6+
7+
export const polar = new Polar({
8+
accessToken: POLAR_ACCESS_TOKEN(),
9+
server: 'sandbox',
10+
})

with-encore/tsconfig.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"moduleResolution": "bundler",
6+
"outDir": "./dist",
7+
"rootDir": "./",
8+
"skipLibCheck": true,
9+
"esModuleInterop": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"strict": false,
12+
"paths": {
13+
"~encore/*": ["./encore.gen/*"]
14+
}
15+
},
16+
"include": ["**/*.ts"],
17+
"exclude": ["node_modules", "dist"]
18+
}

0 commit comments

Comments
 (0)