@flopay/js 1.9.19 → 1.9.21

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.
package/README.md CHANGED
@@ -43,7 +43,7 @@ mounting provider UI. Use `FloPayCheckout` or `SplitCardForm` from
43
43
  - `getUnifiedCheckoutSession(sessionId, nonce)` reads and normalizes a session.
44
44
  - `createSessionIntent(sessionId, nonce, request)` creates a Stripe wallet/APM or direct-PayPal intent. The request is a discriminated union of provider, payment-method category, type, and id, and intent kind; direct-card requests are not representable.
45
45
  - `reportSessionIntentDecline(sessionId, nonce, request)` reports a client-observed non-card decline using a classification code only. Messages, identifiers, card data, credentials, and personal data are rejected before sending.
46
- - `processPayment(userId, params)` submits a saved or approved non-card payment to the session's `/process` route. When the billing API answers `202`, the SDK keeps polling the session until it completes or times out.
46
+ - `processPayment(userId, params)` submits a saved or approved non-card payment to the session's `/process` route. When the billing API answers `202`, the SDK follows its authenticated status endpoint until the latest transaction attempt succeeds, authorizes, declines, requires buyer action, fails, or times out.
47
47
  - `getPaymentsByEmail(email)` looks up prior payments for a saved-payment experience.
48
48
 
49
49
  ### Create a hosted checkout session
@@ -87,20 +87,23 @@ For detached creation, `timeoutMs` is a fresh per-phase budget: the shell POST g
87
87
 
88
88
  ### Server-side card gateways (Worldpay)
89
89
 
90
- The billing API can process hosted-vault cards through an approved Worldpay Corporate Gateway entirely server-side. Whether a session can use it is the billing API's decision, made from the merchant's active gateways and their Operations-approved capability record. `PaymentAPI` sends no capability token, gateway hint, or provider selection on any create, so the same request works for every SDK version and integration style. Everything stays provider-neutral, so existing integrations change nothing:
90
+ The billing API can process hosted-vault and eligible saved cards through an approved Worldpay Corporate Gateway entirely server-side. Whether a session can use it is the billing API's decision, made from the merchant's active gateways and the saved method's eligibility. `PaymentAPI` sends no gateway hint, provider selection, eligibility assertion, merchant code, or scheme reference. Everything stays provider-neutral, so existing integrations change nothing:
91
91
 
92
92
  - Cards still go through the hosted card form, from the embedded `vault` block or `getVaultCapture()`. There is no Worldpay browser SDK, key, or credential: `gateways.worldpayCorporateGateway` has a `null` publishable key and no enabled methods, and it never appears in the normalized `providers` list.
93
93
  - Outcomes arrive as the same `complete`, `decline`, and `error` events. Declines use FloPay's decline taxonomy, never a raw processor code. An ambiguous submission stays pending until the billing API reconciles it; the hosted form reports that as an `error`, never a success, and a later session read returns the reconciled status.
94
- - Worldpay takes one-time item carts only and offers no 3DS authentication, stored credentials, or card setup; the billing API keeps subscription carts on another card gateway.
94
+ - Cardholder authentication (3DS) resumes through the same provider-neutral continuation as every other card gateway. The hosted form reports `action_required` with a Flo-hosted challenge URL, the adapter renders that URL in its own modal overlay, and the outcome is resolved server-side before the hosted form reports the terminal `complete`, `decline`, or `error`. No Worldpay or authentication-provider script, merchant code, ACS URL, or token reaches the browser.
95
+ - Eligible saved-card Auto, Confirm, returning-buyer, and subscription checkouts call the same provider-neutral `/process` route without `tokenizedData`. Immediate success and authorization use the existing Flo shapes; a `202 checkout_processing` response is reconciled through its nonce-protected status URL, including decline, buyer action, and terminal error states.
96
+ - When a saved method cannot be used, `stored_payment_method_unsupported` is preserved as an actionable Flo error with the instruction to choose another payment method. The SDK never silently switches to a different charging behavior. Scheduled subscription rebills, retries, dunning, and stored-credential eligibility remain billing-API responsibilities.
95
97
 
96
- Auto, Confirm, and pre-tokenized sessions charge a stored payment method, which the Worldpay route does not accept; the billing API keeps those on the released Stripe and PayPal routing by itself. Do not add gateway selection, capability headers, or provider-specific branches to a create request, whether through this SDK or a request you build yourself: the billing API owns that decision, and a client-side copy of it would become a second routing authority that drifts from the merchant's real gateway state.
98
+ `PaymentAPI` Auto creates advertise only the generic `async-auto-checkout-v1` response capability so the billing API may return the asynchronous status envelope. It is not a Worldpay capability or routing request; Confirm and Full creates send no capability header. Do not add gateway selection or provider-specific branches to a create request, whether through this SDK or a request you build yourself: the billing API owns that decision.
97
99
 
98
100
  ### Hosted card capture
99
101
 
100
102
  `flopay.cardCapture()` returns a `CardCaptureAdapter` that injects the billing API's self-contained hosted card form. The form owns the PCI card fields, its own submit button, tokenization, the charge (created and confirmed server-side), 3DS, and the result, so **no Stripe.js runs on the card path** and card numbers never enter the SDK runtime. The SDK injects the form and relays its terminal outcome.
101
103
 
102
104
  - Use the session's embedded `vault` block when present. Otherwise fetch one with `PaymentAPI.getVaultCapture(sessionId, nonce)` for a session whose Stripe `enabledPaymentMethods` explicitly includes `card`, or that advertises `gateways.worldpayCorporateGateway`. `getVaultCapture()` shares one active request between concurrent callers, aborts a stalled attempt after ten seconds, and retries transient network failures on the idempotent `POST /v1/checkouts/sessions/{id}/vault/capture` route.
103
- - Outcome events are `ready`, `complete`, `decline`, and `error`. The form is injected same-window, so the adapter rejects terminal outcomes that are not bound to the mounted session, that mismatch the block's `messageToken`, or that arrive from a different `expectedOrigin`. Pass both from the vault block to `mount()`.
105
+ - Outcome events are `ready`, `complete`, `decline`, and `error`; `challenge_dismissed` is a non-terminal notification that the buyer closed the authentication challenge. The form is injected same-window, so the adapter rejects terminal outcomes that are not bound to the mounted session, that mismatch the block's `messageToken`, or that arrive from a different `expectedOrigin`. Pass both from the vault block to `mount()`.
106
+ - When the buyer's bank asks for authentication, the hosted form hands the challenge up to the adapter, which renders the provider-hosted URL in a full-page modal overlay (`role="dialog"`, `aria-modal`, focus moved into the challenge and restored on teardown) above any processing backdrop. The challenge is not an outcome: it reaches no listener, and the challenge URL never appears in an event or in telemetry. The hosted form resolves the outcome and reports it as the usual `complete`, `decline`, or `error`; if nothing comes back at all, the overlay closes itself after the hosted form's own authentication window and reports an `error`. Closing the overlay abandons the challenge and reports the non-terminal `challenge_dismissed` event, so the host can take down its processing state and leave the form retryable straight away; it is not an outcome, and a late `complete`, `decline`, or `error` from the hosted form still reaches your listeners.
104
107
  - A `complete` event's `intentId` is the processing gateway's own reference. It is a Stripe PaymentIntent (or SetupIntent) id only when Stripe took the card; for a server-side card gateway it may be an opaque order reference. Reconcile by `paymentId` and the session instead.
105
108
  - For a merchant-server-created no-charge setup session, create the adapter with `operation: 'card_setup'`. A `complete` event may then carry a display-only `paymentMethod` (FloPay id, brand, last four, expiry, and status).
106
109
  - `capture.applyTheme(theme)` re-skins the mounted form without remounting; `applyTheme(null)` is the explicit classic reset. Theming is cosmetic and never blocks capture.
@@ -117,7 +120,7 @@ posted only when the vault block supplied both `messageToken` and
117
120
  { source: 'flopay-vault-host', type: 'submitLabel', sessionId, messageToken, label }
118
121
  ```
119
122
 
120
- A form mounted without those fields (an older billing API) keeps its default. The label is never added to telemetry. The other host directives (theme, submit gate, field order, and the 3DS `action_completed` signal) also target the exact `expectedOrigin` when one was supplied, falling back to `'*'` only for a widget mounted without it.
123
+ A form mounted without those fields (an older billing API) keeps its default. The label is never added to telemetry. The other host directives (theme, submit gate, and field order) also target the exact `expectedOrigin` when one was supplied, falling back to `'*'` only for a widget mounted without it. There is no host directive for authentication: the hosted form accepts no completion signal from the page, because the outcome of a challenge is resolved server-side.
121
124
 
122
125
  ### Privacy-safe operational telemetry
123
126
 
@@ -328,7 +331,8 @@ await api.reportSessionIntentDecline('session_uuid', 'session_nonce', {
328
331
  | `claimCheckoutSession(sessionId, nonce, payload)` | `Promise<NormalizedCheckoutSession>` | Sends a `PATCH /v1/checkouts/sessions/{id}/claim` |
329
332
  | `createSessionIntent(sessionId, nonce, request, options?)` | `Promise<SessionIntent>` | Creates a Stripe wallet/APM or direct-PayPal intent |
330
333
  | `reportSessionIntentDecline(sessionId, nonce, request, options?)` | `Promise<void>` | Reports a non-card decline classification |
331
- | `processPayment(userId, params, options?)` | `Promise<Response>` | Submits a saved or approved non-card payment to `/process`, polling while the billing API is still processing |
334
+ | `processPayment(userId, params, options?)` | `Promise<Response>` | Submits a saved or approved non-card payment to `/process`; a `202` follows the advertised authenticated status endpoint and maps terminal attempt outcomes back to the existing process response shapes |
335
+ | `waitForCheckoutSessionCompletion(sessionId, options?)` | `Promise<NormalizedCheckoutSession>` | Waits for terminal state; pass the session `nonce` and a processing response's `statusUrl` to reconcile attempt-level success, authorization, decline, buyer action, and failure |
332
336
  | `getPaymentsByEmail(email, options?)` | `Promise<{ data, total, page, limit }>` | Lists prior payments for an email |
333
337
  | `destroy()` | `void` | Tears down the client's telemetry |
334
338
 
@@ -366,6 +370,7 @@ Failures reject with `FloPayError` from `@flopay/shared`. Branch on `type` and `
366
370
  | `InvalidCheckoutSessionResponse` | `api_error` | The billing API returned a malformed session or no session id. |
367
371
  | `session_auto_completed` | `api_error` | Inline creation found a payment method already on file and completed the session without a checkout. |
368
372
  | `checkout_processing_timeout` | `api_error` | `processPayment()` stopped polling while the checkout was still processing. Read the session again shortly. |
373
+ | `stored_payment_method_unsupported` | `api_error` | The backend-selected saved method cannot be used for this transaction. Ask the buyer to choose another payment method; the SDK does not silently change payment behavior. |
369
374
  | `vault_capture_timeout` | `api_error` | `getVaultCapture()` did not return the hosted card form within ten seconds. |
370
375
  | `card_capture_no_window` | `api_error` | `PciVaultCardCapture.mount()` ran outside a browser. |
371
376
  | `card_capture_no_widget_html` | `api_error` | `PciVaultCardCapture.mount()` was called without hosted form HTML. |
@@ -419,8 +419,21 @@ declare class PaymentAPI {
419
419
  * `GET /v1/checkouts/sessions/:id`. Required by post-#640 backends.
420
420
  */
421
421
  nonce?: string;
422
+ /** Advertised authenticated attempt-status endpoint from a `202` response. */
423
+ statusUrl?: string;
424
+ /**
425
+ * Keep polling after an `ACTION_REQUIRED` attempt. Challenge UIs use this
426
+ * while the backend resolves the buyer's completed action server-side;
427
+ * ordinary callers still receive the action immediately.
428
+ */
429
+ continueAfterActionRequired?: boolean;
422
430
  paymentMethodCategory?: TelemetryPaymentMethodCategory;
431
+ /** Cancels polling when the owning UI lifecycle ends. */
432
+ signal?: AbortSignal;
423
433
  }): Promise<NormalizedCheckoutSession>;
434
+ private resolveCheckoutSessionStatus;
435
+ private waitForCheckoutStatus;
436
+ private fetchCheckoutStatus;
424
437
  /** Normalize a raw session into a provider-agnostic shape. */
425
438
  private normalizeRawSession;
426
439
  /** Convert raw session to the SDK CheckoutSession shape. */
@@ -433,6 +446,7 @@ declare class PaymentAPI {
433
446
  private toVaultBlock;
434
447
  private toCheckoutSessionStatus;
435
448
  private resolveProcessResponse;
449
+ private toProcessResponseFromStatus;
436
450
  private reconcileProcessPayment;
437
451
  private toProcessResponseFromSession;
438
452
  private toCheckoutProcessingPending;
@@ -503,14 +517,21 @@ declare class PciVaultCardCapture implements CardCaptureAdapter {
503
517
  private actionOverlay;
504
518
  /**
505
519
  * Listener that catches the `flopay-vault-3ds-return` postMessage from the
506
- * provider's challenge return page. When the SDK owns the challenge iframe
507
- * the return page lives inside *that* iframe (not the widget's), so
508
- * `window.parent` is the host page — the widget's existing message
509
- * listener can't see it. The SDK forwards completion into the widget via
510
- * `action_completed` so the widget kicks `/3ds/complete` immediately
511
- * instead of waiting on the eventual provider webhook.
520
+ * challenge return page. It is an overlay-dismissal accelerator only: the
521
+ * terminal outcome is resolved server-side on `/vault/3ds/return` and
522
+ * reaches the SDK through the widget's own `/status` poll. There is
523
+ * deliberately no client-side completion bridge back into the widget
524
+ * (TeamFloPay/backend#1674).
512
525
  */
513
526
  private threeDsReturnHandler;
527
+ /**
528
+ * Element that held focus when the challenge overlay opened, restored on
529
+ * teardown. A keyboard or screen-reader buyer would otherwise be stranded on
530
+ * the obscured page behind the modal once the challenge is gone.
531
+ */
532
+ private actionOverlayPreviousFocus;
533
+ /** Bound on how long the challenge overlay may stay up (see above). */
534
+ private actionOverlayTimer;
514
535
  /** Per-session integrity token to require on outcomes (from mount options). */
515
536
  private messageToken;
516
537
  /** Strict origin to require on outcomes, when configured. */
@@ -619,22 +640,39 @@ declare class PciVaultCardCapture implements CardCaptureAdapter {
619
640
  *
620
641
  * The overlay tears down on the next terminal outcome
621
642
  * (`complete`/`decline`/`error`) or when the buyer closes it via the backdrop
622
- * close button. Closing manually is a soft abandon — the next `/status` poll
623
- * either reveals a real outcome (the challenge completed via the issuer's
624
- * own redirect to `/vault/3ds/return`, which posts back into the widget) or
625
- * surfaces `requires_action` again so the host can decide what to do.
643
+ * close button. Closing manually is a soft abandon — it emits the
644
+ * non-terminal `challenge_dismissed` so the host can drop its processing
645
+ * backdrop, while the widget keeps polling `/status` and still owns the
646
+ * eventual outcome (an out-of-band approval can land after the challenge is
647
+ * gone).
626
648
  */
627
649
  private showActionRequiredOverlay;
628
650
  /**
629
- * Tell the vault widget that the buyer has completed (or abandoned) the
630
- * challenge. The widget responds by POSTing `/3ds/complete` — its
631
- * sub-300ms sync resolver writes the follow-up attempt row immediately,
632
- * so the next `/status` poll resolves to a terminal outcome instead of
633
- * waiting for the eventual provider webhook. Best-effort — even if the
634
- * postMessage fails the widget's poll loop will eventually catch the
635
- * terminal state from the provider webhook.
651
+ * (Re)start the overlay bound. Deliberately longer than the widget's own
652
+ * `/status` poll window so the widget's deterministic timeout lands first;
653
+ * this only fires when nothing at all came back.
654
+ */
655
+ private armActionRequiredTimeout;
656
+ /**
657
+ * Report a challenge the buyer never finished. Both ways out of a challenge
658
+ * without an outcome — closing the overlay, or never coming back at all —
659
+ * are the same `customer_abandoned` terminal event.
660
+ */
661
+ private reportChallengeAbandoned;
662
+ /**
663
+ * Tear the challenge down and give the buyer a terminal, provider-neutral
664
+ * error. Unlike a manual close, nothing is left to come back to, so this also
665
+ * ends the attempt for the host.
666
+ */
667
+ private timeOutActionRequiredOverlay;
668
+ /**
669
+ * The buyer closed the challenge. Report it as abandoned and tell the host —
670
+ * `challenge_dismissed` is deliberately NON-terminal: the widget is still
671
+ * polling `/status` and a late `complete`/`decline`/`error` must still be
672
+ * delivered. Without it the host's processing backdrop (raised on
673
+ * `submitting`) would hold the buyer behind a modal for the rest of the
674
+ * widget's authentication window.
636
675
  */
637
- private postActionCompleted;
638
676
  private abandonActionRequiredOverlay;
639
677
  private hideActionRequiredOverlay;
640
678
  /**
@@ -419,8 +419,21 @@ declare class PaymentAPI {
419
419
  * `GET /v1/checkouts/sessions/:id`. Required by post-#640 backends.
420
420
  */
421
421
  nonce?: string;
422
+ /** Advertised authenticated attempt-status endpoint from a `202` response. */
423
+ statusUrl?: string;
424
+ /**
425
+ * Keep polling after an `ACTION_REQUIRED` attempt. Challenge UIs use this
426
+ * while the backend resolves the buyer's completed action server-side;
427
+ * ordinary callers still receive the action immediately.
428
+ */
429
+ continueAfterActionRequired?: boolean;
422
430
  paymentMethodCategory?: TelemetryPaymentMethodCategory;
431
+ /** Cancels polling when the owning UI lifecycle ends. */
432
+ signal?: AbortSignal;
423
433
  }): Promise<NormalizedCheckoutSession>;
434
+ private resolveCheckoutSessionStatus;
435
+ private waitForCheckoutStatus;
436
+ private fetchCheckoutStatus;
424
437
  /** Normalize a raw session into a provider-agnostic shape. */
425
438
  private normalizeRawSession;
426
439
  /** Convert raw session to the SDK CheckoutSession shape. */
@@ -433,6 +446,7 @@ declare class PaymentAPI {
433
446
  private toVaultBlock;
434
447
  private toCheckoutSessionStatus;
435
448
  private resolveProcessResponse;
449
+ private toProcessResponseFromStatus;
436
450
  private reconcileProcessPayment;
437
451
  private toProcessResponseFromSession;
438
452
  private toCheckoutProcessingPending;
@@ -503,14 +517,21 @@ declare class PciVaultCardCapture implements CardCaptureAdapter {
503
517
  private actionOverlay;
504
518
  /**
505
519
  * Listener that catches the `flopay-vault-3ds-return` postMessage from the
506
- * provider's challenge return page. When the SDK owns the challenge iframe
507
- * the return page lives inside *that* iframe (not the widget's), so
508
- * `window.parent` is the host page — the widget's existing message
509
- * listener can't see it. The SDK forwards completion into the widget via
510
- * `action_completed` so the widget kicks `/3ds/complete` immediately
511
- * instead of waiting on the eventual provider webhook.
520
+ * challenge return page. It is an overlay-dismissal accelerator only: the
521
+ * terminal outcome is resolved server-side on `/vault/3ds/return` and
522
+ * reaches the SDK through the widget's own `/status` poll. There is
523
+ * deliberately no client-side completion bridge back into the widget
524
+ * (TeamFloPay/backend#1674).
512
525
  */
513
526
  private threeDsReturnHandler;
527
+ /**
528
+ * Element that held focus when the challenge overlay opened, restored on
529
+ * teardown. A keyboard or screen-reader buyer would otherwise be stranded on
530
+ * the obscured page behind the modal once the challenge is gone.
531
+ */
532
+ private actionOverlayPreviousFocus;
533
+ /** Bound on how long the challenge overlay may stay up (see above). */
534
+ private actionOverlayTimer;
514
535
  /** Per-session integrity token to require on outcomes (from mount options). */
515
536
  private messageToken;
516
537
  /** Strict origin to require on outcomes, when configured. */
@@ -619,22 +640,39 @@ declare class PciVaultCardCapture implements CardCaptureAdapter {
619
640
  *
620
641
  * The overlay tears down on the next terminal outcome
621
642
  * (`complete`/`decline`/`error`) or when the buyer closes it via the backdrop
622
- * close button. Closing manually is a soft abandon — the next `/status` poll
623
- * either reveals a real outcome (the challenge completed via the issuer's
624
- * own redirect to `/vault/3ds/return`, which posts back into the widget) or
625
- * surfaces `requires_action` again so the host can decide what to do.
643
+ * close button. Closing manually is a soft abandon — it emits the
644
+ * non-terminal `challenge_dismissed` so the host can drop its processing
645
+ * backdrop, while the widget keeps polling `/status` and still owns the
646
+ * eventual outcome (an out-of-band approval can land after the challenge is
647
+ * gone).
626
648
  */
627
649
  private showActionRequiredOverlay;
628
650
  /**
629
- * Tell the vault widget that the buyer has completed (or abandoned) the
630
- * challenge. The widget responds by POSTing `/3ds/complete` — its
631
- * sub-300ms sync resolver writes the follow-up attempt row immediately,
632
- * so the next `/status` poll resolves to a terminal outcome instead of
633
- * waiting for the eventual provider webhook. Best-effort — even if the
634
- * postMessage fails the widget's poll loop will eventually catch the
635
- * terminal state from the provider webhook.
651
+ * (Re)start the overlay bound. Deliberately longer than the widget's own
652
+ * `/status` poll window so the widget's deterministic timeout lands first;
653
+ * this only fires when nothing at all came back.
654
+ */
655
+ private armActionRequiredTimeout;
656
+ /**
657
+ * Report a challenge the buyer never finished. Both ways out of a challenge
658
+ * without an outcome — closing the overlay, or never coming back at all —
659
+ * are the same `customer_abandoned` terminal event.
660
+ */
661
+ private reportChallengeAbandoned;
662
+ /**
663
+ * Tear the challenge down and give the buyer a terminal, provider-neutral
664
+ * error. Unlike a manual close, nothing is left to come back to, so this also
665
+ * ends the attempt for the host.
666
+ */
667
+ private timeOutActionRequiredOverlay;
668
+ /**
669
+ * The buyer closed the challenge. Report it as abandoned and tell the host —
670
+ * `challenge_dismissed` is deliberately NON-terminal: the widget is still
671
+ * polling `/status` and a late `complete`/`decline`/`error` must still be
672
+ * delivered. Without it the host's processing backdrop (raised on
673
+ * `submitting`) would hold the buyer behind a modal for the rest of the
674
+ * widget's authentication window.
636
675
  */
637
- private postActionCompleted;
638
676
  private abandonActionRequiredOverlay;
639
677
  private hideActionRequiredOverlay;
640
678
  /**