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>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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 })
|
||||
}
|
||||
Reference in New Issue
Block a user