-
Notifications
You must be signed in to change notification settings - Fork 1
Stack Auth & Polar.sh #143
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,34 +1,131 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { NextRequest, NextResponse } from "next/server"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import { getUser } from "@/lib/stack-auth"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||
| getPolarClient, | ||||||||||||||||||||||||||||||||||||||||||||||||
| getPolarOrganizationId, | ||||||||||||||||||||||||||||||||||||||||||||||||
| getPolarProProductId, | ||||||||||||||||||||||||||||||||||||||||||||||||
| isPolarConfigured, | ||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@/lib/polar-client"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { getSanitizedErrorDetails, validatePolarEnv } from "@/lib/env-validation"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| type CheckoutRequest = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| productId?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| successUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| cancelUrl?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function getBaseUrl(): string { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.NEXT_PUBLIC_APP_URL || | ||||||||||||||||||||||||||||||||||||||||||||||||
| process.env.NEXT_PUBLIC_BASE_URL || | ||||||||||||||||||||||||||||||||||||||||||||||||
| "http://localhost:3000" | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| function buildResponse( | ||||||||||||||||||||||||||||||||||||||||||||||||
| status: number, | ||||||||||||||||||||||||||||||||||||||||||||||||
| payload: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| error: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| details?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| isConfigError?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||
| adminMessage?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json(payload, { status }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // NOTE: Polar checkout will be implemented after Stack Auth is fully configured | ||||||||||||||||||||||||||||||||||||||||||||||||
| // This is a placeholder route for now | ||||||||||||||||||||||||||||||||||||||||||||||||
| export async function POST(req: NextRequest) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Authenticate user with Stack Auth | ||||||||||||||||||||||||||||||||||||||||||||||||
| const user = await getUser(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (!user) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||
| { error: "Unauthorized - Please sign in to continue" }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 401 } | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return buildResponse(401, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Unauthorized", | ||||||||||||||||||||||||||||||||||||||||||||||||
| details: "Please sign in to continue", | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // TODO: Implement Polar checkout once Stack Auth is configured with proper API keys | ||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||
| { error: "Polar checkout not yet configured. Please set up Stack Auth first." }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 501 } | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isPolarConfigured()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return buildResponse(503, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Payment system is not configured", | ||||||||||||||||||||||||||||||||||||||||||||||||
| details: "Please contact support while we finish setting up billing.", | ||||||||||||||||||||||||||||||||||||||||||||||||
| isConfigError: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| adminMessage: "Missing Polar environment variables. Run validatePolarEnv() for details.", | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const body = (await req.json().catch(() => ({}))) as CheckoutRequest; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const requestedProductId = body.productId?.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| let productId = requestedProductId ?? ""; | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!productId) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| productId = getPolarProProductId(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return buildResponse(503, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Polar product is not configured", | ||||||||||||||||||||||||||||||||||||||||||||||||
| details: "Set NEXT_PUBLIC_POLAR_PRO_PRODUCT_ID to your Polar product ID.", | ||||||||||||||||||||||||||||||||||||||||||||||||
| isConfigError: true, | ||||||||||||||||||||||||||||||||||||||||||||||||
| adminMessage: "NEXT_PUBLIC_POLAR_PRO_PRODUCT_ID is missing", | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!productId) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return buildResponse(500, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Unable to determine Polar product", | ||||||||||||||||||||||||||||||||||||||||||||||||
| details: "Product ID resolution failed unexpectedly.", | ||||||||||||||||||||||||||||||||||||||||||||||||
| adminMessage: "Polar product ID empty after configuration check", | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| validatePolarEnv(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const polar = getPolarClient(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const baseUrl = getBaseUrl(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const successUrl = | ||||||||||||||||||||||||||||||||||||||||||||||||
| body.successUrl || `${baseUrl}/dashboard/subscription?status=success`; | ||||||||||||||||||||||||||||||||||||||||||||||||
| const cancelUrl = | ||||||||||||||||||||||||||||||||||||||||||||||||
| body.cancelUrl || `${baseUrl}/dashboard/subscription?status=cancelled`; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const checkout = await polar.checkoutSessions.create({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| organizationId: getPolarOrganizationId(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| productPriceId: productId, | ||||||||||||||||||||||||||||||||||||||||||||||||
| successUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||
| cancelUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||
| customerEmail: user.primaryEmail ?? undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||
| customerName: user.name ?? undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||
| metadata: { | ||||||||||||||||||||||||||||||||||||||||||||||||
| userId: user.id, | ||||||||||||||||||||||||||||||||||||||||||||||||
| userEmail: user.primaryEmail ?? undefined, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+90
to
+101
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainUse The CI error stems from calling Also, the Checkout API expects product IDs via Consider updating this block along these lines: - const checkout = await polar.checkoutSessions.create({
- organizationId: getPolarOrganizationId(),
- productPriceId: productId,
- successUrl,
- cancelUrl,
- customerEmail: user.primaryEmail ?? undefined,
- customerName: user.name ?? undefined,
- metadata: {
- userId: user.id,
- userEmail: user.primaryEmail ?? undefined,
- },
- });
+ const checkout = await polar.checkouts.create({
+ products: [productId],
+ successUrl,
+ cancelUrl,
+ customerEmail: user.primaryEmail ?? undefined,
+ customerName: user.displayName ?? undefined,
+ metadata: {
+ userId: user.id,
+ userEmail: user.primaryEmail ?? undefined,
+ },
+ });Please verify the exact option names against the installed 🌐 Web query: 💡 Result: Short answer — signature and how to pass fields TypeScript signature (shape used by @polar-sh/sdk — camelCased by the TS SDK): Example minimal usage: Sources: Polar docs — Create Checkout Session and TypeScript SDK (camelCase note). [1][2] References Use The current call to The Checkout API also provides Update the code: - const checkout = await polar.checkoutSessions.create({
- organizationId: getPolarOrganizationId(),
- productPriceId: productId,
+ const checkout = await polar.checkouts.create({
+ products: [productId],
successUrl,
- cancelUrl,
+ returnUrl,
customerEmail: user.primaryEmail ?? undefined,
- customerName: user.name ?? undefined,
+ customerName: user.displayName ?? undefined,
metadata: {
userId: user.id,
userEmail: user.primaryEmail ?? undefined,
},
});Verify that 📝 Committable suggestion
Suggested change
🧰 Tools🪛 GitHub Actions: CI[error] 90-90: TS2339: Property 'checkoutSessions' does not exist on type 'Polar'. [error] 96-96: TS2339: Property 'name' does not exist on type 'CurrentServerUser'. 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (!checkout?.url) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Polar checkout session did not include a redirect URL"); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| checkoutId: checkout.id, | ||||||||||||||||||||||||||||||||||||||||||||||||
| url: checkout.url, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const details = getSanitizedErrorDetails(error); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const adminMessage = | ||||||||||||||||||||||||||||||||||||||||||||||||
| error instanceof Error ? error.message : "Unknown Polar checkout error"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Error creating Polar checkout session:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return NextResponse.json( | ||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Failed to create checkout session", | ||||||||||||||||||||||||||||||||||||||||||||||||
| details: error instanceof Error ? error.message : "Unknown error" | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| { status: 500 } | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const isAuthError = | ||||||||||||||||||||||||||||||||||||||||||||||||
| typeof details === "string" && | ||||||||||||||||||||||||||||||||||||||||||||||||
| (details.includes("Authentication failed") || | ||||||||||||||||||||||||||||||||||||||||||||||||
| details.includes("invalid or expired")); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const status = isAuthError ? 401 : 500; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return buildResponse(status, { | ||||||||||||||||||||||||||||||||||||||||||||||||
| error: "Unable to start checkout", | ||||||||||||||||||||||||||||||||||||||||||||||||
| details, | ||||||||||||||||||||||||||||||||||||||||||||||||
| adminMessage, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Fix Stack Auth user field: use
displayNameinstead ofnameCurrentServerUser(the return type ofstackServerApp.getUser()) inherits fromCurrentUser, which exposesdisplayName, notname. (docs.stack-auth.com) This causes theProperty 'name' does not exist on type 'CurrentServerUser'error.Change the customer name mapping to use
displayName:🌐 Web query:
💡 Result:
Use displayName — CurrentServerUser inherits CurrentUser which exposes displayName (not name). [1][2]
Sources:
[1] Stack Auth docs — CurrentServerUser / CurrentUser type reference.
[2] Stack Auth docs (next) — CurrentUser.displayName description.
Fix Stack Auth user field: use
displayNameinstead ofnameCurrentServerUser(the return type ofstackServerApp.getUser()) inherits fromCurrentUser, which exposesdisplayName, notname. This causes theProperty 'name' does not exist on type 'CurrentServerUser'error.Change the customer name mapping to use
displayName:📝 Committable suggestion
🧰 Tools
🪛 GitHub Actions: CI
[error] 96-96: TS2339: Property 'name' does not exist on type 'CurrentServerUser'.
🤖 Prompt for AI Agents