-
Notifications
You must be signed in to change notification settings - Fork 1
Stack Auth & Polar.sh #143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
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