fix: handle Polar activation limit gracefully + switch checkout to command pattern

- 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
This commit is contained in:
Sanju Sivalingam
2026-02-18 23:28:19 +05:30
parent e9d1c863e1
commit ce6d1e320b
3 changed files with 104 additions and 80 deletions

View File

@@ -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<string, unknown>) {
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;
}
});
);

View File

@@ -1,38 +1,66 @@
<script lang="ts">
import { activateLicense, activateFromCheckout } from '$lib/api/license.remote';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { onMount } from 'svelte';
import Icon from '@iconify/svelte';
import { LICENSE_ACTIVATE_CHECKOUT, LICENSE_ACTIVATE_MANUAL, LICENSE_PURCHASE_CLICK } from '$lib/analytics/events';
const checkoutId = page.url.searchParams.get('checkout_id');
let showKeyInput = $state(false);
let checkoutStatus = $state<'activating' | 'error' | 'idle'>('idle');
let checkoutError = $state('');
async function activateCheckout() {
if (!checkoutId) return;
checkoutStatus = 'activating';
checkoutError = '';
try {
await activateFromCheckout({ checkoutId });
goto('/dashboard');
} catch (e: any) {
checkoutError = e.message ?? 'Failed to activate from checkout';
checkoutStatus = 'error';
}
}
onMount(() => {
if (checkoutId) activateCheckout();
});
</script>
{#if checkoutId}
<!-- Auto-activate from Polar checkout -->
<div class="mx-auto max-w-md pt-20">
<div class="mb-8 text-center">
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-100">
<Icon icon="ph:spinner-duotone" class="h-6 w-6 animate-spin text-neutral-600" />
{#if checkoutStatus === 'activating'}
<div class="mb-8 text-center">
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-100">
<Icon icon="ph:spinner-duotone" class="h-6 w-6 animate-spin text-neutral-600" />
</div>
<h2 class="text-2xl font-bold">Activating your license...</h2>
<p class="mt-1 text-neutral-500">
We're setting up your account. This will only take a moment.
</p>
</div>
{:else if checkoutStatus === 'error'}
<div class="mb-8 text-center">
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-red-50">
<Icon icon="ph:warning-duotone" class="h-6 w-6 text-red-500" />
</div>
<h2 class="text-2xl font-bold">Activation failed</h2>
<p class="mt-1 text-neutral-500">{checkoutError}</p>
</div>
<h2 class="text-2xl font-bold">Activating your license...</h2>
<p class="mt-1 text-neutral-500">
We're setting up your account. This will only take a moment.
</p>
</div>
<form {...activateFromCheckout} class="space-y-4">
<input type="hidden" {...activateFromCheckout.fields.checkoutId.as('text')} value={checkoutId} />
<button
type="submit"
onclick={activateCheckout}
data-umami-event={LICENSE_ACTIVATE_CHECKOUT}
class="flex w-full items-center justify-center gap-2 rounded-xl bg-neutral-900 px-4 py-2.5 font-medium text-white hover:bg-neutral-800"
>
<Icon icon="ph:seal-check-duotone" class="h-4 w-4" />
Activate Now
<Icon icon="ph:arrow-clockwise-duotone" class="h-4 w-4" />
Retry
</button>
</form>
{/if}
<div class="mt-6 rounded-xl border border-neutral-200 bg-neutral-50 p-4">
<p class="text-center text-sm text-neutral-500">