Files
falah-mobile/src/app/api/webhooks/polar/route.ts
T
FalahMobile e2365e29fe Add Prayer Times, Dhikr counter, Quran tracker with mobile UI overhaul
- Prayer times page: arc progress ring, next-prayer countdown, clean prayer list
- Dhikr counter: SVG ring with glow, 3-phase sequence (SubhanAllah/Alhamdulillah/AllahuAkbar), streak tracking
- Daily Quran reading tracker API routes (GET/POST)
- Bottom nav updated: Prayer + Dhikr tabs, Repeat2 icon for dhikr, Moon for prayer
- Home page redirects to /prayer as primary entry point
- Dockerfile: openssl added to builder stage, explicit prisma schema path fixes build
- Schema: dhikrStreak, quranStreak on User; DhikrSession and QuranReading models
- Stub wallet top-up checkout to fix TypeScript build error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 08:05:51 +01:00

88 lines
2.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { createHmac } from 'crypto'
// Polar product IDs → tier flags
const PRODUCT_TIERS: Record<string, { isPremium: boolean; isPro: boolean }> = {
'a291a3d9-fa82-4e88-ba89-c6005401398f': { isPremium: true, isPro: false }, // Premium
'7f983017-e1f3-440e-be2a-5a5d19c9c7b0': { isPremium: true, isPro: true }, // Pro
}
function verifySignature(body: string, headers: Headers): boolean {
const secret = process.env.POLAR_WEBHOOK_SECRET
if (!secret) return false
// Polar uses Svix: signature is over "id.timestamp.body"
const webhookId = headers.get('webhook-id')
const timestamp = headers.get('webhook-timestamp')
const signature = headers.get('webhook-signature')
if (!webhookId || !timestamp || !signature) return false
const signedContent = `${webhookId}.${timestamp}.${body}`
const secretBytes = Buffer.from(secret.replace('whsec_', ''), 'base64')
const computed = createHmac('sha256', secretBytes).update(signedContent).digest('base64')
// Signature header may contain multiple: "v1,<sig1> v1,<sig2>"
return signature.split(' ').some(s => s === `v1,${computed}`)
}
export async function POST(req: NextRequest) {
const body = await req.text()
if (process.env.POLAR_WEBHOOK_SECRET && !verifySignature(body, req.headers)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
}
let event: { type: string; data: any }
try {
event = JSON.parse(body)
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
}
const { type, data } = event
try {
switch (type) {
case 'subscription.created':
case 'subscription.updated': {
const userId = data.metadata?.userId || data.customer?.metadata?.userId
if (!userId) break
const productId = data.product_id || data.product?.id
const tier = productId ? PRODUCT_TIERS[productId] : null
const isActive = ['active', 'trialing'].includes(data.status)
if (isActive && tier) {
await prisma.user.update({
where: { id: userId },
data: tier,
})
} else if (!isActive) {
await prisma.user.update({
where: { id: userId },
data: { isPremium: false, isPro: false },
})
}
break
}
case 'subscription.revoked':
case 'subscription.canceled': {
const userId = data.metadata?.userId || data.customer?.metadata?.userId
if (!userId) break
await prisma.user.update({
where: { id: userId },
data: { isPremium: false, isPro: false },
})
break
}
}
} catch (e) {
console.error('Polar webhook handler error:', e)
return NextResponse.json({ error: 'Handler error' }, { status: 500 })
}
return NextResponse.json({ received: true })
}