feat(leads): implement Stripe payment component for exhibit licenses
Full implementation of ae_comp__exhibit_payment.svelte (was a 9-line stub).
Reads Stripe config from $ae_loc.site_cfg_json per-event. License tier
selector (1/3/6/10 users) uses {#key} remount pattern to work around
stripe-buy-button web component ignoring attribute changes after mount.
Three states: paid confirmation (priority=true), not-configured hint, payment
form. client_reference_id=exhibit_id ties payments to booth records.
TypeScript declaration for stripe-buy-button added to app.d.ts via
svelte/elements augmentation. exhibit_id prop wired in +page.svelte and
ae_tab__manage.svelte.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
12
src/app.d.ts
vendored
12
src/app.d.ts
vendored
@@ -22,3 +22,15 @@ declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var native_app: any;
|
||||
}
|
||||
|
||||
// Stripe Buy Button web component — needed so Svelte templates accept the element without TS errors.
|
||||
declare module 'svelte/elements' {
|
||||
interface IntrinsicElements {
|
||||
'stripe-buy-button': {
|
||||
'buy-button-id': string;
|
||||
'publishable-key': string;
|
||||
'client-reference-id'?: string;
|
||||
[attr: string]: any;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +478,7 @@ function toggle_manage_tab() {
|
||||
<Tab_add exhibit_id={page.params.exhibit_id ?? ''} />
|
||||
{:else if active_tab === 'payment'}
|
||||
<div class="mx-auto w-full max-w-4xl">
|
||||
<Comp_exhibit_payment />
|
||||
<Comp_exhibit_payment exhibit_id={page.params.exhibit_id ?? ''} />
|
||||
</div>
|
||||
{:else if active_tab === 'list'}
|
||||
<div class="flex w-full flex-col space-y-6">
|
||||
|
||||
@@ -1,11 +1,183 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_payment.svelte
|
||||
* Leads Payment Stub.
|
||||
* ae_comp__exhibit_payment.svelte
|
||||
* Leads Payment — Stripe Buy Button integration.
|
||||
*
|
||||
* Stripe config is read from $ae_loc.site_cfg_json (set per-event by admin in Site Settings):
|
||||
* stripe_publishable_key — Stripe publishable key (pk_live_... or pk_test_...)
|
||||
* stripe_btn_1_license — Stripe Buy Button ID for the 1-user license tier
|
||||
* stripe_btn_3_license — Buy Button ID for 3-user tier
|
||||
* stripe_btn_6_license — Buy Button ID for 6-user tier
|
||||
* stripe_btn_10_license — Buy Button ID for 10-user tier
|
||||
*
|
||||
* client_reference_id = exhibit_id — ties each Stripe payment back to this booth record.
|
||||
* Payment status (priority flag) is read live from Dexie (IDB).
|
||||
*
|
||||
* WHY {#key btn_payment_id}: stripe-buy-button is a web component that ignores attribute
|
||||
* changes after its initial mount. Keying on btn_payment_id forces Svelte to fully
|
||||
* remount the element whenever the selected license tier changes.
|
||||
*/
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db_events } from '$lib/ae_events/db_events';
|
||||
import { ae_loc } from '$lib/stores/ae_stores';
|
||||
import { AlertTriangle, CheckCircle, CreditCard } from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
exhibit_id: string;
|
||||
}
|
||||
let { exhibit_id }: Props = $props();
|
||||
|
||||
const lq__exhibit_obj = liveQuery(() => {
|
||||
if (!exhibit_id) return undefined;
|
||||
return db_events.exhibit.get(exhibit_id);
|
||||
});
|
||||
|
||||
// Stripe config from site_cfg_json — set per-event by admin in Site Settings.
|
||||
const stripe_publishable_key = $derived(
|
||||
($ae_loc.site_cfg_json?.stripe_publishable_key as string | undefined) ?? null
|
||||
);
|
||||
const stripe_btn_ids = $derived({
|
||||
1: ($ae_loc.site_cfg_json?.stripe_btn_1_license as string | undefined) ?? null,
|
||||
3: ($ae_loc.site_cfg_json?.stripe_btn_3_license as string | undefined) ?? null,
|
||||
6: ($ae_loc.site_cfg_json?.stripe_btn_6_license as string | undefined) ?? null,
|
||||
10: ($ae_loc.site_cfg_json?.stripe_btn_10_license as string | undefined) ?? null,
|
||||
});
|
||||
|
||||
const license_tiers = [
|
||||
{ qty: 1, label: '1 user license', price: '$120' },
|
||||
{ qty: 3, label: 'Up to 3 user licenses', price: '$330' },
|
||||
{ qty: 6, label: 'Up to 6 user licenses', price: '$660' },
|
||||
{ qty: 10, label: 'Up to 10 user licenses', price: '$1,100' },
|
||||
] as const;
|
||||
|
||||
type LicenseQty = 1 | 3 | 6 | 10;
|
||||
let selected_qty: LicenseQty = $state(1);
|
||||
|
||||
const btn_payment_id = $derived(stripe_btn_ids[selected_qty] ?? null);
|
||||
const is_stripe_configured = $derived(!!stripe_publishable_key);
|
||||
|
||||
// Inject the Stripe Buy Button JS once per session (idempotent — skips if already present).
|
||||
// document.head is outside Svelte's managed DOM tree so this is safe.
|
||||
$effect(() => {
|
||||
if (!is_stripe_configured) return;
|
||||
if (document.querySelector('script[src*="stripe.com/v3/buy-button"]')) return;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://js.stripe.com/v3/buy-button.js';
|
||||
script.async = true;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="exhibit-payment card p-4">
|
||||
<h3 class="h3">Payment & Licensing</h3>
|
||||
<p>Placeholder for Stripe integration.</p>
|
||||
<div class="exhibit-payment space-y-6">
|
||||
{#if $lq__exhibit_obj?.priority}
|
||||
<!-- Paid Confirmation — shown when admin has marked this booth as paid (priority = true) -->
|
||||
<div class="card preset-tonal-success border-success-500/30 border p-6">
|
||||
<div class="mb-3 flex items-center gap-3">
|
||||
<CheckCircle size="1.5em" class="text-success-500" />
|
||||
<h4 class="text-success-500 text-lg font-bold">Marked as Paid</h4>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
Thank you for your payment. You have purchased
|
||||
<strong>{$lq__exhibit_obj?.license_max ?? 0}</strong> user license(s)
|
||||
for lead retrieval at this event.
|
||||
</p>
|
||||
{#if ($lq__exhibit_obj?.leads_device_sm_qty ?? 0) > 0}
|
||||
<p class="mt-2 text-sm">
|
||||
Rental device(s): <strong>{$lq__exhibit_obj?.leads_device_sm_qty}</strong>.
|
||||
Pick them up at onsite registration.
|
||||
</p>
|
||||
{:else}
|
||||
<p class="mt-2 text-sm opacity-60">
|
||||
No rental devices. Use your own device(s) with this service.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else if !is_stripe_configured}
|
||||
<!-- Stripe not configured — show a setup hint only to admins -->
|
||||
{#if $ae_loc.administrator_access}
|
||||
<div
|
||||
class="card preset-tonal-warning border-warning-500/30 flex items-start gap-3 border p-4">
|
||||
<AlertTriangle size="1.2em" class="text-warning-500 mt-0.5 shrink-0" />
|
||||
<div class="text-sm">
|
||||
<p class="font-bold">Stripe not configured for this site.</p>
|
||||
<p class="mt-1 opacity-70">
|
||||
Add <code class="font-mono text-xs">stripe_publishable_key</code>,
|
||||
<code class="font-mono text-xs">stripe_btn_1_license</code>,
|
||||
<code class="font-mono text-xs">stripe_btn_3_license</code>,
|
||||
<code class="font-mono text-xs">stripe_btn_6_license</code>, and
|
||||
<code class="font-mono text-xs">stripe_btn_10_license</code>
|
||||
to <strong>Site Config JSON</strong> to enable payment.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="py-4 text-center text-sm opacity-50">
|
||||
Online payment is not available at this time. Please contact the event organizer.
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<!-- Payment Form — Stripe configured, booth not yet marked as paid -->
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<CreditCard size="1.2em" class="text-success-500" />
|
||||
<h4 class="text-lg font-bold">Purchase Licenses</h4>
|
||||
</div>
|
||||
|
||||
<div class="card preset-tonal-surface border-surface-500/10 border p-4 text-sm">
|
||||
<p>
|
||||
Each person from your booth who will scan attendee badges needs their own user
|
||||
license. You can use your own smartphone, tablet, or laptop — rental devices are
|
||||
not required.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- License Tier Selector -->
|
||||
<div class="space-y-1">
|
||||
<label class="label">
|
||||
<span class="text-xs font-black tracking-widest uppercase opacity-40"
|
||||
>Select License Tier</span>
|
||||
<select class="select mt-2 w-full" bind:value={selected_qty}>
|
||||
{#each license_tiers as tier (tier.qty)}
|
||||
<option value={tier.qty}>{tier.label} — {tier.price}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<p class="text-xs opacity-50">One license per team member who will scan badges.</p>
|
||||
</div>
|
||||
|
||||
<!-- Stripe Buy Button -->
|
||||
<div class="flex flex-col items-center gap-3">
|
||||
{#if stripe_publishable_key && btn_payment_id}
|
||||
<!-- {#key} forces full remount when tier changes — stripe-buy-button ignores
|
||||
attribute updates after initial render. See component comment above. -->
|
||||
{#key btn_payment_id}
|
||||
<stripe-buy-button
|
||||
buy-button-id={btn_payment_id}
|
||||
publishable-key={stripe_publishable_key}
|
||||
client-reference-id={exhibit_id}>
|
||||
</stripe-buy-button>
|
||||
{/key}
|
||||
<p class="text-center text-xs opacity-40">
|
||||
Payment processed securely via Stripe. Verify quantities on the checkout
|
||||
page.
|
||||
</p>
|
||||
<div class="card preset-tonal-warning w-full p-3 text-xs">
|
||||
<strong>Note:</strong> Payment confirmation may take up to 2 business days
|
||||
to reflect in your account status. Contact
|
||||
<a
|
||||
href="mailto:exhibits@oneskyit.com"
|
||||
class="font-medium hover:underline">exhibits@oneskyit.com</a>
|
||||
with questions.
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm opacity-50">
|
||||
No payment button is configured for this tier. Contact the event organizer.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -428,7 +428,7 @@ function handle_signout() {
|
||||
{#if show_billing}
|
||||
<div
|
||||
class="bg-surface-500/5 border-surface-500/10 animate-in fade-in slide-in-from-top-2 border-t p-4">
|
||||
<Comp_exhibit_payment />
|
||||
<Comp_exhibit_payment {exhibit_id} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user