@aforoai/storefront-widgets 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1185 @@
1
+ /**
2
+ * Local types used internally by the storefront-widgets SDK. Most public
3
+ * types come from `@aforoai/types/embed-studio` (e.g. EmbedKey,
4
+ * EmbedKeyEnvironment) — anything customer-facing belongs there.
5
+ *
6
+ * Types in this file MAY be re-exported from `src/index.ts` if they form
7
+ * part of the locked public surface; check `tests/sdk-contract.test.ts`.
8
+ */
9
+ /** Canonical 8 widget ids served by Prompts 4–7. */
10
+ type WidgetId = 'pricing-card' | 'subscribe-button' | 'checkout-flow' | 'subscription-manager' | 'invoice-list' | 'usage-meter' | 'payment-method' | 'upgrade-cancel';
11
+ /** Event payload envelope. `version` allows non-breaking evolution. */
12
+ interface AforoEventPayload<T = unknown> {
13
+ type: string;
14
+ payload: T;
15
+ version: '1';
16
+ /**
17
+ * Optional widget id the event originated from. Set automatically by
18
+ * widget components; not required for parent-to-widget messages.
19
+ */
20
+ source?: WidgetId;
21
+ /**
22
+ * Optional monotonic id for diagnostics. Auto-stamped by EventBus.
23
+ */
24
+ emittedAt?: number;
25
+ }
26
+ /** Known event types (extended in Prompts 4-7 as widgets ship). */
27
+ type AforoEventType = `aforo.${WidgetId}.ready` | `aforo.${WidgetId}.error` | 'aforo.session.refreshed' | 'aforo.session.cleared' | 'aforo.session.magic_link_requested' | 'aforo.session.magic_link_verified' | 'aforo.session.magic_link_failed' | 'aforo.theme.changed' | 'aforo.subscribe-button.ready' | 'aforo.subscribe-button.disabled' | 'aforo.subscribe.checkout_requested' | 'aforo.subscription.created' | 'aforo.subscribe.error' | 'aforo.invoice-list.invoice.downloaded' | 'aforo.invoice-list.pay_requested' | 'aforo.invoice-list.invoice.expanded' | 'aforo.invoice-list.filter_changed' | 'aforo.invoice-list.search_changed' | 'aforo.invoice-list.invoice.paid' | 'aforo.checkout-flow.step_changed' | 'aforo.checkout-flow.customer_details_submitted' | 'aforo.checkout-flow.payment_initiated' | 'aforo.checkout-flow.payment_completed' | 'aforo.checkout-flow.confirmed' | 'aforo.checkout-flow.cancelled' | 'aforo.checkout-flow.expired' | 'aforo.subscription-manager.subscription.clicked' | 'aforo.subscription-manager.upgrade_requested' | 'aforo.subscription-manager.cancel_requested' | 'aforo.subscription-manager.filter_changed' | 'aforo.usage-meter.metric_clicked' | 'aforo.usage-meter.threshold_reached' | 'aforo.upgrade-cancel.step_changed' | 'aforo.upgrade-cancel.preview_fetched' | 'aforo.upgrade-cancel.deflection_offer_shown' | 'aforo.upgrade-cancel.deflection_offer_accepted' | 'aforo.upgrade-cancel.completed' | 'aforo.upgrade-cancel.abandoned' | 'aforo.payment-method.update_requested' | 'aforo.payment-method.method_set_default' | 'aforo.payment-method.method_removed' | 'aforo.payment-method.updated' | string;
28
+ /** Theme tokens read by widgets. */
29
+ interface ThemeTokens {
30
+ primary: string;
31
+ primaryContrast: string;
32
+ text: string;
33
+ textMuted: string;
34
+ bg: string;
35
+ border: string;
36
+ radius: string;
37
+ fontFamily: string;
38
+ /** Source tier — useful for debugging which layer of the cascade applied. */
39
+ source: 'css-variables' | 'brand-kit' | 'neutral-default' | 'overrides';
40
+ }
41
+ interface ThemeTokenOverrides {
42
+ primary?: string;
43
+ primaryContrast?: string;
44
+ text?: string;
45
+ textMuted?: string;
46
+ bg?: string;
47
+ border?: string;
48
+ radius?: string;
49
+ fontFamily?: string;
50
+ }
51
+ interface SessionConfig {
52
+ tenantSlug: string;
53
+ embedKey: string;
54
+ /** Pre-issued customer bridge token (JWT). Optional for anonymous. */
55
+ bridgeToken?: string;
56
+ /**
57
+ * Magic-link auth mode (Prompt 9 / FR-AUTH-6). When `true`, the session
58
+ * lazy-initialises by consuming an {@code aforo_magic_token} URL param on
59
+ * mount. Mutually exclusive with {@link bridgeToken} — when both are set,
60
+ * {@link bridgeToken} wins (anonymous host portals + magic-link callers
61
+ * naturally only ever set one).
62
+ */
63
+ magicLinkMode?: boolean;
64
+ /** Override the default `https://embed.aforo.ai` origin. Tests + dev. */
65
+ apiBaseUrl?: string;
66
+ /** Override embed-key authenticated endpoint base. */
67
+ embedBaseUrl?: string;
68
+ }
69
+ /**
70
+ * Body for {@code AforoSession.requestMagicLink} / {@code BridgeClient.requestMagicLink}.
71
+ * Mirrors the storefront-service {@code RequestMagicLinkRequest} DTO.
72
+ */
73
+ interface RequestMagicLinkRequest {
74
+ email: string;
75
+ externalId?: string;
76
+ returnUrl: string;
77
+ }
78
+ /** Always {@code {accepted: true}} on the wire — anti-enumeration. */
79
+ interface RequestMagicLinkResponse {
80
+ accepted: boolean;
81
+ }
82
+ /**
83
+ * Success envelope from {@code POST /api/v1/portal/embed/auth/magic-link/verify}.
84
+ * Mirrors the storefront-service {@code VerifyMagicLinkResponse} DTO.
85
+ */
86
+ interface VerifyMagicLinkResponse {
87
+ sessionJwt: string;
88
+ expiresIn: number;
89
+ customerId: string;
90
+ }
91
+ /**
92
+ * Typed error code emitted via {@code aforo.session.magic_link_failed}.
93
+ * {@code INVALID_OR_EXPIRED} maps to the backend's generic 401 envelope
94
+ * (anti-enumeration: not-found / consumed / expired all collapse). Network
95
+ * failures and configuration gaps surface as distinct codes so widgets can
96
+ * branch on retry-ability.
97
+ */
98
+ type MagicLinkFailureCode = 'INVALID_OR_EXPIRED' | 'NO_CUSTOMER_FOR_EMAIL' | 'NETWORK_ERROR' | 'CONFIG_MISSING';
99
+ interface SessionState {
100
+ sessionJwt: string | null;
101
+ expiresAt: number | null;
102
+ tenantSlug: string;
103
+ customerId: string | null;
104
+ }
105
+ interface WidgetMountConfig {
106
+ widget: WidgetId;
107
+ tenantSlug: string;
108
+ embedKey: string;
109
+ bridgeToken?: string;
110
+ layout?: string;
111
+ themeOverrides?: ThemeTokenOverrides;
112
+ /**
113
+ * Additional widget-specific config — passed as a Record so each widget
114
+ * shell can validate its own subset.
115
+ */
116
+ config?: Record<string, unknown>;
117
+ }
118
+ interface TenantBrandKit {
119
+ primaryColor?: string | null;
120
+ secondaryColor?: string | null;
121
+ logoUrl?: string | null;
122
+ fontFamily?: string | null;
123
+ }
124
+ /** Inferred consent posture. Set via parent `aforo:consent-state` message. */
125
+ interface ConsentState {
126
+ telemetry: boolean;
127
+ marketing: boolean;
128
+ }
129
+ /** A single feature row inside a plan card. */
130
+ interface FeaturePayload {
131
+ /** Stable id — used as React key when rendering. */
132
+ id?: string;
133
+ /** Visible label. */
134
+ label: string;
135
+ /** Optional cell value for the comparison-table layout (e.g. "Unlimited"). */
136
+ value?: string;
137
+ /** Truthy → row renders ✓ ; falsy → row renders × (table layout). */
138
+ included?: boolean;
139
+ /** Optional explanation surfaced via `title` attribute / aria-describedby. */
140
+ description?: string;
141
+ }
142
+ /** Per-tier pricing breakpoint (for graduated/tiered models — display only). */
143
+ interface RatePlanPayload {
144
+ ratePlanId: string;
145
+ /** Display unit, e.g. "API call", "GB-hour". */
146
+ unitLabel?: string;
147
+ /** Free quota included before per-unit pricing kicks in. */
148
+ includedUnits?: number | null;
149
+ /** Per-unit price in minor units (cents/paise). */
150
+ perUnitPriceCents?: number | null;
151
+ /** Tiered model identifier — operator-set, surfaced to telemetry. */
152
+ pricingModel?: string;
153
+ }
154
+ /** A single offering as rendered in the pricing card. */
155
+ interface OfferingPayload {
156
+ offeringId: string;
157
+ /** Human-visible name (e.g. "Pro", "Enterprise"). */
158
+ name: string;
159
+ /** Optional localized name override (FR-WIDGET-PC-9). */
160
+ localizedDisplayName?: string;
161
+ /** Optional short subtitle. */
162
+ description?: string;
163
+ /** Optional localized description override (FR-WIDGET-PC-9). */
164
+ localizedDescription?: string;
165
+ /** ISO 4217 currency code, e.g. "USD". */
166
+ currency: string;
167
+ /** Subscription price in MINOR units (cents/paise). NaN/undefined → "Contact us". */
168
+ priceCents: number | null | undefined;
169
+ /** Billing period — `monthly` / `annual` / `one_time` / `custom`. */
170
+ billingCycle: string;
171
+ /** Free-form CTA label (e.g. "Start trial"). Operator may override per-offering. */
172
+ ctaText?: string;
173
+ /** Optional CTA url override (FR-WIDGET-PC-7). When unset the widget's ctaUrl prop wins. */
174
+ ctaUrl?: string;
175
+ /** Featured plan flag (FR-WIDGET-PC-8). */
176
+ featured?: boolean;
177
+ /** Render order — ascending. Stable per FR-WIDGET-PC-1. */
178
+ displayOrder?: number;
179
+ /** Plan status — only PUBLISHED offerings render. */
180
+ status?: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED' | string;
181
+ /** Features list rendered inside the card. */
182
+ features?: FeaturePayload[];
183
+ /** Optional bound rate plan(s) — surfaced in the cta_clicked event payload. */
184
+ ratePlans?: RatePlanPayload[];
185
+ /** Primary rate plan id for telemetry + the cta_clicked envelope. */
186
+ primaryRatePlanId?: string;
187
+ /** Free-form metadata bag — kept opaque, surfaced verbatim in events. */
188
+ metadata?: Record<string, unknown>;
189
+ }
190
+ /** Branding block in the headless config (intersects TenantBrandKit). */
191
+ interface HeadlessBrandingPayload {
192
+ primaryColor?: string | null;
193
+ secondaryColor?: string | null;
194
+ logoUrl?: string | null;
195
+ fontFamily?: string | null;
196
+ /** Optional contact for the "Contact sales" fallback CTA. */
197
+ contactSalesEmail?: string | null;
198
+ }
199
+ /** Top-level shape returned by `GET /api/v1/portal/headless/config`. */
200
+ interface HeadlessConfigResponse {
201
+ /** Tenant slug echo. */
202
+ tenantSlug?: string;
203
+ /** Display name (rendered in operator-empty-state copy). */
204
+ tenantName?: string | null;
205
+ branding?: HeadlessBrandingPayload | null;
206
+ /** Ordered list of PUBLISHED offerings. */
207
+ offerings?: OfferingPayload[];
208
+ /** Locale hint from operator config — widget's prop still wins. */
209
+ defaultLocale?: string | null;
210
+ /**
211
+ * 2026-06-01 — Tenant-wide defaults for embeddable widgets, authored in
212
+ * the admin `EmbeddableWidgetTab`. Read by widgets at mount time as
213
+ * fallback defaults; the customer's mount config (`<script>` data-*
214
+ * attributes) still wins on a per-widget basis.
215
+ *
216
+ * Field shape mirrors `EmbeddableWidgetConfig` in `@aforoai/types`. Kept
217
+ * loose here (`Record<string, unknown>`) so the SDK doesn't have a hard
218
+ * compile dependency on the admin types — widgets pick the fields they
219
+ * recognise and ignore the rest.
220
+ */
221
+ embeddableWidget?: EmbeddableWidgetDefaults | null;
222
+ /**
223
+ * 2026-06-01 — Up to 5 tenant-authored page-context-scoped CSS files.
224
+ * The storefont-ui's `<PageScopedStylesheets>` injector consumes these
225
+ * from `publishedConfig.customStylesheets`; we re-expose them here so
226
+ * headless third-party storefronts can replicate the same cascade in
227
+ * their own builds.
228
+ */
229
+ customStylesheets?: HeadlessCustomStylesheet[] | null;
230
+ }
231
+ /**
232
+ * Subset of {@link EmbeddableWidgetConfig} (in `@aforoai/types`) the SDK
233
+ * recognises. Loose typing — extra fields are dropped, missing fields fall
234
+ * back to widget-level defaults.
235
+ */
236
+ interface EmbeddableWidgetDefaults {
237
+ layout?: 'horizontal' | 'vertical' | string;
238
+ theme?: 'light' | 'dark' | 'auto' | string;
239
+ ctaText?: string;
240
+ showFeatures?: boolean;
241
+ maxPlans?: number;
242
+ borderRadius?: number;
243
+ }
244
+ /** Page-context-scoped CSS stylesheet emitted by the headless config endpoint. */
245
+ interface HeadlessCustomStylesheet {
246
+ id: string;
247
+ name: string;
248
+ description?: string;
249
+ css: string;
250
+ enabled: boolean;
251
+ sortOrder?: number;
252
+ pageContext?: 'LANDING' | 'PRICING' | 'DOCS' | 'CATALOG' | 'PORTAL' | 'ALL' | string;
253
+ }
254
+ /**
255
+ * Live-subscription status set the BFF reports back to the widget for
256
+ * anti-double-subscribe decisions. Mirrors backend pricing-service
257
+ * SubscriptionStatus enum (subset shown — only states that gate a
258
+ * re-subscribe attempt).
259
+ */
260
+ type LiveSubscriptionStatus = 'ACTIVE' | 'TRIALING' | 'PAST_DUE' | 'EXPIRING_SOON';
261
+ /**
262
+ * Response shape for {@code GET /api/v1/portal/embed/subscriptions/by-offering/{offeringId}}.
263
+ *
264
+ * <p>{@code subscriptionId} + {@code status} are omitted when
265
+ * {@code hasActiveSubscription=false} — same shape as a legitimate "not
266
+ * subscribed" response, no existence-leak via differential field presence
267
+ * (Pattern #9).</p>
268
+ */
269
+ interface SubscriptionForOfferingResponse {
270
+ hasActiveSubscription: boolean;
271
+ subscriptionId?: string;
272
+ status?: LiveSubscriptionStatus | string;
273
+ }
274
+ /**
275
+ * Request body for {@code POST /api/v1/portal/embed/checkout/initiate}.
276
+ *
277
+ * <p>Idempotency contract per FR-SEC-19: callers MUST supply a UUID v4 (or
278
+ * other RFC 4122 shape) in {@code idempotencyKey}; same key + same payload
279
+ * → cached response replay, same key + different payload → 409 Conflict.</p>
280
+ */
281
+ interface InitiateCheckoutRequest {
282
+ offeringId: string;
283
+ /** UUID v4 — generated client-side per CTA click. */
284
+ idempotencyKey: string;
285
+ /**
286
+ * Where hosted checkout returns the customer to on success. Validated
287
+ * server-side against the embed key's {@code allowed_domains} list — host
288
+ * outside the allowlist returns 400 (defense vs. open-redirect).
289
+ */
290
+ returnUrl: string;
291
+ /** Optional coupon code applied to the underlying checkout session. */
292
+ couponCode?: string;
293
+ /**
294
+ * Mode advisory — v1 always produces a real checkoutUrl; the
295
+ * {@code embedded-flow} mode is reserved for Prompt 7 (CheckoutFlow widget).
296
+ */
297
+ mode?: 'redirect' | 'event-only' | 'embedded-flow';
298
+ }
299
+ /** Response shape for {@code POST /api/v1/portal/embed/checkout/initiate}. */
300
+ interface InitiateCheckoutResponse {
301
+ checkoutUrl: string;
302
+ checkoutSessionId: string;
303
+ expiresAt: string;
304
+ }
305
+ /**
306
+ * Payload for the {@code aforo.subscription.created} outbound event +
307
+ * the {@code onSubscribed} React callback. Backend webhook is the
308
+ * authoritative source for subscription state; this event is advisory.
309
+ */
310
+ interface SubscriptionPayload {
311
+ subscriptionId: string;
312
+ offeringId: string;
313
+ /** Optional plan name for display. */
314
+ planName?: string;
315
+ /** ISO-4217 currency code echoed from offering. */
316
+ currency?: string;
317
+ /** Price in MINOR units (cents/paise) at the time of subscription. */
318
+ priceCents?: number | null;
319
+ /** Billing period at time of subscription. */
320
+ billingCycle?: string;
321
+ /** Status returned by the BFF after checkout-complete signal. */
322
+ status?: LiveSubscriptionStatus | string;
323
+ /** Server-side timestamp the subscription was created (ISO-8601). */
324
+ createdAt?: string;
325
+ }
326
+ /**
327
+ * Payload for the {@code aforo.subscribe.error} (and other widget error)
328
+ * events + the {@code onError} React callback. Surfaced when a fetch /
329
+ * checkout-initiate fails or when an inbound checkout-completed event
330
+ * carries an error state.
331
+ */
332
+ interface WidgetErrorPayload {
333
+ /** Stable machine-readable code (e.g. SESSION_EXPIRED, NETWORK_ERROR). */
334
+ code: string;
335
+ /** Human-readable message — suitable for surfacing in UI banners. */
336
+ message: string;
337
+ /** HTTP status when the error originated from a network call. */
338
+ status?: number;
339
+ /** Whether the operation is safe to retry (e.g. transient 5xx / timeout). */
340
+ retryable?: boolean;
341
+ /** Optional widget id for cross-event correlation. */
342
+ widget?: WidgetId;
343
+ }
344
+ /**
345
+ * Customer-facing status vocabulary for the AforoInvoiceList widget. The
346
+ * upstream {@code Invoice.status} enum (DRAFT/PENDING/OPEN/PAID/OVERDUE/
347
+ * CANCELLED/VOID/UNCOLLECTIBLE) is normalised to this 4-state set at the
348
+ * BFF slimming boundary — DRAFT invoices are filtered out, and
349
+ * CANCELLED/UNCOLLECTIBLE/VOID collapse to {@code VOID}.
350
+ */
351
+ type InvoiceStatus = 'UNPAID' | 'PAID' | 'OVERDUE' | 'VOID';
352
+ /**
353
+ * Filter chip value for the widget UI + {@code status} query param sent to
354
+ * the BFF. Lowercase per the prompt's API (the BFF uppercases on receive).
355
+ */
356
+ type InvoiceStatusFilter = 'all' | 'unpaid' | 'paid' | 'overdue' | 'void';
357
+ /**
358
+ * Invoice type — STANDARD invoices are the common case; AMENDMENT renders
359
+ * a small "Amendment" badge per the B2 2026-05-09 ship parity. The widget
360
+ * doesn't distinguish AMENDMENT vs AMENDMENT_CREDIT visually beyond the
361
+ * badge — both are post-creation adjustments.
362
+ */
363
+ type InvoiceTypeKind = 'STANDARD' | 'AMENDMENT' | 'AMENDMENT_CREDIT';
364
+ /**
365
+ * Per-row invoice payload — mirrors
366
+ * {@code com.aforo.billing.storefront.embed.dto.EmbedInvoiceResponse}
367
+ * field-for-field. {@code BigDecimal} amounts are serialised by Jackson
368
+ * as JSON numbers (Spring's default {@code JsonNumber} mapping); we type
369
+ * them as {@code number | string} so a future serialiser switch (e.g. to
370
+ * {@code String} for arbitrary precision) doesn't break the widget.
371
+ *
372
+ * <p>NaN-defensive formatting: {@code total}, {@code amountPaid}, and
373
+ * {@code amountDue} render as {@code '—'} when null / undefined / non-
374
+ * finite per FR-WIDGET-X-9.</p>
375
+ */
376
+ interface InvoicePayload {
377
+ id: string;
378
+ invoiceNumber: string;
379
+ /**
380
+ * Customer-facing status (UNPAID / PAID / OVERDUE / VOID). DRAFT
381
+ * invoices are filtered server-side; widget never sees them.
382
+ */
383
+ status: InvoiceStatus | string;
384
+ invoiceType?: InvoiceTypeKind | string;
385
+ issuedAt?: string | null;
386
+ dueAt?: string | null;
387
+ billingPeriodStart?: string | null;
388
+ billingPeriodEnd?: string | null;
389
+ total?: number | string | null;
390
+ amountPaid?: number | string | null;
391
+ amountDue?: number | string | null;
392
+ currency?: string | null;
393
+ lineItemCount?: number | null;
394
+ /**
395
+ * Server-derived overdue flag. Widget UI prefers this over re-computing
396
+ * from {@code dueAt} so all widget instances on a page agree regardless
397
+ * of client clock drift.
398
+ */
399
+ isOverdue?: boolean;
400
+ /**
401
+ * Days from now until {@code dueAt} — negative when overdue. Null when
402
+ * {@code dueAt} is missing.
403
+ */
404
+ daysUntilDue?: number | null;
405
+ }
406
+ /** Paginated envelope returned by {@code GET /api/v1/portal/embed/invoices}. */
407
+ interface InvoicePageResponse {
408
+ content: InvoicePayload[];
409
+ totalElements: number;
410
+ totalPages: number;
411
+ page: number;
412
+ size: number;
413
+ }
414
+ /**
415
+ * Filter query for {@code BridgeClient.fetchInvoices}. The widget builds
416
+ * one of these on every fetch, and a stable JSON encoding of it is the
417
+ * cache key (so refetching the same page is a noop).
418
+ */
419
+ interface InvoiceListFilter {
420
+ page: number;
421
+ size: number;
422
+ /** {@code 'all'} → unspecified (BFF ignores the param entirely). */
423
+ status?: InvoiceStatusFilter;
424
+ /** Substring match against {@code invoice_number}. Max 200 chars (BFF clamps). */
425
+ search?: string;
426
+ /**
427
+ * Server-side sort. v1 widget UI doesn't expose this (defaults to
428
+ * {@code issuedAt,desc}); kept on the API for forward-compat with
429
+ * sortable-column work (Phase 1 follow-up).
430
+ */
431
+ sort?: string;
432
+ }
433
+ /** Response shape for {@code GET /api/v1/portal/embed/invoices/{id}/pdf-url}. */
434
+ interface InvoicePdfUrlResponse {
435
+ /**
436
+ * Absolute URL pointing at the session-authenticated companion endpoint
437
+ * (v1) or the standalone signed URL (v2). Widget opens in a new tab via
438
+ * {@code window.open(url, '_blank', 'noopener,noreferrer')}.
439
+ */
440
+ downloadUrl: string;
441
+ /** ISO-8601 timestamp the widget should consider {@code downloadUrl} stale. */
442
+ expiresAt: string;
443
+ fileName: string;
444
+ /** Always null in v1 (upstream doesn't emit Content-Length yet). */
445
+ sizeBytes?: number | null;
446
+ }
447
+ /**
448
+ * Request body for {@code POST /api/v1/portal/embed/invoices/{id}/pay/initiate}.
449
+ *
450
+ * <p>Idempotency-Key is sent as an HTTP header (FR-SEC-19), NOT in the body —
451
+ * mirrors the EmbedCheckoutController convention. The widget generates a
452
+ * UUID v4 on every CTA click.</p>
453
+ */
454
+ interface InitiateInvoicePaymentRequest {
455
+ returnUrl: string;
456
+ }
457
+ /**
458
+ * Response shape for {@code POST /api/v1/portal/embed/invoices/{id}/pay/initiate}.
459
+ *
460
+ * <p><b>Phase 0 stub (Prompt 6A, 2026-05-27):</b> the BFF always returns
461
+ * {@code {supported: false, reason: 'EMBEDDED_CHECKOUT_NOT_YET_WIRED'}} with
462
+ * HTTP 200. Prompt 7 (CheckoutFlow widget) drop-in replaces the response
463
+ * to populate {@code checkoutUrl} / {@code checkoutSessionId} /
464
+ * {@code expiresAt} — the type stays the same so the widget's call site
465
+ * doesn't change.</p>
466
+ */
467
+ interface InitiateInvoicePaymentResponse {
468
+ supported: boolean;
469
+ reason?: string;
470
+ /** Populated when Prompt 7 ships the real CheckoutFlow integration. */
471
+ checkoutUrl?: string;
472
+ checkoutSessionId?: string;
473
+ expiresAt?: string;
474
+ }
475
+ /**
476
+ * Payload for the {@code aforo.invoice-list.invoice.downloaded} outbound
477
+ * event + the {@code onDownload} React callback (when the widget's
478
+ * downloaded action fires).
479
+ */
480
+ interface InvoiceDownloadedPayload {
481
+ invoiceId: string;
482
+ invoiceNumber: string;
483
+ fileName: string;
484
+ sizeBytes?: number | null;
485
+ }
486
+ /**
487
+ * Payload for the {@code aforo.invoice-list.pay_requested} outbound event +
488
+ * the {@code onPayRequested} React callback. Phase 0: customer's parent
489
+ * page handles payment in its own flow. Phase 1 (Prompt 7): the embedded
490
+ * CheckoutFlow widget consumes this signal in-place.
491
+ */
492
+ interface InvoicePayRequestedPayload {
493
+ invoiceId: string;
494
+ invoiceNumber: string;
495
+ total: number | string | null;
496
+ currency: string | null;
497
+ status: string;
498
+ dueAt: string | null;
499
+ returnUrl: string;
500
+ }
501
+ /** Payload for the informational {@code aforo.invoice-list.filter_changed} event. */
502
+ interface InvoiceFilterChangedPayload {
503
+ status: InvoiceStatusFilter;
504
+ }
505
+ /** Payload for the informational {@code aforo.invoice-list.search_changed} event. */
506
+ interface InvoiceSearchChangedPayload {
507
+ /** Empty string when the search was cleared. */
508
+ query: string;
509
+ }
510
+ /** Payload for the informational {@code aforo.invoice-list.invoice.expanded} event. */
511
+ interface InvoiceExpandedPayload {
512
+ invoiceId: string;
513
+ invoiceNumber: string;
514
+ expanded: boolean;
515
+ }
516
+ /**
517
+ * Payload for the {@code aforo.invoice-list.invoice.paid} outbound event.
518
+ * Fires when the parent posts inbound {@code aforo:invoice-paid} OR when
519
+ * Prompt 7's CheckoutFlow signals a successful invoice payment. Backend
520
+ * webhook is still authoritative per FR-SEC-23.
521
+ */
522
+ interface InvoicePaidPayload {
523
+ invoiceId: string;
524
+ invoiceNumber?: string;
525
+ }
526
+ /** Cart type — SUBSCRIBE creates a new subscription; INVOICE_PAYMENT pays an existing invoice. */
527
+ type CheckoutCartType = 'SUBSCRIBE' | 'INVOICE_PAYMENT';
528
+ /** Backend cart status — must stay in lock-step with `EmbedCheckoutCartStatus.java`. */
529
+ type CheckoutCartStatus = 'CREATED' | 'CUSTOMER_DETAILS' | 'PAYMENT_REDIRECTED' | 'COMPLETED' | 'EXPIRED' | 'CANCELLED';
530
+ /**
531
+ * Client-side phase — combines backend status with UI-layer states. The
532
+ * widget renders different content per phase:
533
+ *
534
+ * - `bootstrapping` — bridge-token → session-JWT exchange + cart create
535
+ * (loading skeleton, role=status)
536
+ * - `customer-details` — Step 1 form (name + email + billing address)
537
+ * - `payment` — Step 2 payment-provider iframe
538
+ * - `confirming` — Step 3 confirm + provisioning spinner
539
+ * - `completed` — Step 4 success screen
540
+ * - `cancelled` / `expired` / `error` — terminal/recoverable states
541
+ */
542
+ type CheckoutFlowPhase = 'bootstrapping' | 'customer-details' | 'payment' | 'confirming' | 'completed' | 'cancelled' | 'expired' | 'error';
543
+ /** Payment provider — drives which iframe contract the widget renders. */
544
+ type CheckoutPaymentProvider = 'stripe' | 'razorpay' | 'paypal';
545
+ /**
546
+ * Billing address — captured at the customer-details step. All fields
547
+ * optional per backend `UpdateCartCustomerDetailsRequest.BillingAddress`.
548
+ */
549
+ interface CheckoutBillingAddress {
550
+ line1?: string;
551
+ line2?: string;
552
+ city?: string;
553
+ state?: string;
554
+ postalCode?: string;
555
+ /** ISO 3166-1 alpha-2 — e.g. "US", "DE". 2 chars max. */
556
+ country?: string;
557
+ }
558
+ /**
559
+ * Request body for {@code POST /api/v1/portal/embed/checkout/cart}.
560
+ *
561
+ * <p>Idempotency-Key header carries the per-click UUID v4; not part of the
562
+ * body. Cart-create is the only POST that doesn't take an explicit
563
+ * idempotencyKey field — the {@code BridgeClient.createCart} method accepts
564
+ * one as a separate parameter, mirroring the {@code initiateInvoicePayment}
565
+ * call shape from Prompt 6B.</p>
566
+ */
567
+ interface CreateCartRequest {
568
+ cartType: CheckoutCartType;
569
+ /** Offering id when cartType=SUBSCRIBE; invoice id when cartType=INVOICE_PAYMENT. */
570
+ targetId: string;
571
+ /**
572
+ * Where to return the customer if the payment flow leaves the iframe
573
+ * (PayPal redirect, 3DS challenges in a new tab). Validated server-side
574
+ * against the embed key's allowed_domains list.
575
+ */
576
+ returnUrl: string;
577
+ }
578
+ /**
579
+ * Request body for {@code PATCH /api/v1/portal/embed/checkout/cart/{cartId}/customer-details}.
580
+ * Both fields optional — widget pre-fills + skips for returning customers.
581
+ */
582
+ interface UpdateCustomerDetailsRequest {
583
+ billingAddress?: CheckoutBillingAddress;
584
+ displayName?: string;
585
+ }
586
+ /**
587
+ * Request body for {@code POST /api/v1/portal/embed/checkout/cart/{cartId}/initiate-payment}.
588
+ *
589
+ * <p>Called AFTER the customer enters card details in the provider iframe.
590
+ * The {@code gatewayPaymentIntentId} MUST match the cart's stored intent
591
+ * id (defense against intent-swap attacks per Prompt 7A round-2 audit).</p>
592
+ */
593
+ interface InitiateCartPaymentRequest {
594
+ gatewayPaymentMethodId: string;
595
+ gatewayPaymentIntentId: string;
596
+ }
597
+ /**
598
+ * Response shape for {@code POST /cart}, {@code GET /cart/{id}}, and
599
+ * {@code PATCH /cart/{id}/customer-details}.
600
+ *
601
+ * <p>Gateway secrets ({@code gatewayClientSecret}, {@code gatewayOrderId},
602
+ * {@code gatewayApprovalUrl}, {@code gatewaySandboxMode}) are populated
603
+ * ONLY on the cart-create response per FR-SEC-9. Subsequent GET / PATCH
604
+ * reads return null for those fields — widget MUST sessionStorage them.</p>
605
+ */
606
+ interface CheckoutCartResponse {
607
+ cartId: string;
608
+ cartType: CheckoutCartType;
609
+ targetId: string;
610
+ status: CheckoutCartStatus;
611
+ totalCents: number;
612
+ currency: string;
613
+ gatewayProvider?: CheckoutPaymentProvider | string | null;
614
+ /** Stripe — null on read after create. */
615
+ gatewayClientSecret?: string | null;
616
+ /** Razorpay / hosted — null on read after create. */
617
+ gatewayOrderId?: string | null;
618
+ /** PayPal redirect URL — null on read after create. */
619
+ gatewayApprovalUrl?: string | null;
620
+ /** PayPal — true when test creds, null for others. */
621
+ gatewaySandboxMode?: boolean | null;
622
+ /** Populated when status=COMPLETED + cartType=SUBSCRIBE. */
623
+ subscriptionId?: string | null;
624
+ /** Populated when status=COMPLETED + cartType=INVOICE_PAYMENT. */
625
+ invoiceId?: string | null;
626
+ /** ISO-8601 — widget shows countdown when within last 5 min. */
627
+ expiresAt: string;
628
+ completedAt?: string | null;
629
+ }
630
+ /**
631
+ * Response shape for {@code POST /cart/{cartId}/confirm}.
632
+ *
633
+ * <p>SUBSCRIBE-variant populates {@code subscriptionId} + {@code subscriptionStatus};
634
+ * INVOICE_PAYMENT-variant populates {@code invoiceId} + {@code invoiceStatus}.</p>
635
+ *
636
+ * <p><b>FR-SEC-23 reminder:</b> these fields are returned synchronously for
637
+ * widget UX but the authoritative state comes via Kafka webhook events.</p>
638
+ */
639
+ interface ConfirmCartResponse {
640
+ cartId: string;
641
+ status: CheckoutCartStatus;
642
+ subscriptionId?: string | null;
643
+ subscriptionStatus?: string | null;
644
+ invoiceId?: string | null;
645
+ invoiceStatus?: string | null;
646
+ completedAt?: string | null;
647
+ }
648
+ /**
649
+ * Typed error envelope from the embed checkout endpoints — mirrors backend
650
+ * {@code CartErrorResponse} record. Surfaces 402 Payment Required (declined
651
+ * cards, 3DS challenges) + 409 Conflict (already-subscribed, invoice-already-paid,
652
+ * cart-state-invalid).
653
+ *
654
+ * <p>Codes the widget UI branches on:</p>
655
+ * <ul>
656
+ * <li>{@code PAYMENT_FAILED} — show error + retry affordance</li>
657
+ * <li>{@code REQUIRES_ACTION} — open {@code nextActionUrl} in popup; listen
658
+ * for {@code aforo:payment-challenge-completed} inbound event</li>
659
+ * <li>{@code ALREADY_SUBSCRIBED} — show "Already subscribed" with link to
660
+ * {@code existingSubscriptionId} subscription</li>
661
+ * <li>{@code INVOICE_ALREADY_PAID} — show "Invoice already paid" + dismiss</li>
662
+ * <li>{@code CART_STATE_INVALID} — cart not in expected state for transition</li>
663
+ * </ul>
664
+ */
665
+ interface CheckoutCartErrorResponse {
666
+ code: string;
667
+ message: string;
668
+ requiresAction?: boolean;
669
+ nextActionUrl?: string | null;
670
+ gatewayCode?: string | null;
671
+ existingSubscriptionId?: string | null;
672
+ }
673
+ /**
674
+ * Payload for {@code aforo.checkout-flow.step_changed} outbound event.
675
+ * Fires whenever the widget's phase changes (e.g. customer-details → payment).
676
+ */
677
+ interface CheckoutStepChangedPayload {
678
+ from: CheckoutFlowPhase;
679
+ to: CheckoutFlowPhase;
680
+ cartId: string;
681
+ cartType: CheckoutCartType;
682
+ }
683
+ /**
684
+ * Payload for {@code aforo.checkout-flow.customer_details_submitted} event.
685
+ * Fires after a successful PATCH /customer-details — does NOT include the
686
+ * raw billing address (PII), only the fields that were populated.
687
+ */
688
+ interface CheckoutCustomerDetailsSubmittedPayload {
689
+ cartId: string;
690
+ /** True when the customer supplied a non-empty billing address (any field). */
691
+ billingAddressProvided: boolean;
692
+ /** True when displayName was supplied. */
693
+ displayNameProvided: boolean;
694
+ }
695
+ /**
696
+ * Payload for {@code aforo.checkout-flow.payment_initiated} event. Fires
697
+ * AFTER /initiate-payment returns 200 — i.e. payment method id is captured
698
+ * but the cart is not yet confirmed (still in PAYMENT_REDIRECTED state).
699
+ */
700
+ interface CheckoutPaymentInitiatedPayload {
701
+ cartId: string;
702
+ cartType: CheckoutCartType;
703
+ /** Provider that owned the tokenization step. */
704
+ gatewayProvider: CheckoutPaymentProvider | string;
705
+ }
706
+ /**
707
+ * Payload for {@code aforo.checkout-flow.payment_completed} event. Fires
708
+ * AFTER the gateway tokenization succeeds AND the cart is confirmed —
709
+ * the customer's payment has cleared.
710
+ */
711
+ interface CheckoutPaymentCompletedPayload {
712
+ cartId: string;
713
+ cartType: CheckoutCartType;
714
+ gatewayProvider: CheckoutPaymentProvider | string;
715
+ }
716
+ /**
717
+ * Payload for {@code aforo.checkout-flow.confirmed} event. Fires at the
718
+ * same moment as the SUBSCRIBE-variant aforo.subscription.created OR the
719
+ * INVOICE_PAYMENT-variant aforo.invoice-list.invoice.paid event, but
720
+ * carries the unified shape regardless of cart type.
721
+ */
722
+ interface CheckoutConfirmedPayload {
723
+ cartId: string;
724
+ cartType: CheckoutCartType;
725
+ /** Populated for SUBSCRIBE carts. */
726
+ subscriptionId?: string | null;
727
+ /** Populated for INVOICE_PAYMENT carts. */
728
+ invoiceId?: string | null;
729
+ }
730
+ /**
731
+ * Payload for {@code aforo.checkout-flow.cancelled} event. Fires when the
732
+ * customer dismisses the flow OR the parent posts {@code aforo:checkout-flow-dismissed}.
733
+ */
734
+ interface CheckoutCancelledPayload {
735
+ cartId: string;
736
+ cartType: CheckoutCartType;
737
+ /** UI vs. parent-driven dismissal. */
738
+ source: 'customer' | 'parent';
739
+ }
740
+ /**
741
+ * Payload for {@code aforo.checkout-flow.expired} event. Fires when the
742
+ * cart's {@code expiresAt} window elapses while the widget is still
743
+ * mounted in a non-terminal phase.
744
+ */
745
+ interface CheckoutExpiredPayload {
746
+ cartId: string;
747
+ cartType: CheckoutCartType;
748
+ expiresAt: string;
749
+ }
750
+ /**
751
+ * Customer-facing subscription status vocabulary. Mirrors backend
752
+ * {@code SubscriptionStatus} enum minus operator-internal states.
753
+ *
754
+ * <p>Status meanings:</p>
755
+ * <ul>
756
+ * <li>{@code ACTIVE} — billing normally</li>
757
+ * <li>{@code TRIALING} — in trial period</li>
758
+ * <li>{@code PAST_DUE} — payment failed, dunning in progress</li>
759
+ * <li>{@code PAUSED} — customer-initiated pause</li>
760
+ * <li>{@code EXPIRING_SOON} — auto-renew off, within 30 days of expiry</li>
761
+ * <li>{@code EXPIRED} — past renewal date, no auto-renew</li>
762
+ * <li>{@code CANCELLED} — customer cancelled (terminal)</li>
763
+ * <li>{@code SUSPENDED} — operator suspended (terminal-ish)</li>
764
+ * </ul>
765
+ */
766
+ type SubscriptionStatus = 'ACTIVE' | 'TRIALING' | 'PAST_DUE' | 'PAUSED' | 'EXPIRING_SOON' | 'EXPIRED' | 'CANCELLED' | 'SUSPENDED';
767
+ /**
768
+ * Filter chip value for the SubscriptionManager widget UI + {@code status}
769
+ * query param sent to the BFF. Lowercase per the prompt's API; the BFF
770
+ * uppercases on receive.
771
+ */
772
+ type SubscriptionStatusFilter = 'all' | 'ACTIVE' | 'TRIALING' | 'PAST_DUE' | 'PAUSED' | 'EXPIRING_SOON' | 'EXPIRED' | 'CANCELLED' | 'SUSPENDED';
773
+ /**
774
+ * Per-row subscription payload — mirrors {@code EmbedSubscriptionResponse}
775
+ * field-for-field. {@code BigDecimal} amounts are serialised by Jackson as
776
+ * JSON numbers; we type them as {@code number | string} so a future
777
+ * serializer switch (e.g. to {@code String} for arbitrary precision) doesn't
778
+ * break the widget.
779
+ *
780
+ * <p>NaN-defensive formatting: {@code mrr}, {@code nextInvoiceAmount} render
781
+ * as {@code '—'} when null / undefined / non-finite per FR-WIDGET-X-9.</p>
782
+ */
783
+ interface SubscriptionPayloadV2 {
784
+ id: string;
785
+ planName: string;
786
+ offeringId: string;
787
+ status: SubscriptionStatus | string;
788
+ startedAt?: string | null;
789
+ currentPeriodStart?: string | null;
790
+ currentPeriodEnd?: string | null;
791
+ renewsAt?: string | null;
792
+ mrr?: number | string | null;
793
+ currency?: string | null;
794
+ nextInvoiceAmount?: number | string | null;
795
+ trialEndsAt?: string | null;
796
+ isExpiringSoon?: boolean;
797
+ daysUntilRenewal?: number | null;
798
+ }
799
+ /** Paginated envelope returned by {@code GET /api/v1/portal/embed/subscriptions}. */
800
+ interface SubscriptionPageResponse {
801
+ content: SubscriptionPayloadV2[];
802
+ totalElements: number;
803
+ totalPages: number;
804
+ page: number;
805
+ size: number;
806
+ }
807
+ /**
808
+ * Filter query for {@code BridgeClient.fetchSubscriptions}. The widget builds
809
+ * one of these on every fetch; the BFF receives it via query params.
810
+ */
811
+ interface SubscriptionListFilter {
812
+ page: number;
813
+ size: number;
814
+ /** {@code 'all'} → unspecified (BFF ignores the param entirely). */
815
+ status?: SubscriptionStatusFilter;
816
+ /**
817
+ * Server-side sort. Default {@code startedAt,desc}; allowed values
818
+ * {@code startedAt,desc/asc}, {@code renewsAt,asc/desc}, {@code mrr,desc/asc}
819
+ * (mrr falls back to default at pricing-service today — Phase 1 follow-up).
820
+ */
821
+ sort?: string;
822
+ }
823
+ /**
824
+ * Per-metric usage row — mirrors {@code EmbedUsageMetricResponse}.
825
+ *
826
+ * <p>{@code quotaLimit=0} signals an unlimited / no-quota metric (e.g.
827
+ * PERCENTAGE / FLAT_RATE pricing) — the widget renders "Unlimited" in that
828
+ * case. {@code quotaUsedPct=0.0} with {@code quotaLimit=0} is consistent
829
+ * (no limit known → no pct).</p>
830
+ *
831
+ * <p>{@code isApproximated=true} for TIERED / VOLUME / STAIRCASE /
832
+ * INCLUDED_QUOTA pricing models — the widget renders a ⚠ icon next to the
833
+ * metric name with a tooltip explaining the upper-bound approximation.</p>
834
+ */
835
+ interface UsageMetricPayload {
836
+ metricName: string;
837
+ displayName?: string;
838
+ quotaLimit: number;
839
+ quotaUsed: number;
840
+ quotaUsedPct: number;
841
+ unit?: string | null;
842
+ pricingModel?: string | null;
843
+ isApproximated: boolean;
844
+ projectedEndOfPeriod?: number | null;
845
+ currency?: string | null;
846
+ estimatedSpend?: number | string | null;
847
+ }
848
+ /**
849
+ * Current-period usage payload — mirrors {@code EmbedUsageResponse}.
850
+ *
851
+ * <p>{@code periodProgress} is server-computed (0.0 - 1.0) for clock-drift
852
+ * resistance — the widget renders the progress bar at this width directly
853
+ * rather than recomputing client-side.</p>
854
+ *
855
+ * <p>{@code approximationNote} is non-null when ANY metric in {@code metrics}
856
+ * has {@code isApproximated=true} — the widget renders it as a single
857
+ * envelope-level banner instead of per-metric duplication.</p>
858
+ */
859
+ interface UsageResponse {
860
+ subscriptionId: string;
861
+ periodStart?: string | null;
862
+ periodEnd?: string | null;
863
+ periodProgress: number;
864
+ metrics: UsageMetricPayload[];
865
+ totalEstimatedSpend?: number | string | null;
866
+ approximationNote?: string | null;
867
+ computedAt?: string | null;
868
+ }
869
+ /** Payload for the {@code aforo.subscription-manager.subscription.clicked} event. */
870
+ interface SubscriptionClickedPayload {
871
+ subscriptionId: string;
872
+ offeringId: string;
873
+ planName: string;
874
+ status: string;
875
+ }
876
+ /** Payload for the {@code aforo.subscription-manager.upgrade_requested} event. */
877
+ interface SubscriptionUpgradeRequestedPayload {
878
+ subscriptionId: string;
879
+ offeringId: string;
880
+ planName: string;
881
+ status: string;
882
+ /**
883
+ * When {@code true}, the parent should drive the customer to a payment-
884
+ * update flow rather than an upgrade flow. Fired when the customer
885
+ * clicks "Update payment" on a PAST_DUE row.
886
+ */
887
+ paymentUpdate?: boolean;
888
+ }
889
+ /** Payload for the {@code aforo.subscription-manager.cancel_requested} event. */
890
+ interface SubscriptionCancelRequestedPayload {
891
+ subscriptionId: string;
892
+ offeringId: string;
893
+ planName: string;
894
+ status: string;
895
+ }
896
+ /** Payload for the {@code aforo.subscription-manager.filter_changed} event. */
897
+ interface SubscriptionFilterChangedPayload {
898
+ status: SubscriptionStatusFilter;
899
+ }
900
+ /** Payload for the {@code aforo.usage-meter.metric_clicked} event (compact mode). */
901
+ interface UsageMetricClickedPayload {
902
+ subscriptionId: string;
903
+ metricName: string;
904
+ quotaUsed: number;
905
+ quotaLimit: number;
906
+ quotaUsedPct: number;
907
+ }
908
+ /**
909
+ * Threshold values the {@code aforo.usage-meter.threshold_reached} event
910
+ * fires at. Crossings detected client-side by comparing current vs prior
911
+ * {@code quotaUsedPct}.
912
+ */
913
+ type UsageThreshold = 70 | 90 | 100;
914
+ /** Payload for the {@code aforo.usage-meter.threshold_reached} event. */
915
+ interface UsageThresholdReachedPayload {
916
+ subscriptionId: string;
917
+ metricName: string;
918
+ quotaUsed: number;
919
+ quotaLimit: number;
920
+ quotaUsedPct: number;
921
+ threshold: UsageThreshold;
922
+ /** When non-null, the projected end-of-period quantity at the time of crossing. */
923
+ projectedEndOfPeriod?: number | null;
924
+ }
925
+ /** When the change applies. UPGRADE forces IMMEDIATE; DOWNGRADE/CANCEL default PERIOD_END. */
926
+ type ApplyAt = 'IMMEDIATE' | 'PERIOD_END';
927
+ /** Direction of the requested subscription change. Derived server-side from price comparison. */
928
+ type SubscriptionChangeType = 'UPGRADE' | 'DOWNGRADE' | 'LATERAL';
929
+ /** Compact offering summary used by the upgrade preview "from/to" comparison cards. */
930
+ interface OfferingSummary {
931
+ id: string;
932
+ name: string;
933
+ priceCents: number;
934
+ currency: string;
935
+ }
936
+ /** Request body for {@code POST /preview-change}. NO Idempotency-Key (read). */
937
+ interface PreviewChangeRequest {
938
+ targetOfferingId: string;
939
+ applyAt: ApplyAt;
940
+ }
941
+ /**
942
+ * Response body for {@code POST /preview-change}. Pattern #18 fail-soft:
943
+ * {@code BridgeClient.previewSubscriptionChange} returns {@code null} on
944
+ * transport / non-2xx / malformed JSON so the widget can render an inline
945
+ * error + retry without blocking Back navigation.
946
+ *
947
+ * Amounts are in MINOR units (cents). Net positive → customer charged today;
948
+ * net negative → customer credited; zero → "No charge today".
949
+ */
950
+ interface PreviewChangeResponse {
951
+ currentOffering: OfferingSummary;
952
+ targetOffering: OfferingSummary;
953
+ changeType: SubscriptionChangeType;
954
+ applyAt: ApplyAt;
955
+ /** Credit for the unused portion of the current period (positive value). */
956
+ creditCents: number;
957
+ /** Prorated charge for the target plan (positive value). */
958
+ debitCents: number;
959
+ /** debitCents - creditCents. Positive = customer pays today; negative = customer credited. */
960
+ netCents: number;
961
+ currency: string;
962
+ /** Fraction of the period remaining at change-effective time, 0..1. */
963
+ prorationFactor: number;
964
+ /** ISO-8601 timestamp of the current period end. */
965
+ currentPeriodEnd: string;
966
+ /** Backend-generated plain-English summary suitable for Step 3 confirm. */
967
+ summary: string;
968
+ /** Caveats list rendered as footnote (e.g. "Discount expires at next renewal"). */
969
+ caveats: string[];
970
+ }
971
+ /** Request body for {@code POST /upgrade}. Idempotency-Key REQUIRED on header. */
972
+ interface UpgradeRequest {
973
+ targetOfferingId: string;
974
+ applyAt: ApplyAt;
975
+ }
976
+ /**
977
+ * Response body for {@code POST /upgrade}. Mutation path — throws
978
+ * {@link BridgeClientError} on failure.
979
+ *
980
+ * - {@code status=ACTIVE} + {@code appliedAt} set when IMMEDIATE succeeded.
981
+ * - {@code status=ACTIVE_PENDING_CHANGE} + {@code scheduledFor} set when PERIOD_END.
982
+ * - {@code invoiceId} present on net-positive upgrades; {@code creditNoteId} on net-negative downgrades.
983
+ */
984
+ interface UpgradeResponse {
985
+ subscriptionId: string;
986
+ previousOfferingId: string;
987
+ newOfferingId: string;
988
+ status: 'ACTIVE' | 'ACTIVE_PENDING_CHANGE';
989
+ /** ISO-8601 timestamp when an IMMEDIATE change applied, else null. */
990
+ appliedAt: string | null;
991
+ /** ISO-8601 timestamp when a PERIOD_END change will apply, else null. */
992
+ scheduledFor: string | null;
993
+ /** Invoice produced by an IMMEDIATE upgrade with net-positive charge. */
994
+ invoiceId: string | null;
995
+ /** Credit note produced by an IMMEDIATE downgrade with net-negative refund. */
996
+ creditNoteId: string | null;
997
+ }
998
+ /** 6 canonical cancel reason categories surfaced as Step 1 radio options. */
999
+ type CancelReason = 'TOO_EXPENSIVE' | 'NOT_USING_ENOUGH' | 'MISSING_FEATURE' | 'TECHNICAL_ISSUES' | 'SWITCHED_TO_COMPETITOR' | 'OTHER';
1000
+ /** Which deflection offer the widget surfaced at Step 2. Captured for V50 audit. */
1001
+ type RetentionOfferShown = 'DISCOUNT_20_PCT' | 'PAUSE_3_MONTHS' | 'DOWNGRADE' | 'NONE';
1002
+ /**
1003
+ * What the customer did with the offer. Backend maps the widget's
1004
+ * {@code CANCELLED_ANYWAY} + {@code ACCEPTED_OFFER} to V50's
1005
+ * {@code DECLINED_OFFERS} — this is a BFF detail, the widget sends the values
1006
+ * below verbatim.
1007
+ */
1008
+ type RetentionOutcome = 'NONE' | 'DECLINED_OFFERS' | 'CANCELLED_ANYWAY' | 'NO_OFFER_PRESENTED' | 'ACCEPTED_OFFER';
1009
+ /** Request body for {@code POST /cancel}. Idempotency-Key REQUIRED on header. */
1010
+ interface CancelWithFeedbackRequest {
1011
+ applyAt: ApplyAt;
1012
+ reason: CancelReason;
1013
+ /** Free-form additional context, capped at 1000 chars client-side; backend caps at 64. */
1014
+ reasonDetail?: string;
1015
+ retentionOfferShown: RetentionOfferShown;
1016
+ retentionOutcome: RetentionOutcome;
1017
+ }
1018
+ /**
1019
+ * Response body for {@code POST /cancel}. Mutation path — throws
1020
+ * {@link BridgeClientError} on failure.
1021
+ *
1022
+ * - {@code status=CANCELLED} + {@code cancelledAt} set when IMMEDIATE succeeded.
1023
+ * - {@code status=ACTIVE_PENDING_CANCEL} + {@code effectiveAt} ≈ current period end when PERIOD_END.
1024
+ * - {@code creditNoteId} present on IMMEDIATE cancellations with prorated refund.
1025
+ */
1026
+ interface CancelResponse {
1027
+ subscriptionId: string;
1028
+ status: 'CANCELLED' | 'ACTIVE_PENDING_CANCEL';
1029
+ /** ISO-8601 timestamp when IMMEDIATE cancel applied, else null. */
1030
+ cancelledAt: string | null;
1031
+ /** ISO-8601 timestamp the cancellation takes/took effect (today for IMMEDIATE, period end for PERIOD_END). */
1032
+ effectiveAt: string;
1033
+ /** V50 feedback row id — useful for audit cross-reference. */
1034
+ feedbackId: string;
1035
+ /** Credit note produced by an IMMEDIATE cancel with prorated refund. */
1036
+ creditNoteId: string | null;
1037
+ }
1038
+ /** Convenience union emitted as the {@code onCompleted} callback argument. */
1039
+ interface UpgradeCancelResultPayload {
1040
+ mode: 'upgrade' | 'cancel';
1041
+ upgrade?: UpgradeResponse;
1042
+ cancel?: CancelResponse;
1043
+ }
1044
+ /** Payload for {@code aforo.upgrade-cancel.step_changed}. Non-PII. */
1045
+ interface UpgradeCancelStepChangedPayload {
1046
+ from: number;
1047
+ to: number;
1048
+ mode: 'upgrade' | 'cancel';
1049
+ }
1050
+ /**
1051
+ * Payload for {@code aforo.upgrade-cancel.preview_fetched}. Non-PII —
1052
+ * carries the change shape but never the actual amounts (parent pages
1053
+ * shouldn't observe per-customer pricing from event traffic).
1054
+ */
1055
+ interface UpgradeCancelPreviewFetchedPayload {
1056
+ changeType: SubscriptionChangeType;
1057
+ applyAt: ApplyAt;
1058
+ /** True when the preview returned a non-zero net charge. */
1059
+ hasNet: boolean;
1060
+ }
1061
+ /** Payload for {@code aforo.upgrade-cancel.deflection_offer_shown}. */
1062
+ interface UpgradeCancelDeflectionOfferShownPayload {
1063
+ reason: CancelReason;
1064
+ offer: RetentionOfferShown;
1065
+ }
1066
+ /** Payload for {@code aforo.upgrade-cancel.deflection_offer_accepted}. */
1067
+ interface UpgradeCancelDeflectionOfferAcceptedPayload {
1068
+ reason: CancelReason;
1069
+ offer: RetentionOfferShown;
1070
+ }
1071
+ /**
1072
+ * Payload for {@code aforo.upgrade-cancel.completed}. Non-PII — carries
1073
+ * mode + status + presence flags only, never invoice/credit-note ids.
1074
+ *
1075
+ * FR-SEC-23 reminder: this event is INFORMATIONAL. The customer backend
1076
+ * MUST receive the {@code subscription.status_changed} webhook as the
1077
+ * authoritative signal. postMessage events are advisory only.
1078
+ */
1079
+ interface UpgradeCancelCompletedPayload {
1080
+ mode: 'upgrade' | 'cancel';
1081
+ status: string;
1082
+ hasInvoice: boolean;
1083
+ hasCreditNote: boolean;
1084
+ }
1085
+ /** Payload for {@code aforo.upgrade-cancel.abandoned}. */
1086
+ interface UpgradeCancelAbandonedPayload {
1087
+ step: number;
1088
+ mode: 'upgrade' | 'cancel';
1089
+ }
1090
+ /**
1091
+ * Interface every public method of the real BridgeClient mirrors by name.
1092
+ * The sandbox package implements this against client-side mock fixtures;
1093
+ * tests can install a recording stub. All methods MUST be async.
1094
+ *
1095
+ * Read methods that today return null on miss preserve that contract
1096
+ * (Pattern #18). Write methods that today throw on failure also throw
1097
+ * here when the demo provider chooses to surface an error.
1098
+ */
1099
+ interface DemoModeBridgeProvider {
1100
+ exchange(bridgeToken: string): Promise<{
1101
+ sessionJwt: string;
1102
+ expiresAt: number;
1103
+ customerId: string | null;
1104
+ }>;
1105
+ getTenantBrandKit(slug?: string): Promise<TenantBrandKit | null>;
1106
+ fetchHeadlessConfig(slug?: string): Promise<HeadlessConfigResponse | null>;
1107
+ sendTelemetry(batch: ReadonlyArray<Record<string, unknown>>): Promise<boolean>;
1108
+ fetchSubscriptionForOffering(offeringId: string, sessionJwt: string): Promise<SubscriptionForOfferingResponse | null>;
1109
+ initiateCheckout(request: InitiateCheckoutRequest, sessionJwt: string): Promise<InitiateCheckoutResponse>;
1110
+ fetchInvoices(filter: InvoiceListFilter, sessionJwt: string): Promise<InvoicePageResponse>;
1111
+ fetchInvoicePdfUrl(invoiceId: string, sessionJwt: string): Promise<InvoicePdfUrlResponse | null>;
1112
+ initiateInvoicePayment(invoiceId: string, request: InitiateInvoicePaymentRequest, idempotencyKey: string, sessionJwt: string): Promise<InitiateInvoicePaymentResponse>;
1113
+ createCart(request: CreateCartRequest, idempotencyKey: string, sessionJwt: string): Promise<CheckoutCartResponse>;
1114
+ getCart(cartId: string, sessionJwt: string): Promise<CheckoutCartResponse | null>;
1115
+ updateCustomerDetails(cartId: string, request: UpdateCustomerDetailsRequest, idempotencyKey: string, sessionJwt: string): Promise<CheckoutCartResponse>;
1116
+ initiatePayment(cartId: string, request: InitiateCartPaymentRequest, idempotencyKey: string, sessionJwt: string): Promise<CheckoutCartResponse>;
1117
+ confirmCart(cartId: string, idempotencyKey: string, sessionJwt: string): Promise<ConfirmCartResponse>;
1118
+ cancelCart(cartId: string, sessionJwt: string): Promise<void>;
1119
+ health(): Promise<{
1120
+ status: string;
1121
+ } | null>;
1122
+ requestMagicLink(req: RequestMagicLinkRequest, idempotencyKey?: string): Promise<RequestMagicLinkResponse>;
1123
+ verifyMagicLink(token: string, externalId?: string): Promise<VerifyMagicLinkResponse>;
1124
+ fetchSubscriptions(filter: SubscriptionListFilter, sessionJwt: string): Promise<SubscriptionPageResponse>;
1125
+ fetchUsage(subscriptionId: string, sessionJwt: string): Promise<UsageResponse | null>;
1126
+ previewSubscriptionChange(subscriptionId: string, request: PreviewChangeRequest, sessionJwt: string): Promise<PreviewChangeResponse | null>;
1127
+ upgradeSubscription(subscriptionId: string, request: UpgradeRequest, idempotencyKey: string, sessionJwt: string): Promise<UpgradeResponse>;
1128
+ cancelSubscription(subscriptionId: string, request: CancelWithFeedbackRequest, idempotencyKey: string, sessionJwt: string): Promise<CancelResponse>;
1129
+ fetchPaymentMethods(sessionJwt: string): Promise<PaymentMethodListResponse>;
1130
+ createPaymentMethodSetupIntent(sessionJwt: string, idempotencyKey: string): Promise<SetupIntentResponse | null>;
1131
+ setDefaultPaymentMethod(methodId: string, sessionJwt: string): Promise<void>;
1132
+ deletePaymentMethod(methodId: string, sessionJwt: string): Promise<void>;
1133
+ }
1134
+ /** Brand identifier — common card networks + ACH/SEPA shapes. Open string
1135
+ * so future providers (e.g. UPI / iDEAL) extend without an SDK bump. */
1136
+ type PaymentMethodBrand = "visa" | "mastercard" | "amex" | "discover" | "diners" | "jcb" | "unionpay" | "unknown" | string;
1137
+ /** Type — `card` is v1 baseline. `us_bank_account` / `sepa_debit` flow
1138
+ * through the same provider-iframe pattern in Phase 1. */
1139
+ type PaymentMethodType = "card" | "us_bank_account" | "sepa_debit" | string;
1140
+ interface PaymentMethodPayload {
1141
+ id: string;
1142
+ type: PaymentMethodType;
1143
+ brand: PaymentMethodBrand;
1144
+ last4: string;
1145
+ expiryMonth?: number;
1146
+ expiryYear?: number;
1147
+ holderName?: string;
1148
+ isDefault: boolean;
1149
+ /** ISO-8601 timestamp the method was created in billing-service. */
1150
+ createdAt?: string;
1151
+ }
1152
+ interface PaymentMethodListResponse {
1153
+ methods: PaymentMethodPayload[];
1154
+ defaultMethodId: string | null;
1155
+ }
1156
+ /** Returned by POST /payment-methods/setup-intent — the widget forwards
1157
+ * `clientSecret` to the parent page via the update_requested event. */
1158
+ interface SetupIntentResponse {
1159
+ clientSecret: string;
1160
+ provider: "stripe" | "razorpay" | "paypal" | string;
1161
+ /** Provider-specific opaque payload (e.g. Razorpay order id, PayPal
1162
+ * billing-token). Not introspected by the SDK. */
1163
+ providerData?: Record<string, unknown>;
1164
+ }
1165
+ declare global {
1166
+ interface Window {
1167
+ /**
1168
+ * The loader's shared `AforoEmbed` instance, attached at bootstrap
1169
+ * (see `loader/loader.ts`). Per-widget bundles (`loader/*.entry.ts`)
1170
+ * MUST register against this, not a locally-imported copy of
1171
+ * `src/core/AforoEmbed` — each widget bundle compiles to a standalone
1172
+ * IIFE, and importing `AforoEmbed` directly inlines a private copy of
1173
+ * the module (its own private `widgetRegistry`), disconnected from
1174
+ * the loader's copy. Registering into the private copy while the
1175
+ * loader's `mount()` reads from its own copy means the registration
1176
+ * silently never reaches the loader — "No mount handler registered"
1177
+ * even though the bundle loaded successfully.
1178
+ */
1179
+ aforoEmbed?: {
1180
+ _registerWidget: (id: WidgetId, mountFn: (element: HTMLElement, config: WidgetMountConfig) => () => void) => void;
1181
+ };
1182
+ }
1183
+ }
1184
+
1185
+ export type { CheckoutCustomerDetailsSubmittedPayload as $, ApplyAt as A, PreviewChangeResponse as B, CheckoutCartType as C, DemoModeBridgeProvider as D, UpgradeRequest as E, UpgradeResponse as F, CancelWithFeedbackRequest as G, HeadlessConfigResponse as H, InvoiceStatusFilter as I, CancelResponse as J, SetupIntentResponse as K, SessionConfig as L, SessionState as M, AforoEventPayload as N, OfferingPayload as O, PaymentMethodListResponse as P, AforoEventType as Q, RequestMagicLinkRequest as R, SubscriptionPayload as S, ThemeTokenOverrides as T, UsageMetricPayload as U, VerifyMagicLinkResponse as V, WidgetErrorPayload as W, CancelReason as X, CheckoutBillingAddress as Y, CheckoutCartErrorResponse as Z, CheckoutCartStatus as _, SubscriptionForOfferingResponse as a, CheckoutExpiredPayload as a0, CheckoutFlowPhase as a1, CheckoutPaymentCompletedPayload as a2, CheckoutPaymentInitiatedPayload as a3, CheckoutPaymentProvider as a4, CheckoutStepChangedPayload as a5, ConsentState as a6, FeaturePayload as a7, HeadlessBrandingPayload as a8, InvoiceDownloadedPayload as a9, UsageMetricClickedPayload as aA, UsageThresholdReachedPayload as aB, WidgetId as aC, WidgetMountConfig as aD, InvoiceExpandedPayload as aa, InvoiceFilterChangedPayload as ab, InvoicePaidPayload as ac, InvoicePayRequestedPayload as ad, InvoiceSearchChangedPayload as ae, InvoiceStatus as af, InvoiceTypeKind as ag, LiveSubscriptionStatus as ah, MagicLinkFailureCode as ai, OfferingSummary as aj, RatePlanPayload as ak, RetentionOfferShown as al, RetentionOutcome as am, SubscriptionCancelRequestedPayload as an, SubscriptionChangeType as ao, SubscriptionClickedPayload as ap, SubscriptionFilterChangedPayload as aq, SubscriptionStatus as ar, SubscriptionUpgradeRequestedPayload as as, ThemeTokens as at, UpgradeCancelAbandonedPayload as au, UpgradeCancelCompletedPayload as av, UpgradeCancelDeflectionOfferAcceptedPayload as aw, UpgradeCancelDeflectionOfferShownPayload as ax, UpgradeCancelPreviewFetchedPayload as ay, UpgradeCancelStepChangedPayload as az, CheckoutConfirmedPayload as b, CheckoutCancelledPayload as c, CheckoutCartResponse as d, SubscriptionStatusFilter as e, SubscriptionPayloadV2 as f, SubscriptionPageResponse as g, InvoicePayload as h, InvoicePageResponse as i, UsageThreshold as j, UsageResponse as k, UpgradeCancelResultPayload as l, TenantBrandKit as m, InitiateCheckoutRequest as n, InitiateCheckoutResponse as o, InvoiceListFilter as p, InvoicePdfUrlResponse as q, InitiateInvoicePaymentRequest as r, InitiateInvoicePaymentResponse as s, CreateCartRequest as t, UpdateCustomerDetailsRequest as u, InitiateCartPaymentRequest as v, ConfirmCartResponse as w, RequestMagicLinkResponse as x, SubscriptionListFilter as y, PreviewChangeRequest as z };