Initial Falah Mobile rebuild — all 7 blocks complete
- Auth system (login/register/profile) - Nur AI chat with persona system - Souq marketplace with FLH economy - Forum community with Shariah moderation - Wallet & FLH top-up - Premium/Pro tier upgrade with Polar.sh - Halal Monitor with map & bookmarks - Home dashboard with daily verse & streaks - Health endpoints Next.js 16.2.7, Prisma v5 SQLite, JWT auth, Tailwind CSS v4
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
/**
|
||||
* Polar.sh webhook handler.
|
||||
*
|
||||
* In production, verify the webhook signature using Polar's webhook secret.
|
||||
* For development, we accept a direct ?userId=&tier= fallback for testing.
|
||||
*/
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
// Check for direct fallback query params (dev/testing)
|
||||
const url = new URL(req.url);
|
||||
const directUserId = url.searchParams.get("userId");
|
||||
const directTier = url.searchParams.get("tier");
|
||||
|
||||
if (directUserId && directTier) {
|
||||
return handleDirectUpgrade(directUserId, directTier);
|
||||
}
|
||||
|
||||
// Production path: Parse Polar webhook event
|
||||
const body = await req.json();
|
||||
const event = body.type || body.event;
|
||||
|
||||
if (!event) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing event type" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Handle checkout.completed event
|
||||
if (
|
||||
event === "checkout.completed" ||
|
||||
event === "checkout.updated"
|
||||
) {
|
||||
const checkout = body.data?.checkout || body.data;
|
||||
if (!checkout) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing checkout data" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const userId = checkout.metadata?.userId;
|
||||
const priceId = checkout.product?.priceId ||
|
||||
checkout.productPrice?.id ||
|
||||
checkout.metadata?.priceId;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing userId in metadata" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Determine tier from priceId or product metadata
|
||||
let tier: "premium" | "pro" = "premium";
|
||||
if (priceId?.includes("pro") || checkout.metadata?.tier === "pro") {
|
||||
tier = "pro";
|
||||
}
|
||||
|
||||
return applyUpgrade(userId, tier);
|
||||
}
|
||||
|
||||
// Acknowledge other events silently
|
||||
return NextResponse.json({ received: true });
|
||||
} catch (error) {
|
||||
console.error("Polar webhook error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Webhook processing failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDirectUpgrade(userId: string, tier: string) {
|
||||
if (tier !== "premium" && tier !== "pro") {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid tier. Must be 'premium' or 'pro'." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
return applyUpgrade(userId, tier as "premium" | "pro");
|
||||
}
|
||||
|
||||
async function applyUpgrade(userId: string, tier: "premium" | "pro") {
|
||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "User not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const trialEndsAt = new Date();
|
||||
trialEndsAt.setDate(trialEndsAt.getDate() + 7);
|
||||
|
||||
const updateData: Record<string, unknown> = {
|
||||
trialEndsAt,
|
||||
};
|
||||
|
||||
if (tier === "pro") {
|
||||
updateData.isPro = true;
|
||||
updateData.isPremium = false; // Pro supersedes Premium
|
||||
} else {
|
||||
updateData.isPremium = true;
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
tier,
|
||||
trialEndsAt,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user