leads: event-level payment config + Stripe key migration
- New /events/[event_id]/leads/config page: administrator UI for mod_exhibits_json. Controls leads_require_payment toggle and Stripe keys (publishable key + buy button IDs per license tier). - leads_require_payment (mod_exhibits_json) now gates all billing UI: header CreditCard button in exhibit +page.svelte and Licenses & Billing accordion in ae_tab__manage.svelte. Default false (client covers costs). - Stripe keys migrated from site_cfg_json to mod_exhibits_json (per-event). ae_comp__exhibit_payment accepts them as optional props; falls back to site_cfg_json for events not yet migrated. - Fixed "My Leads" bug for shared-passcode users: search_params now maps licensee_email 'my' → 'shared_passcode' literal (not kv.key passcode string) so filters correctly match stored external_person_id values. - Event settings: Exhibits section replaced with config link + raw JSON fallback, matching pres_mgmt/badges pattern. - Docs updated: README.md, MODULE__AE_Events_Exhibitor_Leads.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -114,13 +114,12 @@ Exhibit configuration and app settings.
|
||||
- Sign out button
|
||||
|
||||
**Lead Retrieval Config**:
|
||||
- Exhibit Leads Licensees — manage staff accounts (`administrator_access` only; gap: should also allow shared-passcode users — see Known Gaps)
|
||||
- Exhibit Leads Licensees — manage staff accounts (`administrator_access` OR signed in via shared exhibit passcode)
|
||||
- Qualifiers & Questions — custom question config
|
||||
- Licenses & Billing — stub (Stripe not yet implemented)
|
||||
- Licenses & Billing — Stripe payment (only shown when `event.mod_exhibits_json.leads_require_payment = true`)
|
||||
|
||||
**App Settings**:
|
||||
- Auto-hide header/footer toggle
|
||||
- Show Payment Tab toggle
|
||||
- Show Extra Details toggle
|
||||
- Refresh interval (1–120 seconds, default 25s), countdown timer, last-refresh timestamp
|
||||
- Reload App, Clear IDB, Hard Reset (clears localStorage)
|
||||
@@ -239,17 +238,27 @@ Leads are never hard-deleted. "Remove Lead" sets `enable = false`. Key behaviors
|
||||
|
||||
## Known Gaps
|
||||
|
||||
None currently. See TODO__Agents.md for remaining smoke test items.
|
||||
|
||||
## Implemented (previously listed as gaps)
|
||||
|
||||
### Payment / Stripe
|
||||
`ae_comp__exhibit_payment.svelte` is a stub. The Stripe integration is not implemented.
|
||||
The payment tab can be hidden via "Show Payment Tab" in App Settings.
|
||||
`ae_comp__exhibit_payment.svelte` is fully implemented. Three states: paid (`priority=true` green
|
||||
confirmation card), Stripe not configured (admin hint), payment form with license tier selector.
|
||||
Visibility is event-wide: set `event.mod_exhibits_json.leads_require_payment = true` in the event
|
||||
settings JSON to enable. When `false` (default), both the header CreditCard button and the
|
||||
"Licenses & Billing" accordion in the Manage tab are hidden. The Stripe component itself is
|
||||
unchanged — gating is done in `+page.svelte` and `ae_tab__manage.svelte`.
|
||||
|
||||
### License Management — Shared Passcode Access
|
||||
Implemented. The license section in the Manage tab is visible to Aether admins and to anyone
|
||||
signed in via the shared exhibit passcode (`auth_exhibit_kv[exhibit_id].type === 'shared'`).
|
||||
Guard in [ae_tab__manage.svelte](src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte):
|
||||
```svelte
|
||||
{#if $ae_loc.administrator_access || $events_loc.leads.auth_exhibit_kv?.[exhibit_id]?.type === 'shared'}
|
||||
```
|
||||
|
||||
### "My Leads" filter for shared-passcode users
|
||||
Fixed. `external_person_id` is stored as the literal `'shared_passcode'` for shared users (not
|
||||
the raw passcode string). The `search_params` derived in `+page.svelte` now checks `kv.type ===
|
||||
'shared'` and resolves to `'shared_passcode'` instead of `kv.key`, so the "My Leads" filter
|
||||
correctly returns their captured records.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ All data is cached in IndexedDB (Dexie.js) for offline use, with background API
|
||||
| `ae_comp__exhibit_tracking_obj_li.svelte` | Lead list item renderer |
|
||||
| `ae_comp__exhibit_license_list.svelte` | License slot manager (admin) |
|
||||
| `ae_comp__exhibit_custom_questions.svelte` | Custom question config editor (admin) |
|
||||
| `ae_comp__exhibit_payment.svelte` | **STUB** — Stripe placeholder, not functional |
|
||||
| `ae_comp__exhibit_payment.svelte` | Stripe payment component (fully implemented) |
|
||||
| `ae_comp__exhibit_search.svelte` | Exhibit search input on the landing page |
|
||||
|
||||
### Lead detail components (within `lead/[exhibit_tracking_id]/`)
|
||||
@@ -114,11 +114,16 @@ Two scan modes (toggled per exhibit):
|
||||
|
||||
## Known Gaps / Not Yet Implemented
|
||||
|
||||
- **Payment / Stripe** — `ae_comp__exhibit_payment.svelte` is a stub. The payment tab can be
|
||||
hidden via the "Show Payment Tab" toggle in the Manage tab's App Settings.
|
||||
- None currently. See `TODO__Agents.md` for the remaining smoke test checklist.
|
||||
|
||||
## Implemented (previously listed as gaps)
|
||||
|
||||
- **Payment / Stripe** — `ae_comp__exhibit_payment.svelte` is fully implemented. Three states:
|
||||
paid (`priority=true` green card), Stripe not configured (admin hint), payment form.
|
||||
Visibility is controlled event-wide by `event.mod_exhibits_json.leads_require_payment`.
|
||||
Set to `true` in event settings JSON to enable Stripe for an event; default `false` hides
|
||||
all billing UI (header CreditCard button + Manage tab "Licenses & Billing" accordion).
|
||||
|
||||
- **`allow_tracking` gate** — enforced in both `ae_comp__lead_qr_scanner.svelte` and
|
||||
`ae_comp__lead_manual_search.svelte`. Badges without `allow_tracking = true` are blocked.
|
||||
- **Show/hide hidden records** — toggle in `ae_comp__exhibit_tracking_search.svelte`; filters
|
||||
|
||||
317
src/routes/events/[event_id]/(leads)/leads/config/+page.svelte
Normal file
317
src/routes/events/[event_id]/(leads)/leads/config/+page.svelte
Normal file
@@ -0,0 +1,317 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Leads / Exhibits Config Page
|
||||
* Route: /events/[event_id]/leads/config
|
||||
*
|
||||
* Admin UI for managing event.mod_exhibits_json (ExhibitsRemoteCfg).
|
||||
* Access: administrator_access only.
|
||||
*
|
||||
* Controls:
|
||||
* - leads_require_payment — show/hide Stripe billing UI event-wide
|
||||
* - Stripe keys — publishable key + buy button IDs per license tier
|
||||
* (previously lived in site_cfg_json; moved here so they
|
||||
* are per-event rather than per-site)
|
||||
*
|
||||
* Save pattern: load → merge draft → PATCH event.mod_exhibits_json via V3 API.
|
||||
*/
|
||||
import { untrack } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { db_events } from '$lib/ae_events/db_events';
|
||||
import { ae_loc, ae_api } from '$lib/stores/ae_stores';
|
||||
import { events_func } from '$lib/ae_events/ae_events_functions';
|
||||
import { api } from '$lib/api/api';
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
CreditCard,
|
||||
Info,
|
||||
Lock,
|
||||
Save,
|
||||
Settings
|
||||
} from '@lucide/svelte';
|
||||
|
||||
interface ExhibitsRemoteCfg {
|
||||
/** When true, Stripe billing UI (header CreditCard button + Manage tab accordion) is visible. */
|
||||
leads_require_payment: boolean;
|
||||
/** Stripe publishable key (pk_live_... or pk_test_...). */
|
||||
stripe_publishable_key: string | null;
|
||||
/** Stripe Buy Button IDs per license tier. */
|
||||
stripe_btn_1_license: string | null;
|
||||
stripe_btn_3_license: string | null;
|
||||
stripe_btn_6_license: string | null;
|
||||
stripe_btn_10_license: string | null;
|
||||
}
|
||||
|
||||
let event_id = $derived(page.params.event_id ?? '');
|
||||
|
||||
let lq__event_obj = $derived(
|
||||
liveQuery(async () => {
|
||||
if (!event_id) return null;
|
||||
return await db_events.event.get(event_id);
|
||||
})
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Draft state — initialized from the live event config
|
||||
// ---------------------------------------------------------------------------
|
||||
const cfg_defaults: ExhibitsRemoteCfg = {
|
||||
leads_require_payment: false,
|
||||
stripe_publishable_key: null,
|
||||
stripe_btn_1_license: null,
|
||||
stripe_btn_3_license: null,
|
||||
stripe_btn_6_license: null,
|
||||
stripe_btn_10_license: null
|
||||
};
|
||||
|
||||
let draft: ExhibitsRemoteCfg = $state({ ...cfg_defaults });
|
||||
let draft_initialized = $state(false);
|
||||
let initial_json = $state('');
|
||||
|
||||
$effect(() => {
|
||||
const event_obj = $lq__event_obj;
|
||||
if (event_obj && !draft_initialized) {
|
||||
untrack(() => {
|
||||
const saved = (event_obj.mod_exhibits_json ?? {}) as Partial<ExhibitsRemoteCfg>;
|
||||
draft = { ...cfg_defaults, ...saved };
|
||||
initial_json = JSON.stringify(draft);
|
||||
draft_initialized = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let is_dirty = $derived(draft_initialized && JSON.stringify(draft) !== initial_json);
|
||||
|
||||
// Section collapse state
|
||||
let sections: Record<string, boolean> = $state({
|
||||
payment: true,
|
||||
stripe: true
|
||||
});
|
||||
function toggle(key: string) {
|
||||
sections[key] = !sections[key];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Save
|
||||
// ---------------------------------------------------------------------------
|
||||
let save_status: 'idle' | 'saving' | 'success' | 'error' = $state('idle');
|
||||
|
||||
async function save() {
|
||||
if (!event_id) return;
|
||||
save_status = 'saving';
|
||||
try {
|
||||
// Preserve any unknown keys already in mod_exhibits_json (future-proofing).
|
||||
const current_cfg = ($lq__event_obj?.mod_exhibits_json ?? {}) as any;
|
||||
const new_cfg = { ...current_cfg, ...draft };
|
||||
|
||||
await api.update_ae_obj({
|
||||
api_cfg: $ae_api,
|
||||
obj_type: 'event',
|
||||
obj_id: event_id,
|
||||
fields: { mod_exhibits_json: new_cfg },
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
// Reload event so consumers pick up the new config immediately.
|
||||
await events_func.load_ae_obj_id__event({
|
||||
api_cfg: $ae_api,
|
||||
event_id: event_id,
|
||||
log_lvl: 1
|
||||
});
|
||||
|
||||
initial_json = JSON.stringify(draft);
|
||||
save_status = 'success';
|
||||
setTimeout(() => (save_status = 'idle'), 3000);
|
||||
} catch (e) {
|
||||
console.error('Failed to save exhibits config', e);
|
||||
save_status = 'error';
|
||||
setTimeout(() => (save_status = 'idle'), 5000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Leads Config</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if !$ae_loc.administrator_access}
|
||||
<div class="p-8 text-center opacity-50">
|
||||
<Lock size="3em" class="mx-auto mb-2" />
|
||||
<p>Administrator access required.</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mx-auto w-full max-w-2xl space-y-4 px-2 py-4">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="/events/{event_id}/leads"
|
||||
class="btn btn-sm preset-tonal-surface"
|
||||
title="Back to Leads">
|
||||
<ArrowLeft size="1em" />
|
||||
</a>
|
||||
<Settings size="1.2em" class="text-primary-500" />
|
||||
<h1 class="text-xl font-bold">Leads Config</h1>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if save_status === 'success'}
|
||||
<span class="badge preset-tonal-success flex items-center gap-1">
|
||||
<Check size="1em" /> Saved
|
||||
</span>
|
||||
{:else if save_status === 'error'}
|
||||
<span class="badge preset-tonal-error flex items-center gap-1">
|
||||
<AlertTriangle size="1em" /> Error saving
|
||||
</span>
|
||||
{/if}
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-filled-primary-500"
|
||||
onclick={save}
|
||||
disabled={!is_dirty || save_status === 'saving'}>
|
||||
<Save size="1em" class="mr-1" />
|
||||
{save_status === 'saving' ? 'Saving...' : 'Save'}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="text-surface-500 text-sm">
|
||||
Changes here update <code>event.mod_exhibits_json</code> and take effect immediately
|
||||
for all exhibitors at this event (no re-login required).
|
||||
</p>
|
||||
|
||||
{#if !draft_initialized}
|
||||
<p class="text-surface-400 italic">Loading event config...</p>
|
||||
{:else}
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- PAYMENT -->
|
||||
<!-- ================================================================ -->
|
||||
<section class="border-surface-200-800 rounded-xl border">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
|
||||
onclick={() => toggle('payment')}>
|
||||
<span class="flex items-center gap-2">
|
||||
<CreditCard size="1em" class="text-success-500" />
|
||||
Payment
|
||||
</span>
|
||||
{#if sections.payment}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
|
||||
</button>
|
||||
{#if sections.payment}
|
||||
<div class="border-surface-200-800 space-y-3 border-t px-4 py-4">
|
||||
<label class="flex items-start gap-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox mt-0.5"
|
||||
bind:checked={draft.leads_require_payment} />
|
||||
<span>
|
||||
<span class="font-semibold">Require Payment (Stripe)</span>
|
||||
<span class="text-surface-500 ml-2 text-sm">
|
||||
Shows the Stripe billing UI to exhibitors — the header CreditCard
|
||||
button and the "Licenses & Billing" accordion in the Manage tab.
|
||||
Leave <strong>off</strong> when the client is covering costs.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="flex items-start gap-2 rounded-lg bg-surface-500/5 p-3 text-xs opacity-60">
|
||||
<Info size="1em" class="mt-0.5 shrink-0" />
|
||||
<span>
|
||||
The Stripe payment component code is always present. This toggle only
|
||||
controls visibility — no code changes needed when switching events.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- STRIPE CONFIG -->
|
||||
<!-- ================================================================ -->
|
||||
<section class="border-surface-200-800 rounded-xl border">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between px-4 py-3 text-left font-semibold"
|
||||
onclick={() => toggle('stripe')}>
|
||||
<span>
|
||||
Stripe Keys
|
||||
<span class="text-surface-500 text-xs font-normal ml-1">(per-event)</span>
|
||||
</span>
|
||||
{#if sections.stripe}<ChevronUp size="1em" />{:else}<ChevronDown size="1em" />{/if}
|
||||
</button>
|
||||
{#if sections.stripe}
|
||||
<div class="border-surface-200-800 space-y-4 border-t px-4 py-4">
|
||||
<div class="flex items-start gap-2 text-xs opacity-60">
|
||||
<Info size="1em" class="mt-0.5 shrink-0" />
|
||||
<span>
|
||||
These keys apply only to this event's Exhibitor Leads module.
|
||||
Find them in the Stripe Dashboard → Products → Buy Buttons.
|
||||
The Buy Button IDs are per license tier (1, 3, 6, or 10 users).
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Publishable Key</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm font-mono text-xs"
|
||||
placeholder="pk_live_... or pk_test_..."
|
||||
bind:value={draft.stripe_publishable_key} />
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Buy Button — 1 License</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm font-mono text-xs"
|
||||
placeholder="buy_btn_..."
|
||||
bind:value={draft.stripe_btn_1_license} />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Buy Button — 3 Licenses</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm font-mono text-xs"
|
||||
placeholder="buy_btn_..."
|
||||
bind:value={draft.stripe_btn_3_license} />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Buy Button — 6 Licenses</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm font-mono text-xs"
|
||||
placeholder="buy_btn_..."
|
||||
bind:value={draft.stripe_btn_6_license} />
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium">Buy Button — 10 Licenses</span>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-sm font-mono text-xs"
|
||||
placeholder="buy_btn_..."
|
||||
bind:value={draft.stripe_btn_10_license} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<!-- Bottom save -->
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-filled-primary-500"
|
||||
onclick={save}
|
||||
disabled={!is_dirty || save_status === 'saving'}>
|
||||
<Save size="1em" class="mr-1" />
|
||||
{save_status === 'saving' ? 'Saving...' : 'Save Config'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -134,15 +134,41 @@ const lq__exhibit_obj = liveQuery(() => {
|
||||
return db_events.exhibit.get(exhibit_id);
|
||||
});
|
||||
|
||||
// Event-level config — read mod_exhibits_json for payment visibility + Stripe keys.
|
||||
// Configured via /events/[event_id]/leads/config (administrator only).
|
||||
const lq__event_obj = liveQuery(() => {
|
||||
const event_id = page.params.event_id;
|
||||
if (!event_id) return undefined;
|
||||
return db_events.event.get(event_id);
|
||||
});
|
||||
|
||||
// When false (default), all billing UI is hidden — set true when event uses Stripe payment.
|
||||
let leads_require_payment = $derived(
|
||||
($lq__event_obj?.mod_exhibits_json as any)?.leads_require_payment === true
|
||||
);
|
||||
|
||||
// Stripe keys from mod_exhibits_json — passed to the payment component.
|
||||
// The component falls back to site_cfg_json if these are null (legacy migration path).
|
||||
let stripe_cfg = $derived({
|
||||
stripe_publishable_key: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_publishable_key ?? null,
|
||||
stripe_btn_1_license: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_btn_1_license ?? null,
|
||||
stripe_btn_3_license: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_btn_3_license ?? null,
|
||||
stripe_btn_6_license: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_btn_6_license ?? null,
|
||||
stripe_btn_10_license: ($lq__event_obj?.mod_exhibits_json as any)?.stripe_btn_10_license ?? null,
|
||||
});
|
||||
|
||||
// Standardized Reactive Search Pattern
|
||||
let search_params = $derived.by(() => {
|
||||
let licensee_email = $events_loc.leads.tracking__qry__licensee_email;
|
||||
|
||||
// Resolve "My Leads" to actual email
|
||||
// Resolve "My Leads" to the correct identity used when storing leads.
|
||||
// Shared-passcode users store 'shared_passcode' literal (not the passcode string itself).
|
||||
// Licensed users store their email. Aether bypass users store $ae_loc.access_type.
|
||||
if (licensee_email === 'my') {
|
||||
licensee_email =
|
||||
$events_loc.leads.auth_exhibit_kv?.[page.params.exhibit_id ?? '']
|
||||
?.key || 'all';
|
||||
const kv = $events_loc.leads.auth_exhibit_kv?.[page.params.exhibit_id ?? ''];
|
||||
licensee_email = kv?.type === 'shared'
|
||||
? 'shared_passcode'
|
||||
: kv?.key || 'all';
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -440,8 +466,8 @@ function toggle_manage_tab() {
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<!-- Payment (Conditional) -->
|
||||
{#if $ae_loc.show_leads_payment}
|
||||
<!-- Payment (Conditional) — visible only when event-level leads_require_payment is true -->
|
||||
{#if leads_require_payment}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm px-2 transition-colors sm:px-3"
|
||||
@@ -478,7 +504,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 exhibit_id={page.params.exhibit_id ?? ''} />
|
||||
<Comp_exhibit_payment exhibit_id={page.params.exhibit_id ?? ''} {...stripe_cfg} />
|
||||
</div>
|
||||
{:else if active_tab === 'list'}
|
||||
<div class="flex w-full flex-col space-y-6">
|
||||
@@ -512,7 +538,7 @@ function toggle_manage_tab() {
|
||||
</div>
|
||||
{:else if active_tab === 'manage'}
|
||||
<div class="mx-auto w-full max-w-4xl">
|
||||
<Tab_manage />
|
||||
<Tab_manage {leads_require_payment} {stripe_cfg} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* 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
|
||||
* Stripe config priority (first wins):
|
||||
* 1. Props passed from the parent (sourced from event.mod_exhibits_json — preferred)
|
||||
* 2. $ae_loc.site_cfg_json fallback (legacy — for events not yet migrated to mod_exhibits_json)
|
||||
*
|
||||
* Props are set in the exhibit +page.svelte from lq__event_obj.mod_exhibits_json and passed
|
||||
* down so this component doesn't need to know the event_id or do its own Dexie query.
|
||||
*
|
||||
* client_reference_id = exhibit_id — ties each Stripe payment back to this booth record.
|
||||
* Payment status (priority flag) is read live from Dexie (IDB).
|
||||
@@ -24,23 +24,38 @@ import { AlertTriangle, CheckCircle, CreditCard } from '@lucide/svelte';
|
||||
|
||||
interface Props {
|
||||
exhibit_id: string;
|
||||
/** From event.mod_exhibits_json — set in the Leads Config page. Falls back to site_cfg_json. */
|
||||
stripe_publishable_key?: string | null;
|
||||
stripe_btn_1_license?: string | null;
|
||||
stripe_btn_3_license?: string | null;
|
||||
stripe_btn_6_license?: string | null;
|
||||
stripe_btn_10_license?: string | null;
|
||||
}
|
||||
let { exhibit_id }: Props = $props();
|
||||
let {
|
||||
exhibit_id,
|
||||
stripe_publishable_key: prop_stripe_key = null,
|
||||
stripe_btn_1_license: prop_btn_1 = null,
|
||||
stripe_btn_3_license: prop_btn_3 = null,
|
||||
stripe_btn_6_license: prop_btn_6 = null,
|
||||
stripe_btn_10_license: prop_btn_10 = null
|
||||
}: 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.
|
||||
// Stripe config: prefer event-level props (mod_exhibits_json); fall back to site_cfg_json
|
||||
// for events that haven't been migrated to the per-event config page yet.
|
||||
const stripe_publishable_key = $derived(
|
||||
prop_stripe_key ??
|
||||
($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,
|
||||
1: prop_btn_1 ?? ($ae_loc.site_cfg_json?.stripe_btn_1_license as string | undefined) ?? null,
|
||||
3: prop_btn_3 ?? ($ae_loc.site_cfg_json?.stripe_btn_3_license as string | undefined) ?? null,
|
||||
6: prop_btn_6 ?? ($ae_loc.site_cfg_json?.stripe_btn_6_license as string | undefined) ?? null,
|
||||
10: prop_btn_10 ?? ($ae_loc.site_cfg_json?.stripe_btn_10_license as string | undefined) ?? null,
|
||||
});
|
||||
|
||||
const license_tiers = [
|
||||
|
||||
@@ -30,6 +30,23 @@ import {
|
||||
UserX,
|
||||
Users
|
||||
} from '@lucide/svelte';
|
||||
interface StripeCfg {
|
||||
stripe_publishable_key?: string | null;
|
||||
stripe_btn_1_license?: string | null;
|
||||
stripe_btn_3_license?: string | null;
|
||||
stripe_btn_6_license?: string | null;
|
||||
stripe_btn_10_license?: string | null;
|
||||
}
|
||||
|
||||
// leads_require_payment: event-level flag from mod_exhibits_json.
|
||||
// When false (default), all billing UI is hidden — client is covering costs.
|
||||
// Set mod_exhibits_json.leads_require_payment = true in event settings to enable Stripe.
|
||||
// stripe_cfg: Stripe keys from mod_exhibits_json, passed through to the payment component.
|
||||
let {
|
||||
leads_require_payment = false,
|
||||
stripe_cfg = {}
|
||||
}: { leads_require_payment?: boolean; stripe_cfg?: StripeCfg } = $props();
|
||||
|
||||
const exhibit_id = $derived(page.params.exhibit_id ?? '');
|
||||
|
||||
let lq__exhibit_obj = $derived(
|
||||
@@ -397,41 +414,43 @@ function handle_signout() {
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Billing -->
|
||||
<div class="p-0">
|
||||
<button
|
||||
class="hover:bg-surface-500/5 group flex w-full items-center justify-between p-4 transition-colors"
|
||||
onclick={() => (show_billing = !show_billing)}>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="bg-success-500/10 text-success-500 rounded-lg p-2">
|
||||
<CreditCard size="1.2em" />
|
||||
</div>
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-bold">
|
||||
Licenses & Billing
|
||||
<!-- Billing — only shown when event-level leads_require_payment is enabled -->
|
||||
{#if leads_require_payment}
|
||||
<div class="p-0">
|
||||
<button
|
||||
class="hover:bg-surface-500/5 group flex w-full items-center justify-between p-4 transition-colors"
|
||||
onclick={() => (show_billing = !show_billing)}>
|
||||
<div class="flex items-center gap-4">
|
||||
<div
|
||||
class="bg-success-500/10 text-success-500 rounded-lg p-2">
|
||||
<CreditCard size="1.2em" />
|
||||
</div>
|
||||
<div class="text-xs opacity-50">
|
||||
Review licenses and manage payment
|
||||
<div class="text-left">
|
||||
<div class="text-sm font-bold">
|
||||
Licenses & Billing
|
||||
</div>
|
||||
<div class="text-xs opacity-50">
|
||||
Review licenses and manage payment
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if show_billing}
|
||||
<ChevronDown size="1.2em" class="opacity-20" />
|
||||
{:else}
|
||||
<ChevronRight
|
||||
size="1.2em"
|
||||
class="opacity-20 transition-transform group-hover:translate-x-1" />
|
||||
{/if}
|
||||
</button>
|
||||
{#if show_billing}
|
||||
<ChevronDown size="1.2em" class="opacity-20" />
|
||||
{:else}
|
||||
<ChevronRight
|
||||
size="1.2em"
|
||||
class="opacity-20 transition-transform group-hover:translate-x-1" />
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#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 {exhibit_id} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#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 {exhibit_id} {...stripe_cfg} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -462,14 +481,6 @@ function handle_signout() {
|
||||
class="checkbox"
|
||||
bind:checked={$ae_loc.auto_hide_nav} />
|
||||
</label>
|
||||
<label
|
||||
class="hover:bg-surface-500/10 flex cursor-pointer items-center justify-between rounded-lg p-2 transition-colors">
|
||||
<span class="text-sm">Show Payment Tab</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
bind:checked={$ae_loc.show_leads_payment} />
|
||||
</label>
|
||||
<label
|
||||
class="hover:bg-surface-500/10 flex cursor-pointer items-center justify-between rounded-lg p-2 transition-colors">
|
||||
<span class="text-sm">Show Extra Details</span>
|
||||
|
||||
@@ -383,24 +383,38 @@ async function handle_save(field_name: string, data: any) {
|
||||
</details>
|
||||
|
||||
<details class="details">
|
||||
<summary class="summary">Exhibits (mod_exhibits_json)</summary>
|
||||
<div class="p-4">
|
||||
<AE_Comp_Editor_CodeMirror
|
||||
readonly={false}
|
||||
content={tmp_exhibits_json_str}
|
||||
bind:new_content={tmp_exhibits_json_str}
|
||||
show_line_numbers={true}
|
||||
placeholder="JSON config"
|
||||
class_li="p-1 preset-outlined-success-400-600 shadow-lg rounded-lg" />
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-tonal-primary"
|
||||
onclick={() => {
|
||||
handle_save(
|
||||
'mod_exhibits_json',
|
||||
tmp_exhibits_json_str
|
||||
);
|
||||
}}>Save</button>
|
||||
<summary class="summary">Exhibits / Leads (mod_exhibits_json)</summary>
|
||||
<div class="p-4 space-y-3">
|
||||
<p class="text-sm text-surface-500">
|
||||
Configure Leads payment visibility and Stripe keys for this event.
|
||||
</p>
|
||||
<a
|
||||
href="/events/{event_id}/leads/config"
|
||||
class="btn preset-tonal-primary">
|
||||
Go to Leads Config →
|
||||
</a>
|
||||
<!-- Raw JSON fallback for debugging / emergency edits -->
|
||||
<details class="mt-2">
|
||||
<summary class="cursor-pointer text-xs text-surface-400">Raw JSON (advanced)</summary>
|
||||
<div class="mt-2 space-y-2">
|
||||
<AE_Comp_Editor_CodeMirror
|
||||
readonly={false}
|
||||
content={tmp_exhibits_json_str}
|
||||
bind:new_content={tmp_exhibits_json_str}
|
||||
show_line_numbers={true}
|
||||
placeholder="JSON config"
|
||||
class_li="p-1 preset-outlined-success-400-600 shadow-lg rounded-lg" />
|
||||
<button
|
||||
type="button"
|
||||
class="btn preset-tonal-primary"
|
||||
onclick={() => {
|
||||
handle_save(
|
||||
'mod_exhibits_json',
|
||||
tmp_exhibits_json_str
|
||||
);
|
||||
}}>Save Raw JSON</button>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user