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:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user