diff --git a/documentation/MODULE__AE_Events_Exhibitor_Leads.md b/documentation/MODULE__AE_Events_Exhibitor_Leads.md
index f8d81239..150dc3ad 100644
--- a/documentation/MODULE__AE_Events_Exhibitor_Leads.md
+++ b/documentation/MODULE__AE_Events_Exhibitor_Leads.md
@@ -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.
---
diff --git a/src/routes/events/[event_id]/(leads)/README.md b/src/routes/events/[event_id]/(leads)/README.md
index a54b1f7a..d94cbbba 100644
--- a/src/routes/events/[event_id]/(leads)/README.md
+++ b/src/routes/events/[event_id]/(leads)/README.md
@@ -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
diff --git a/src/routes/events/[event_id]/(leads)/leads/config/+page.svelte b/src/routes/events/[event_id]/(leads)/leads/config/+page.svelte
new file mode 100644
index 00000000..7ecb840b
--- /dev/null
+++ b/src/routes/events/[event_id]/(leads)/leads/config/+page.svelte
@@ -0,0 +1,317 @@
+
+
+
+ Leads Config
+
+
+{#if !$ae_loc.administrator_access}
+
+ Changes here update event.mod_exhibits_json and take effect immediately
+ for all exhibitors at this event (no re-login required).
+
+
+ {#if !draft_initialized}
+
Loading event config...
+ {:else}
+
+
+
+
+
+
+ {#if sections.payment}
+
+
+
+
+
+ The Stripe payment component code is always present. This toggle only
+ controls visibility — no code changes needed when switching events.
+
+
+
+ {/if}
+
+
+
+
+
+
+
+ {#if sections.stripe}
+
+
+
+
+ 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).
+
+
+
+
+
+
+
+
+
+
+
+
+ {/if}
+
+
+
+
+
+
+
+ {/if}
+
+{/if}
diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/+page.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/+page.svelte
index 60b6f128..4a453527 100644
--- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/+page.svelte
+++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/+page.svelte
@@ -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}
-
- {#if $ae_loc.show_leads_payment}
+
+ {#if leads_require_payment}
{:else if active_tab === 'payment'}
-
+
{:else if active_tab === 'list'}
@@ -512,7 +538,7 @@ function toggle_manage_tab() {
{:else if active_tab === 'manage'}
-
+
{/if}
diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_payment.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_payment.svelte
index f457ba5a..2519264b 100644
--- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_payment.svelte
+++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_comp__exhibit_payment.svelte
@@ -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 = [
diff --git a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte
index d44d2056..3ee8e259 100644
--- a/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte
+++ b/src/routes/events/[event_id]/(leads)/leads/exhibit/[exhibit_id]/ae_tab__manage.svelte
@@ -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}
-
-