From ce6d1e320b6faad620213b62de49f30ec52afe7d Mon Sep 17 00:00:00 2001 From: Sanju Sivalingam Date: Wed, 18 Feb 2026 23:28:19 +0530 Subject: [PATCH] fix: handle Polar activation limit gracefully + switch checkout to command pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Server: wrap licenseKeys.activate() in try/catch — if activation limit reached, treat as already-activated and proceed to store the key - Web: switch activateFromCheckout from form() to command() pattern for programmatic invocation with proper error handling - Activate page: auto-fires on mount, shows spinner/error states, retry button --- server/src/routes/license.ts | 45 +++++++--- web/src/lib/api/license.remote.ts | 83 +++++++------------ .../routes/dashboard/activate/+page.svelte | 56 +++++++++---- 3 files changed, 104 insertions(+), 80 deletions(-) diff --git a/server/src/routes/license.ts b/server/src/routes/license.ts index d079eb1..1d265a4 100644 --- a/server/src/routes/license.ts +++ b/server/src/routes/license.ts @@ -55,12 +55,18 @@ license.post("/activate", async (c) => { // Determine plan from benefit ID or default to "ltd" const plan = "ltd"; - // Activate the key (tracks activation count on Polar's side) - await polar.licenseKeys.activate({ - key, - organizationId: env.POLAR_ORGANIZATION_ID, - label: `${currentUser.email}`, - }); + // Activate the key (may fail if already activated from previous attempt) + try { + await polar.licenseKeys.activate({ + key, + organizationId: env.POLAR_ORGANIZATION_ID, + label: `${currentUser.email}`, + }); + } catch (activateErr) { + const msg = activateErr instanceof Error ? activateErr.message : String(activateErr); + if (!msg.includes("limit")) throw activateErr; + console.log(`[License] Key already activated for ${currentUser.email}, storing anyway`); + } // Store on user record await db @@ -140,12 +146,19 @@ license.post("/activate-checkout", async (c) => { return c.json({ error: "No license key found for this purchase" }, 400); } - // 3. Activate the key - await polar.licenseKeys.activate({ - key: customerKey.key, - organizationId: env.POLAR_ORGANIZATION_ID, - label: `${currentUser.email}`, - }); + // 3. Activate the key (may fail if already activated from previous attempt) + try { + await polar.licenseKeys.activate({ + key: customerKey.key, + organizationId: env.POLAR_ORGANIZATION_ID, + label: `${currentUser.email}`, + }); + } catch (activateErr) { + const msg = activateErr instanceof Error ? activateErr.message : String(activateErr); + if (!msg.includes("limit")) throw activateErr; + // Limit reached = key was already activated, that's fine — proceed to store + console.log(`[License] Key already activated for ${currentUser.email}, storing anyway`); + } // 4. Store on user record await db @@ -161,6 +174,14 @@ license.post("/activate-checkout", async (c) => { } catch (err) { const message = err instanceof Error ? err.message : String(err); console.error(`[License] Checkout activation failed for ${currentUser.email}:`, message); + + if (message.includes("limit")) { + return c.json({ error: "License key activation limit reached" }, 400); + } + if (message.includes("not found") || message.includes("invalid")) { + return c.json({ error: "Invalid or expired checkout" }, 400); + } + return c.json({ error: "Failed to activate from checkout" }, 500); } }); diff --git a/web/src/lib/api/license.remote.ts b/web/src/lib/api/license.remote.ts index 5fbf451..a0959c8 100644 --- a/web/src/lib/api/license.remote.ts +++ b/web/src/lib/api/license.remote.ts @@ -1,69 +1,44 @@ -import { form, query, getRequestEvent } from '$app/server'; +import { form, command, getRequestEvent } from '$app/server'; import { redirect } from '@sveltejs/kit'; -import { db } from '$lib/server/db'; -import { user } from '$lib/server/db/schema'; -import { eq } from 'drizzle-orm'; import { env } from '$env/dynamic/private'; +import * as v from 'valibot'; import { activateLicenseSchema, activateCheckoutSchema } from '$lib/schema/license'; -export const getLicenseStatus = query(async () => { +/** Forward a request to the DroidClaw server with internal auth */ +async function serverFetch(path: string, body: Record) { const { locals } = getRequestEvent(); - if (!locals.user) return null; + if (!locals.user) throw new Error('Not authenticated'); - const rows = await db - .select({ plan: user.plan, polarLicenseKey: user.polarLicenseKey }) - .from(user) - .where(eq(user.id, locals.user.id)) - .limit(1); + const serverUrl = env.SERVER_URL || 'http://localhost:8080'; + const internalSecret = env.INTERNAL_SECRET || ''; - const row = rows[0]; - return { - activated: !!row?.plan, - plan: row?.plan ?? null, - licenseKey: row?.polarLicenseKey ?? null - }; -}); + const res = await fetch(`${serverUrl}${path}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-internal-secret': internalSecret, + 'x-internal-user-id': locals.user.id + }, + body: JSON.stringify(body) + }); + + const data = await res.json().catch(() => ({ error: 'Unknown error' })); + if (!res.ok) throw new Error(data.error ?? `Error ${res.status}`); + return data; +} export const activateLicense = form(activateLicenseSchema, async (data) => { const { locals } = getRequestEvent(); if (!locals.user) return; - const serverUrl = env.SERVER_URL || 'http://localhost:8080'; - const internalSecret = env.INTERNAL_SECRET || ''; - - const res = await fetch(`${serverUrl}/license/activate`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-internal-secret': internalSecret, - 'x-internal-user-id': locals.user.id - }, - body: JSON.stringify({ key: data.key }) - }); - - if (res.ok) { - redirect(303, '/dashboard'); - } + await serverFetch('/license/activate', { key: data.key }); + redirect(303, '/dashboard'); }); -export const activateFromCheckout = form(activateCheckoutSchema, async (data) => { - const { locals } = getRequestEvent(); - if (!locals.user) return; - - const serverUrl = env.SERVER_URL || 'http://localhost:8080'; - const internalSecret = env.INTERNAL_SECRET || ''; - - const res = await fetch(`${serverUrl}/license/activate-checkout`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-internal-secret': internalSecret, - 'x-internal-user-id': locals.user.id - }, - body: JSON.stringify({ checkoutId: data.checkoutId }) - }); - - if (res.ok) { - redirect(303, '/dashboard'); +export const activateFromCheckout = command( + v.object({ checkoutId: v.string() }), + async ({ checkoutId }) => { + const result = await serverFetch('/license/activate-checkout', { checkoutId }); + return result; } -}); +); diff --git a/web/src/routes/dashboard/activate/+page.svelte b/web/src/routes/dashboard/activate/+page.svelte index 7ed23cb..0f326b6 100644 --- a/web/src/routes/dashboard/activate/+page.svelte +++ b/web/src/routes/dashboard/activate/+page.svelte @@ -1,38 +1,66 @@ {#if checkoutId}
-
-
- + {#if checkoutStatus === 'activating'} +
+
+ +
+

Activating your license...

+

+ We're setting up your account. This will only take a moment. +

+
+ {:else if checkoutStatus === 'error'} +
+
+ +
+

Activation failed

+

{checkoutError}

-

Activating your license...

-

- We're setting up your account. This will only take a moment. -

-
-
- -
+ {/if}