fix: self-hosted useSend support + purchase-first activate UX

- Pass USESEND_BASE_URL to UseSend SDK for self-hosted instances
- Add debug logging for email sends
- Redesign activate page: prominent Purchase Now button, collapsible license key input
This commit is contained in:
Sanju Sivalingam
2026-02-18 23:08:10 +05:30
parent 9b2ca21d28
commit e9d1c863e1
3 changed files with 67 additions and 40 deletions

View File

@@ -8,6 +8,7 @@ import * as schema from './db/schema';
import { sendEmail } from './email'; import { sendEmail } from './email';
export const auth = betterAuth({ export const auth = betterAuth({
baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:5173',
database: drizzleAdapter(db, { database: drizzleAdapter(db, {
provider: 'pg', provider: 'pg',
schema schema
@@ -15,14 +16,16 @@ export const auth = betterAuth({
plugins: [sveltekitCookies(getRequestEvent), apiKey()], plugins: [sveltekitCookies(getRequestEvent), apiKey()],
emailVerification: { emailVerification: {
sendVerificationEmail: async ({ user, url }) => { sendVerificationEmail: async ({ user, url }) => {
console.log('[Email] sendVerificationEmail called for:', user.email, 'url:', url);
try { try {
await sendEmail({ const result = await sendEmail({
to: user.email, to: user.email,
subject: 'Verify your DroidClaw email', subject: 'Verify your DroidClaw email',
text: `Hi ${user.name || 'there'},\n\nClick the link below to verify your email:\n\n${url}\n\nThis link expires in 1 hour.\n\n-- DroidClaw` text: `Hi ${user.name || 'there'},\n\nClick the link below to verify your email:\n\n${url}\n\nThis link expires in 1 hour.\n\n-- DroidClaw`
}); });
console.log('[Email] sendEmail result:', JSON.stringify(result));
} catch (err) { } catch (err) {
console.error('Failed to send verification email:', err); console.error('[Email] Failed to send verification email:', err);
} }
}, },
sendOnSignUp: true, sendOnSignUp: true,

View File

@@ -1,12 +1,13 @@
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import { UseSend } from 'usesend-js'; import { UseSend } from 'usesend-js';
if (!env.USESEND_API_KEY) throw new Error('USESEND_API_KEY is not set');
const usesend = new UseSend(env.USESEND_API_KEY);
const EMAIL_FROM = 'noreply@app.droidclaw.ai'; const EMAIL_FROM = 'noreply@app.droidclaw.ai';
function getClient() {
if (!env.USESEND_API_KEY) throw new Error('USESEND_API_KEY is not set');
return new UseSend(env.USESEND_API_KEY, env.USESEND_BASE_URL);
}
export async function sendEmail({ export async function sendEmail({
to, to,
subject, subject,
@@ -16,7 +17,8 @@ export async function sendEmail({
subject: string; subject: string;
text: string; text: string;
}) { }) {
return usesend.emails.send({ console.log('[Email] API key prefix:', env.USESEND_API_KEY?.slice(0, 15) + '...');
return getClient().emails.send({
to, to,
from: EMAIL_FROM, from: EMAIL_FROM,
subject, subject,

View File

@@ -5,6 +5,8 @@
import { LICENSE_ACTIVATE_CHECKOUT, LICENSE_ACTIVATE_MANUAL, LICENSE_PURCHASE_CLICK } from '$lib/analytics/events'; import { LICENSE_ACTIVATE_CHECKOUT, LICENSE_ACTIVATE_MANUAL, LICENSE_PURCHASE_CLICK } from '$lib/analytics/events';
const checkoutId = page.url.searchParams.get('checkout_id'); const checkoutId = page.url.searchParams.get('checkout_id');
let showKeyInput = $state(false);
</script> </script>
{#if checkoutId} {#if checkoutId}
@@ -69,49 +71,69 @@
</form> </form>
</div> </div>
{:else} {:else}
<!-- Manual activation (no checkout ID) --> <!-- Purchase-first flow -->
<div class="mx-auto max-w-md pt-20"> <div class="mx-auto max-w-md pt-20">
<div class="mb-8 text-center"> <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"> <div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-neutral-900">
<Icon icon="ph:seal-check-duotone" class="h-6 w-6 text-neutral-600" /> <Icon icon="ph:robot-duotone" class="h-6 w-6 text-white" />
</div> </div>
<h2 class="text-2xl font-bold">Activate your license</h2> <h2 class="text-2xl font-bold">Get started with DroidClaw</h2>
<p class="mt-1 text-neutral-500"> <p class="mt-2 text-neutral-500">
Enter the license key you received after purchasing DroidClaw. Unlock AI-powered Android device control.
</p> </p>
</div> </div>
<form {...activateLicense} class="space-y-4"> <a
<label class="block"> href="https://sandbox-api.polar.sh/v1/checkout-links/polar_cl_5pGavRIJJhM8ge6p0UaeaadT2bCiqL04CYXgW3bwVac/redirect"
<span class="flex items-center gap-1.5 text-sm font-medium text-neutral-700"> data-umami-event={LICENSE_PURCHASE_CLICK}
<Icon icon="ph:key-duotone" class="h-4 w-4 text-neutral-400" /> class="flex w-full items-center justify-center gap-2 rounded-xl bg-neutral-900 px-4 py-3 text-base font-medium text-white hover:bg-neutral-800"
License Key >
</span> <Icon icon="ph:credit-card-duotone" class="h-5 w-5" />
<input Purchase Now
{...activateLicense.fields.key.as('text')} </a>
placeholder="DROIDCLAW-XXXX-XXXX-XXXX"
class="mt-1 block w-full rounded-xl border border-neutral-300 px-4 py-2.5 focus:border-neutral-900 focus:outline-none"
/>
{#each activateLicense.fields.key.issues() ?? [] as issue (issue.message)}
<p class="mt-1 text-sm text-red-600">{issue.message}</p>
{/each}
</label>
<div class="mt-10">
<button <button
type="submit" type="button"
data-umami-event={LICENSE_ACTIVATE_MANUAL} onclick={() => showKeyInput = !showKeyInput}
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" class="flex w-full items-center justify-center gap-1.5 text-sm text-neutral-400 hover:text-neutral-600"
> >
<Icon icon="ph:seal-check-duotone" class="h-4 w-4" /> Already have a license key?
Activate <Icon
icon="ph:caret-down"
class="h-3.5 w-3.5 transition-transform {showKeyInput ? 'rotate-180' : ''}"
/>
</button> </button>
</form>
<p class="mt-6 text-center text-sm text-neutral-400"> {#if showKeyInput}
Don't have a key? <div class="mt-4">
<a href="https://sandbox-api.polar.sh/v1/checkout-links/polar_cl_5pGavRIJJhM8ge6p0UaeaadT2bCiqL04CYXgW3bwVac/redirect" data-umami-event={LICENSE_PURCHASE_CLICK} class="font-medium text-neutral-700 underline hover:text-neutral-900"> <form {...activateLicense} class="space-y-4">
Purchase here <label class="block">
</a> <span class="flex items-center gap-1.5 text-sm font-medium text-neutral-700">
</p> <Icon icon="ph:key-duotone" class="h-4 w-4 text-neutral-400" />
License Key
</span>
<input
{...activateLicense.fields.key.as('text')}
placeholder="DROIDCLAW-XXXX-XXXX-XXXX"
class="mt-1 block w-full rounded-xl border border-neutral-300 px-4 py-2.5 focus:border-neutral-900 focus:outline-none"
/>
{#each activateLicense.fields.key.issues() ?? [] as issue (issue.message)}
<p class="mt-1 text-sm text-red-600">{issue.message}</p>
{/each}
</label>
<button
type="submit"
data-umami-event={LICENSE_ACTIVATE_MANUAL}
class="flex w-full items-center justify-center gap-2 rounded-xl border border-neutral-300 px-4 py-2.5 text-sm font-medium text-neutral-700 hover:bg-neutral-50"
>
<Icon icon="ph:seal-check-duotone" class="h-4 w-4" />
Activate
</button>
</form>
</div>
{/if}
</div>
</div> </div>
{/if} {/if}