@flopay/shared 1.9.16 → 1.9.17

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
@@ -1,6 +1,6 @@
1
1
  # @flopay/shared
2
2
 
3
- Shared types, error classes, constants, and validation helpers used across all FloPay packages. This package contains no runtime logic beyond error factories and validation functions -- it is primarily a type/constant library.
3
+ Shared types, error classes, constants, and validation helpers used by every FloPay SDK package. It is primarily a type and constant library: apart from error factories, validation helpers, and the privacy-safe telemetry builders it has no runtime behavior of its own.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,295 +8,347 @@ Shared types, error classes, constants, and validation helpers used across all F
8
8
  pnpm add @flopay/shared
9
9
  ```
10
10
 
11
- Within the monorepo, packages depend on it via `workspace:*`.
11
+ `@flopay/js`, `@flopay/react`, and `@flopay/node` already depend on this package, so add it directly only when your own code imports from it. Install the same version as the other `@flopay/*` packages you use.
12
12
 
13
- ## Quick Start
13
+ Its one runtime dependency is [`validator`](https://www.npmjs.com/package/validator), used only by the postal-code helpers. It is imported through the `validator/lib/isPostalCode.js` submodule so bundlers include just that check, and it installs automatically.
14
14
 
15
- ### Environment Configuration
15
+ ## Usage
16
+
17
+ ### Configure the billing API environment
18
+
19
+ Choose which FloPay billing API the SDK talks to, once, at application startup. Either set the `NEXT_PUBLIC_FLOPAY_ENV` environment variable to `production` or `staging` (no code needed), or call `configureFlopay()`:
16
20
 
17
21
  ```ts
18
22
  import { configureFlopay } from '@flopay/shared';
19
23
 
20
- // Call once at app startup — determines which billing API URL is used
21
- configureFlopay({ environment: 'production' }); // or 'staging'
24
+ configureFlopay({ environment: 'production' });
25
+ ```
26
+
27
+ `resolveBillingApiUrl()` returns the URL every FloPay package uses. It takes, in order: an explicit URL argument, the `NEXT_PUBLIC_FLOPAY_ENV` environment variable, the `configureFlopay()` setting, and finally the staging billing API.
28
+
29
+ ### Pick a theme
30
+
31
+ `THEMES` maps each bundle id to a coherent pair: an `appearance` for Stripe-rendered elements and the hosted card form, and `buttonsLayout` styles for the React checkout's own surfaces. The React components accept the same ids through their `theme` prop, so you rarely need the bundle directly.
22
32
 
23
- // Or set NEXT_PUBLIC_FLOPAY_ENV=production in your .env file (no code needed)
33
+ ```ts
34
+ import { THEMES } from '@flopay/shared';
35
+
36
+ const { appearance, buttonsLayout } = THEMES['glass-dark'];
24
37
  ```
25
38
 
26
- ### Resolve Billing API URL
39
+ ### Validate buyer input
40
+
41
+ `isValidEmail()` mirrors the billing API's email rule. It returns `false` for nullish, blank, padded, or malformed values, so trim input yourself when surrounding whitespace is acceptable.
42
+
43
+ The postal-code helpers are a faithful mirror of the billing API's own postcode validator: the same pinned `validator` `isPostalCode` rules, the same country normalization, and the same fail-open cases. The SDK therefore never blocks a postcode the server accepts, and never opens a submit gate for one the server's account update will reject. `@flopay/react` uses them to block a malformed AVS postcode before the card is captured and to show the expected format inline.
44
+
45
+ - A **2-letter code** is passed straight through, so `'UK'` (not a `validator` locale) is unsupported, while the full name `'United Kingdom'` resolves to `GB`.
46
+ - `isValidPostalCode()` **fails open** in exactly the billing API's three cases: a blank postcode, an unresolvable country, or a country `validator` has no rule for. Check emptiness and `isPostalCodeSupported()` yourself to tell "required" apart from "malformed".
47
+ - `getPostalCodeExample()` returns the same example the billing API embeds in its validation message, so an inline hint and a server rejection never contradict each other.
48
+
49
+ ### Build your own checkout-create request
50
+
51
+ `@flopay/js` and `@flopay/react` handle this automatically. Use the helpers only if you build the `POST /v1/checkouts/sessions` request yourself for a checkout this SDK renders.
52
+
53
+ - **Idempotency.** Send a stable `Idempotency-Key` so a timeout or lost response cannot mint a second session. `resolveIdempotencyKey()` returns your key unchanged after validating it, or generates a cryptographically random one (`crypto.randomUUID()`, else 16 random bytes). Without a secure random source it returns `undefined` and you should omit the header rather than send a weak key. Keys are never derived from request data. Reuse one key for every retry of one logical checkout, and never for a new purchase. The header is optional: omitting it keeps the billing API's unkeyed behavior.
54
+
55
+ ### Gateway decisions belong to the billing API
56
+
57
+ `@flopay/shared` exposes no helper for declaring, selecting, or filtering payment gateways or gateway capabilities, and none should be added. Which gateway processes a session (Stripe, PayPal, Worldpay Corporate Gateway, or a future provider) is decided by the billing API from the merchant's configured gateways and their approved capability records. Clients that build `POST /v1/checkouts/sessions` themselves send the same provider-neutral body the SDK sends and render whatever `gateways` the response advertises.
58
+
59
+ ### Privacy-safe telemetry contract
60
+
61
+ `@flopay/shared` exports the versioned, closed telemetry types and builders used by both browser packages. Builders copy only allowlisted enum, numeric, and opaque fields: arbitrary messages, stacks, payloads, URLs, credentials, customer data, and unknown nested fields cannot enter serialized events. `serializeTelemetryBatch()` emits the billing API's v1 envelope with a fixed, closed package identity (`@flopay/shared`, `@flopay/js`, or `@flopay/react`).
62
+
63
+ - `TELEMETRY_LOG_NAMES` is the complete SDK-authored lifecycle catalog, and `TELEMETRY_FAILURE_CATEGORIES` the complete technical-failure catalog: `server_error`, `transport_error`, `invalid_response`, and `provider_runtime`. Only `buildTelemetryErrorEvent()` accepts a `failureCategory`; every other builder drops it.
64
+ - PayPal lifecycle events may carry the closed `inAppBrowser`, `appFamily`, `popupCapability`, `presentationMode`, and `terminalReason` dimensions. Invalid values and raw user-agent or provider fields are dropped.
65
+ - Detached session creation is measured as two stages: `session_shell` (create to a mountable card form) and `session_claim` (the background attachment of catalog and buyer data). A successful `session_create` still spans the whole logical create, so end-to-end measurements are unchanged. Claim failures are reported only under `session_claim`.
66
+
67
+ ### Filter third-party errors in your own Sentry client
68
+
69
+ Hosts that already use Sentry can install the dependency-free predicate as their `beforeSend` callback:
27
70
 
28
71
  ```ts
29
- import { resolveBillingApiUrl } from '@flopay/shared';
72
+ import * as Sentry from '@sentry/browser';
73
+ import { dropThirdPartyOnlyError } from '@flopay/shared';
30
74
 
31
- // Reads from: explicit param → env var → configureFlopay() → staging fallback
32
- const url = resolveBillingApiUrl();
75
+ Sentry.init({ beforeSend: dropThirdPartyOnlyError });
33
76
  ```
34
77
 
35
- ### Other Imports
78
+ It returns `null` only when every exception value has a non-empty stack, no frame resolves to app code or an `@flopay/*` module, and at least one frame resolves to a browser-extension URL or Stripe's hosted `dahlia/stripe.js` controller. Mixed, unrelated-CDN, incomplete, anonymous-only, and genuine app errors are returned unchanged. Frame provenance comes from paths and module names; Sentry's `in_app` flag is ignored because host source-map rewriting can mark third-party frames as app-owned. The helper is structurally typed (`SentryEventLike`), so FloPay adds no `@sentry/*` dependency.
79
+
80
+ ## Examples
81
+
82
+ Configure the environment and resolve the billing API URL:
83
+
84
+ ```ts
85
+ import { configureFlopay, getFloPayEnvironment, resolveBillingApiUrl } from '@flopay/shared';
86
+
87
+ configureFlopay({ environment: 'production' });
88
+
89
+ console.log(getFloPayEnvironment()); // 'production'
90
+ console.log(resolveBillingApiUrl()); // the production billing API, unless NEXT_PUBLIC_FLOPAY_ENV overrides it
91
+ console.log(resolveBillingApiUrl('https://billing.example.com')); // an explicit URL always wins
92
+ ```
93
+
94
+ Validate buyer input with the same rules as the billing API:
36
95
 
37
96
  ```ts
38
97
  import {
39
- FloPayError,
40
- validationError,
41
- SDK_VERSION,
42
- DEFAULT_APPEARANCE,
43
- NIGHT_APPEARANCE,
44
- THEMES,
45
- CURRENCY_MAP,
46
98
  getCurrencyByCountry,
47
- isCardHolderNamePlaceholder,
48
- isValidPublishableKey,
99
+ getPostalCodeExample,
100
+ isPostalCodeSupported,
49
101
  isValidEmail,
102
+ isValidPostalCode,
103
+ isValidPublishableKey,
50
104
  } from '@flopay/shared';
51
105
 
52
- // Pick a coherent theme bundle and pass both halves to your checkout
53
- const { appearance, buttonsLayout } = THEMES['glass-dark'];
106
+ isValidEmail('buyer@example.com'); // true
107
+ isValidEmail(' buyer@example.com '); // false: trim first when whitespace is acceptable
108
+ isValidPublishableKey('pk_test_abc123'); // true
109
+
110
+ isPostalCodeSupported('US'); // true
111
+ isPostalCodeSupported('AE'); // false: the UAE has no postcodes
112
+ isPostalCodeSupported('United Kingdom'); // true: full names resolve to GB
113
+ isPostalCodeSupported('UK'); // false: 2-letter codes pass straight through
114
+ isValidPostalCode('GB', 'SW1A 1AA'); // true
115
+ isValidPostalCode('GB', '12345'); // false: a US shape in GB
116
+ isValidPostalCode('US', ''); // true: blank fails open, so enforce "required" separately
117
+ getPostalCodeExample('CA'); // 'A1A 1A1'
118
+
119
+ const { currency, symbol } = getCurrencyByCountry('DE'); // 'EUR', '€'; unknown countries fall back to USD
120
+ console.log(currency, symbol);
121
+ ```
122
+
123
+ Build a checkout-create request yourself with the same idempotency header the SDK sends:
54
124
 
55
- import type {
56
- FloPayConfig,
57
- FloPayEnvironment,
58
- CheckoutSession,
59
- PaymentResult,
60
- PaymentProviderAdapter,
61
- FloPayAppearance,
125
+ ```ts
126
+ import {
127
+ IDEMPOTENCY_KEY_HEADER,
128
+ type InlineSessionDraft,
129
+ resolveBillingApiUrl,
130
+ resolveIdempotencyKey,
62
131
  } from '@flopay/shared';
63
132
 
64
- // Validate a key
65
- isValidPublishableKey('pk_test_abc123'); // true
66
- isValidEmail('buyer@example.com'); // true
67
- isValidEmail(' buyer@example.com '); // false — callers trim intentionally
133
+ const draft: InlineSessionDraft = {
134
+ clientId: 'your-client-id',
135
+ currency: 'EUR',
136
+ items: [{ providerItemId: 'product-1', providerItemName: 'Widget', totalAmount: 29.99 }],
137
+ account: { userId: 'user_1' },
138
+ successUrl: 'https://shop.example.com/success',
139
+ cancelUrl: 'https://shop.example.com/cancel',
140
+ };
68
141
 
69
- // Look up currency by country
70
- const info = getCurrencyByCountry('DE');
71
- // { currency: 'EUR', symbol: '\u20ac', country: 'Germany', countryCode: 'DE', tax: 1 }
142
+ const idempotencyKey = resolveIdempotencyKey('checkout:order-123');
143
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
144
+ if (idempotencyKey) headers[IDEMPOTENCY_KEY_HEADER] = idempotencyKey;
72
145
 
73
- // Create a structured error
74
- throw validationError('Email is required', 'email');
146
+ console.log(`${resolveBillingApiUrl()}/v1/checkouts/sessions`, headers, JSON.stringify(draft));
75
147
  ```
76
148
 
77
- ## API Reference
149
+ Handle SDK errors by their stable `type` and `code`:
150
+
151
+ ```ts
152
+ import { FloPayError, resolveIdempotencyKey } from '@flopay/shared';
153
+
154
+ try {
155
+ resolveIdempotencyKey('');
156
+ } catch (error) {
157
+ if (error instanceof FloPayError && error.type === 'validation_error') {
158
+ console.warn(error.code, error.param); // 'InvalidIdempotencyKey' 'idempotencyKey'
159
+ }
160
+ }
161
+ ```
162
+
163
+ ## Public API
78
164
 
79
165
  ### Types
80
166
 
81
167
  | Type | Description |
82
168
  |------|-------------|
83
169
  | `FloPayConfig` | Top-level config: `publishableKey`, `locale?`, `appearance?`, `apiVersion?`, `telemetry?` (set `false` to opt out of browser operational telemetry) |
170
+ | `FloPayEnvironment` | Billing API environment accepted by `configureFlopay()`: `'staging'` / `'production'` |
84
171
  | `FloPayAppearance` | Theme config: `theme` (`'default'`/`'flat'`/`'night'`/`'none'`), `variables?`, `rules?` |
85
172
  | `FloPayThemeVariables` | CSS custom property overrides: `colorPrimary`, `colorBackground`, `colorText`, `colorDanger`, `borderRadius`, `fontFamily`, `fontSizeBase`, `spacingUnit` |
86
- | `ButtonsLayoutStyles` | Style overrides for the `buttons` layout: `cardButton`, `cardButtonFontSize`, `cardFormContainer`, `cardInputBorder`, `cardInputColor`, `cardInputPlaceholderColor`, `cardInputFontSize`, `cardInputBackground`, `nameInput`, `backButton`, `backButtonFontSize`, `backButtonIcon`, `submitButton`, `submitButtonFontSize`, `title`, `titleFontSize`, `errorBanner` |
87
- | `ButtonsLayoutTheme` | Theme preset name: `'default'` / `'minimal'` / `'rounded'` / `'dark'` / `'modern-light'` / `'modern-dark'` / `'bold-light'` / `'bold-dark'` / `'glass-light'` / `'glass-dark'` |
173
+ | `ButtonsLayoutStyles` | Style overrides for the React checkout's `layout="buttons"` surface: `cardButton`, `cardButtonFontSize`, `cardFormContainer`, `cardInputBorder`, `cardInputColor`, `cardInputPlaceholderColor`, `cardInputFontSize`, `cardInputBackground`, `nameInput`, `backButton`, `backButtonFontSize`, `backButtonIcon`, `submitButton`, `submitButtonFontSize`, `title`, `titleFontSize`, `errorBanner` |
174
+ | `ButtonsLayoutTheme` | Legacy buttons-layout preset name: `'default'` / `'minimal'` / `'rounded'` / `'dark'` / `'modern-light'` / `'modern-dark'` / `'bold-light'` / `'bold-dark'` / `'glass-light'` / `'glass-dark'` |
88
175
  | `ThemeBundle` | A coherent theme bundle: `{ appearance: FloPayAppearance, buttonsLayout: ButtonsLayoutStyles }` |
89
176
  | `ThemeBundleId` | Bundle id: `'modern-light'` / `'modern-dark'` / `'bold-light'` / `'bold-dark'` / `'glass-light'` / `'glass-dark'` |
90
- | `ThemeId` | Accepted by the React components' `theme` prop: `'classic'` ∪ `ThemeBundleId` |
91
- | `VaultCardThemeColors` | Theme directive the SDK posts to the hosted vault card form: the flat keys every hosted asset understands (`primaryColor`, `primaryHoverColor`, `inputBackgroundColor`, `textColor`, `borderColor`, `placeholderColor`, `errorColor`, `successColor`, `fontFamily`, `fontSize`, `fontWeight`, `borderRadius`) plus the optional `submitButton` snapshot. Partial objects stay valid and are forwarded unchanged. |
92
- | `VaultSubmitButtonTheme` | Complete, bounded snapshot for the backend-hosted submit control (billing API v1.9.6+, TeamFloPay/backend#1448): `foregroundColor`, `background` / `hoverBackground` / `disabledBackground` (`VaultSubmitBackground`), `typography` (`VaultSubmitTypography`), `spacing` (`VaultSubmitSpacing`), `shadow` (`VaultShadowLayer[]`, zero to four layers), `borderRadius`, `disabledOpacity`, `backdropBlur`. Structured values only — never raw CSS; the hosted asset ignores an invalid or out-of-range member and theme input never blocks capture. |
93
- | `VaultSubmitBackground` | `VaultSolidBackground` (`{ type: 'solid', color }`) or `VaultLinearGradientBackground` (`{ type: 'linearGradient', angle: 0..360, stops: VaultGradientStop[] }` with two to five `{ color, position: 0..100 }` stops in non-decreasing order). Colors are `VaultThemeColor`: `#RRGGBB`, integer-channel `rgb()`, or `rgba()` with alpha `0..1`. |
94
- | `CardCaptureAdapter` | Hosted card-capture abstraction the React card surfaces depend on: `mount()`, `on()`, `unmount()`, plus optional live controls `applyTheme(theme \| null)` (`null` is the explicit classic reset the hosted form restores its historical neutral values from), `setSubmitGate()`, `setCardFieldOrder()`, `setSubmitLabel(label \| null)` (the hosted card submit wording, billing API v1.9.35+; `null` resets to the session default, `CONFIRM PAYMENT` or `ADD CARD`; retained and replayed on every `ready`, and posted only to the vault block's exact `expectedOrigin` with its `messageToken`). `CardCaptureMountOptions.submitLabel` sets the initial wording. |
177
+ | `ThemeId` | Accepted by the React components' `theme` prop: `'classic'` or any `ThemeBundleId` |
178
+ | `VaultCardThemeColors` | Theme directive sent to the hosted card form: the flat keys every hosted form understands (`primaryColor`, `primaryHoverColor`, `inputBackgroundColor`, `textColor`, `borderColor`, `placeholderColor`, `errorColor`, `successColor`, `fontFamily`, `fontSize`, `fontWeight`, `borderRadius`) plus the optional `submitButton` snapshot. Partial objects stay valid and are forwarded unchanged. |
179
+ | `VaultSubmitButtonTheme` | Complete, bounded snapshot for the hosted submit control (billing API v1.9.6+): `foregroundColor`, `background` / `hoverBackground` / `disabledBackground` (`VaultSubmitBackground`), `typography` (`VaultSubmitTypography`), `spacing` (`VaultSubmitSpacing`), `shadow` (zero to four `VaultShadowLayer` values), `borderRadius`, `disabledOpacity`, `backdropBlur`. Structured values only, never raw CSS: the hosted form ignores an invalid or out-of-range member, and theme input never blocks capture. |
180
+ | `VaultSubmitBackground` | `VaultSolidBackground` (`{ type: 'solid', color }`) or `VaultLinearGradientBackground` (`{ type: 'linearGradient', angle, stops }` with two to five `VaultGradientStop` values in non-decreasing order). Colors are `VaultThemeColor`: `#RRGGBB`, integer-channel `rgb()`, or `rgba()` with alpha `0..1`. |
181
+ | `CardCaptureAdapter` | Hosted card-capture abstraction the React card surfaces depend on: `mount()`, `on()`, `unmount()`, plus the optional live controls `applyTheme(theme \| null)` (`null` is the explicit classic reset), `setSubmitGate()`, `setCardFieldOrder()`, and `setSubmitLabel(label \| null)` (hosted submit wording, billing API v1.9.35+; `null` resets to the session default, `CONFIRM PAYMENT` or `ADD CARD`; retained and replayed on every `'ready'` event, and posted only to the exact `VaultCaptureBlock.expectedOrigin` with its `VaultCaptureBlock.messageToken`). `CardCaptureMountOptions.submitLabel` sets the initial wording. |
95
182
  | `CheckoutMode` | Checkout mode: `'full'` / `'auto'` / `'confirm'` |
96
- | `CheckoutSessionMode` | Mode accepted on session reads: `CheckoutMode` plus `'setup'`. Browser session-creation types deliberately continue to reject `'setup'`. |
97
- | `CheckoutMetadata` | Caller-owned checkout snapshot: `Record<string, string>`. Creation accepts this shape or `null`; session reads contain the map only when the backend returns it. |
98
- | `CheckoutSession` | Session object: `id`, `clientSecret`, `mode`, `status` (`open`/`authorized`/`complete`/`expired`), `amount`, `currency`, `captureMethod?`, `paymentId?`, `authorizationExpiresAt?`, `failureOutcome?`, `customer?`, `checkoutMode?`, `checkoutMetadata?`, `buyerIdentified?` (false for a catalog-attached anonymous session), `vault?` (hosted card widget), `products?` (unified `CheckoutSessionProduct[]`), `successUrl?`, `cancelUrl?`, `coupons?`, `subtotalAmount?` / `discountAmount?` / `totalAmount?` (cart-currency major units; populated by billing API ≥ v1.1.2), `gateways?` (map keyed by gateway code — `stripe`, `paypal`, …), `accountData?`, `tagsData?` |
99
- | `SavedCardDisplay` | Display-only saved-card DTO: FloPay `id`, `brand`, `lastFour`, `expiryMonth`, `expiryYear`, and lifecycle `status`. It contains no reusable credential. |
100
- | `PaymentMethodRemovalError` | Merchant-server removal union covering indistinguishable `404 payment_method_not_found`, the three stable `409` conflict codes, and retry-safe `502 payment_method_deletion_failed` guidance. |
101
- | `CheckoutSessionProduct` | Unified product from session response: `uuid`, `checkoutSessionId`, `type` (`'item'`/`'subscription'`), `code?`, `name?`, `description?`, `quantity`, `totalAmount?`, `overrideAmount?`, `currency?`, `metadata?` |
183
+ | `CheckoutSessionMode` | Mode accepted on session reads: any `CheckoutMode` plus `'setup'`. Browser session-creation types deliberately reject `'setup'`. |
184
+ | `CheckoutMetadata` | Caller-owned checkout snapshot: `Record<string, string>`. Creation accepts this shape or `null`; session reads contain the map only when the billing API returns it. |
185
+ | `CheckoutSession` | Session object: `id`, `clientSecret`, `mode`, `status` (`open`/`authorized`/`complete`/`expired`), `amount`, `currency`, `captureMethod?`, `paymentId?`, `authorizationExpiresAt?`, `failureOutcome?`, `customer?`, `checkoutMode?`, `checkoutMetadata?`, `buyerIdentified?` (false for a catalog-attached anonymous session), `vault?` (the hosted card form), `products?`, `successUrl?`, `cancelUrl?`, `coupons?`, `subtotalAmount?` / `discountAmount?` / `totalAmount?` (cart-currency major units), `gateways?` (map keyed by gateway code), `accountData?`, `tagsData?` |
186
+ | `SavedCardDisplay` | Display-only saved card: FloPay `id`, `brand`, `lastFour`, `expiryMonth`, `expiryYear`, and lifecycle `status`. It contains no reusable credential. |
187
+ | `CheckoutSessionProduct` | Product from a session response: `uuid`, `checkoutSessionId`, `type` (`'item'`/`'subscription'`), `code?`, `name?`, `description?`, `quantity`, `totalAmount?`, `overrideAmount?`, `currency?`, `metadata?` |
102
188
  | `CheckoutProductType` | `'item'` / `'subscription'` |
103
- | `CheckoutProduct` | Unified product input for session creation: `type`, `code?`, `name?`, `quantity?`, `totalAmount?`, `overrideAmount?`, `currency?`, `metadata?` |
104
- | `CheckoutGateway` | Per-gateway config from the billing API. `publishableKey?` identifies the keyed gateway, including the direct-PayPal client ID at `gateways.paypal.publishableKey`. `paypalPublishableKey?` is distinct and Stripe-only: it is the dedicated Stripe sub-account key used to render PayPal through Stripe Elements. Also includes `environment?` (`'sandbox'`/`'live'`), `stripeClientSecret?` (Stripe-only), `providerObjectType?` (direct PayPal: `'order'`/`'subscription'`/`'setup_token'`), bounded `approvalPresentation?` (direct PayPal: `'app_switch'`/`'redirect'`), and `enabledPaymentMethods?` (Stripe-only). |
105
- | `CheckoutGateways` | Map keyed by gateway code (e.g. `stripe`, `paypal`). A session can advertise multiple concurrent gateways. A session opted into server-owned card routing may also carry `worldpayCorporateGateway` (billing API, TeamFloPay/backend#1563): a card gateway processed entirely server-side behind the hosted vault form, with `publishableKey: null`, empty `enabledPaymentMethods`, and no browser credentials. Merchant code never needs to branch on it. |
106
- | `GatewayEnvironment` | `'sandbox'` / `'live'` — drives the PayPal JS SDK script and any other environment-scoped behavior |
189
+ | `CheckoutProduct` | Product input for session creation: `type?`, `code?`, `name?`, `quantity?`, `totalAmount?`, `overrideAmount?`, `currency?`, `metadata?` |
190
+ | `CheckoutGateway` | Per-gateway config from the billing API. `publishableKey?` identifies the keyed gateway, including the direct-PayPal client ID. `paypalPublishableKey?` is Stripe-only: the dedicated Stripe account key used to render PayPal through Stripe Elements. Also `environment?` (`'sandbox'`/`'live'`), `stripeClientSecret?` (Stripe-only), `providerObjectType?` (direct PayPal: `'order'`/`'subscription'`/`'setup_token'`), `approvalPresentation?` (direct PayPal: `'app_switch'`/`'redirect'`), and `enabledPaymentMethods?` (Stripe-only). |
191
+ | `CheckoutGateways` | Map keyed by gateway code (`stripe`, `paypal`). A session can advertise several gateways at once. A session opted into server-owned card routing may also carry `worldpayCorporateGateway`: a card gateway processed entirely server-side behind the hosted card form, with no browser credentials. Merchant code never needs to branch on it. |
192
+ | `GatewayEnvironment` | `'sandbox'` / `'live'`, which selects the PayPal JS SDK script and other environment-scoped behavior |
107
193
  | `PaymentResult` | Payment outcome: `status` (`'authorized'`/`'succeeded'`/`'processing'`/`'requires_action'`/`'failed'`); authorized results carry `paymentId?`, `sessionId?`, and `authorizationExpiresAt?` |
108
194
  | `CheckoutFailureOutcome` | Stable authorization lifecycle failure: `'authorization_declined'` / `'authentication_required'` / `'authorization_expired'` / `'capture_failed'` |
109
195
  | `PayPalPaymentResult` | Stripe-hosted PayPal outcome: `status`, `paymentIntentId?`, `paymentMethodId?`, `error?` |
110
- | `CreateSessionIntentRequest` | Discriminated Stripe wallet/APM or direct-PayPal intent request. PayPal sends `intentKind: 'payment'` for Orders/subscriptions and `'setup'` for a standalone setup token. Direct card is not representable. |
111
- | `SessionIntent` | Matching discriminated response. Stripe carries `clientSecret`; PayPal carries a server-derived `providerObjectType` (`'order'`/`'subscription'`/`'setup_token'`). Provider, payment-method category/type/id, intent kind, and provider object ID are kept as separate fields. |
112
- | `PayPalProviderObjectType` | `'order'` / `'subscription'` / `'setup_token'` — the direct-PayPal continuation selected by the backend and verified by the SDK. |
113
- | `PayPalApprovalPresentation` | `'app_switch'` / `'redirect'` — a backend-advertised one-time Order mode; it carries no return URL or resume credential. |
196
+ | `CreateSessionIntentRequest` | Discriminated Stripe wallet/APM or direct-PayPal intent request. PayPal sends `intentKind: 'payment'` for Orders and subscriptions and `'setup'` for a standalone setup token. Direct card is not representable. |
197
+ | `SessionIntent` | Matching discriminated response. Stripe carries `clientSecret`; PayPal carries a server-derived `providerObjectType`. Provider, payment-method category, type, and id, intent kind, and provider object id are separate fields. |
198
+ | `PayPalProviderObjectType` | `'order'` / `'subscription'` / `'setup_token'`: the direct-PayPal continuation selected by the billing API and verified by the SDK |
199
+ | `PayPalApprovalPresentation` | `'app_switch'` / `'redirect'`: a billing-API-advertised, one-time Order mode. It carries no return URL or resume credential. |
114
200
  | `SessionIntentDeclineRequest` | Non-card provider decline classification. Runtime reporting accepts safe classification codes only and rejects messages or identifier-shaped values. |
115
201
  | `PaymentProviderAdapter` | Minimal provider interface for initialization, raw-provider access, and lifecycle management. Card collection uses `CardCaptureAdapter`; React owns live Stripe Elements directly. |
116
202
  | `Customer` | `id`, `email`, `firstName?`, `lastName?`, `gender?`, `city?`, `state?`, `country?`, `zip?` |
117
203
  | `LineItem` | `price?`, `priceData?`, `quantity` |
118
204
  | `PriceData` | `currency`, `unitAmount`, `productData`, `recurring?` |
119
205
  | `RecurringInterval` | `interval` (`'month'`/`'year'`), `intervalCount?` |
120
- | `BillingProvider` | `'recurly'` / `'chargebee'` / `'stripe'` |
121
- | `TokenizedBody` | `id?`, `type?`, `threeDSecureActionResultTokenId?`, `originalPaymentMethodId?`, `gateway?` (`'stripe'` / `'paypal'` / custom), `paymentMethodType?` (Stripe wire identifier — `'card'`, `'apple_pay'`, `'cashapp'`, `'klarna'`, … for `gateway='stripe'`; `'paypal'` for `gateway='paypal'`), `isPaypal?` *(deprecated — replaced by `gateway` + `paymentMethodType`; while populated for PayPal compatibility, the SDK emits the boolean `true`, never the string `"true"`)* |
122
- | `NormalizedCheckoutSession` | Provider-agnostic session: `provider`, `mode`, `data`, `checkoutMetadata?`, `raw?`. The snapshot is also exposed on `data.session.checkoutMetadata?`. |
206
+ | `BillingProvider` | Gateway code advertised for a session: `'stripe'` / `'paypal'` |
207
+ | `TokenizedBody` | `id?`, `type?`, `threeDSecureActionResultTokenId?`, `originalPaymentMethodId?`, `gateway?`, `paymentMethodType?` (the gateway's wire identifier, such as `'card'` or `'apple_pay'` for Stripe and `'paypal'` for PayPal), `isPaypal?` (deprecated in favor of `gateway` and `paymentMethodType`; still populated as the boolean `true` for PayPal) |
208
+ | `NormalizedCheckoutSession` | Provider-agnostic session: `providers` (every advertised gateway code), `mode`, `data`, `checkoutMetadata?`, `raw?`. The session itself is `data.session`, which also exposes `data.session.checkoutMetadata`. |
123
209
  | `CheckoutModeKind` | `'tokenize'` / `'redirect'` |
124
- | `CreateSessionParams` | Full session creation params: `billingApiUrl`, `checkoutBaseUrl`, `clientId`, `currency` (required — SDK throws `FloPayError({ type: 'validation_error', code: 'CurrencyRequired' })` before issuing the request when none can be resolved), `captureMethod?` (`'automatic'`/`'manual'`; omitted preserves immediate capture), `products?` (unified shape; takes precedence), `items?`, `subscriptions?` (legacy — folded into `products[]` before send), `account`, `successUrl`, `cancelUrl`, `checkoutMode?`, `checkoutMetadata?: CheckoutMetadata \| null`, `couponCodes?`, `tagsData?`, `redirectParams?`, `setCookie?`, `timeoutMs?`, `utmMetadata?` |
125
- | `CheckoutSessionResult` | `{ status: 201; redirectUrl; nonce }` / `{ status: 204 }` / `{ status: number }` (`nonce` is the session-bound checkout token returned by the billing API — forward it on every continuation call against the same session via `x-checkout-session-token`) |
210
+ | `CreateSessionParams` | Full session-creation params: `billingApiUrl`, `checkoutBaseUrl`, `clientId`, `currency` (required, or resolvable from the first item, subscription, or product), `captureMethod?` (`'automatic'`/`'manual'`; omitted keeps immediate capture), `products?` (takes precedence), `items?`, `subscriptions?` (legacy, folded into products before sending), `account`, `successUrl`, `cancelUrl`, `checkoutMode?`, `checkoutMetadata?`, `couponCodes?`, `tagsData?`, `redirectParams?`, `setCookie?`, `timeoutMs?`, `utmMetadata?`, `idempotencyKey?` |
211
+ | `CheckoutSessionResult` | `{ status: 201; redirectUrl; nonce }` / `{ status: 204 }` / `{ status: number }`. The `nonce` is the session-bound checkout token: forward it as `x-checkout-session-token` on every continuation call for the same session. |
126
212
  | `CheckoutItem` | `providerItemId`, `providerItemName?`, `quantity?`, `totalAmount`, `overrideAmount?`, `currency?` |
127
213
  | `CheckoutSubscription` | `providerPlanId`, `providerPlanName?`, `quantity?`, `totalAmount`, `overrideAmount?`, `currency?` |
128
214
  | `CheckoutAccount` | `userId`, `firstName?`, `lastName?`, `email`, `country?`, `gender?`, `city?`, `state?`, `zip?` |
129
- | `InlineSessionParams` | Inline browser session creation parameters, including `checkoutMetadata?: CheckoutMetadata \| null`. For detached creation, `timeoutMs` is applied independently to the shell POST and initial catalogue claim PATCH. |
130
- | `InlineSessionDraft` | Browser checkout draft. Inherits nullable `checkoutMetadata`, keeps `userId` required, and allows `account.email` to be omitted and collected during checkout. |
131
- | `InlineSessionPatch` | Just-in-time inline draft update. `checkoutMetadata` replaces the whole current map and accepts explicit `null`. |
132
- | `DetachedCheckoutSession` | Mountable `shell`, stable catalog-attachment `claimed` promise, session `nonce`, and an independently replay-safe `claim(account?)` used to attach late buyer identity. |
133
- | `ProcessPaymentParams` | `sessionId`, `nonce` (required — session-bound checkout token, forwarded as `x-checkout-session-token`), `tokenizedData?`, `accountData`, `chv?` |
215
+ | `InlineSessionParams` | Browser session-creation parameters, including nullable `checkoutMetadata?`. For detached creation, `timeoutMs` applies independently to the shell request and the initial catalog claim. |
216
+ | `InlineSessionDraft` | Browser checkout draft. Keeps `account.userId` required but lets `account.email` be omitted and collected during checkout. |
217
+ | `InlineSessionPatch` | Just-in-time draft update. `checkoutMetadata` replaces the whole current map and accepts an explicit `null`. |
218
+ | `DetachedCheckoutSession` | Mountable `shell`, the catalog-attachment `claimed` promise, the session `nonce`, and an independently replay-safe `claim()` that attaches a late buyer identity. |
219
+ | `ProcessPaymentParams` | `sessionId`, `nonce` (required session-bound checkout token), `tokenizedData?`, `accountData`, `chv?` |
220
+ | `PaymentMethodRemovalError` | Merchant-server saved-method removal union: an indistinguishable `404 payment_method_not_found`, three stable `409` conflict codes, and a retry-safe `502 payment_method_deletion_failed` |
134
221
  | `CreateCustomerParams` | `email`, `name?`, `metadata?` |
135
222
  | `UpdateCustomerParams` | `email?`, `name?`, `metadata?` |
136
223
  | `WebhookEvent` | `id`, `type`, `data`, `created` |
137
224
  | `CurrencyInfo` | `currency`, `symbol`, `country`, `countryCode`, `tax` |
138
225
  | `CountryOption` | `code`, `name`, `flag` |
139
226
  | `TagsData` | `googleContainerId?`, `sessionId?`, `testEventCode?` |
227
+ | `SentryEventLike` | The structural event shape `dropThirdPartyOnlyError()` inspects |
140
228
 
141
- ### Error Classes
142
-
143
- | Export | Description |
144
- |--------|-------------|
145
- | `FloPayError` | Custom error class with `type`, `code?`, `declineCode?`, `param?`, `statusCode?` |
146
- | `FloPayErrorType` | `'validation_error'` / `'api_error'` / `'authentication_error'` / `'rate_limit_error'` / `'network_error'` |
147
- | `validationError(message, param?)` | Factory for validation errors |
148
- | `apiError(message, code?, statusCode?)` | Factory for API errors |
149
- | `authenticationError(message)` | Factory for auth errors |
150
- | `rateLimitError(message)` | Factory for rate limit errors |
151
- | `networkError(message)` | Factory for network errors |
152
-
153
- ### Privacy-safe telemetry contract
154
-
155
- `@flopay/shared` exports the versioned closed telemetry types and builders used
156
- by both browser packages: `buildTelemetryErrorEvent`,
157
- `buildTelemetryLogEvent`, `buildTelemetryPerformanceEvent`,
158
- `buildTelemetryTerminalEvent`, and `serializeTelemetryBatch`. Builders copy
159
- only allowlisted enum/numeric/opaque fields; arbitrary messages, stacks,
160
- payloads, URLs, credentials, customer data, and unknown nested fields cannot
161
- enter serialized events.
162
-
163
- PayPal lifecycle events may additionally carry the closed `inAppBrowser`,
164
- `appFamily`, `popupCapability`, `presentationMode`, and `terminalReason`
165
- dimensions. Invalid values and raw user-agent/provider fields are dropped.
166
-
167
- Detached session creation (TeamFloPay/backend#1099) is measured as two stages
168
- so each phase can be tracked on its own: `session_shell` is create →
169
- mountable card form, and `session_claim` is the background attach of buyer and
170
- catalog data (request category `session_claim`, log names
171
- `session.shell.ready`, `session.claim.started`, `session.claim.completed`).
172
- A successful `session_create` continues to span the whole logical create — shell
173
- *and* claim — so existing end-to-end dashboards keep measuring the same thing,
174
- and reports the create's attempt count rather than the claim's. Claim failures
175
- are reported only under `session_claim`, with duration measured from claim
176
- start; they do not emit a failed `session_create` span after the shell succeeds.
177
-
178
- `TELEMETRY_LOG_NAMES` is the complete SDK-authored lifecycle catalog. The
179
- `TELEMETRY_FAILURE_CATEGORIES` export is the complete technical-failure
180
- catalog: `server_error`, `transport_error`, `invalid_response`, and
181
- `provider_runtime`. `TelemetryFailureCategory` exposes the corresponding
182
- union type. `failureCategory` is optional and accepted only by
183
- `buildTelemetryErrorEvent`; legacy technical events may omit it, and every
184
- non-error builder drops injected category fields.
185
-
186
- The serializer emits the backend-owned v1 envelope with the fixed closed
187
- package identity (`@flopay/shared`, `@flopay/js`, or
188
- `@flopay/react`) and event classes. See
189
- [`docs/TELEMETRY.md`](../../docs/TELEMETRY.md) for the privacy and operational
190
- policy.
191
-
192
- ### Host-owned Sentry filtering
193
-
194
- Hosts that already use Sentry can install the dependency-free predicate as
195
- their `beforeSend` callback:
196
-
197
- ```ts
198
- import * as Sentry from '@sentry/browser';
199
- import { dropThirdPartyOnlyError } from '@flopay/shared';
200
-
201
- Sentry.init({ beforeSend: dropThirdPartyOnlyError });
202
- ```
203
-
204
- It returns `null` only when every exception value has a non-empty stack, no
205
- frame resolves to app code or an `@flopay/*` module, and at least one frame
206
- resolves to a browser-extension URL or Stripe's hosted `dahlia/stripe.js`
207
- controller. Other frames in a dropped stack may be anonymous or pathless.
208
- Mixed, unrelated-CDN, incomplete, anonymous-only, and genuine app errors are
209
- returned unchanged by identity. Frame provenance comes from paths and module
210
- names; Sentry's `in_app` value is deliberately ignored because host source-map
211
- rewriting can mark third-party frames as app-owned.
229
+ ### Configuration
212
230
 
213
- The helper is pure and structurally typed (`SentryEventLike` /
214
- `SentryStackFrameLike`); it does not add an `@sentry/*` dependency, inspect
215
- messages, or bundle Sentry configuration.
231
+ | Function | Description |
232
+ |----------|-------------|
233
+ | `configureFlopay(config)` | Sets the global billing API environment. Call once at startup. |
234
+ | `getFloPayEnvironment()` | The currently configured environment. |
235
+ | `resolveBillingApiUrl(billingApiUrl?)` | The billing API URL, from an explicit argument, then `NEXT_PUBLIC_FLOPAY_ENV`, then `configureFlopay()`, then staging. |
216
236
 
217
237
  ### Constants
218
238
 
219
- | Export | Description |
220
- |--------|-------------|
221
- | `SDK_VERSION` | Current SDK version (`'1.4.9'`) |
222
- | `DEFAULT_API_BASE_URL` | `'https://api.flopay.io'` |
239
+ | Constant | Description |
240
+ |----------|-------------|
241
+ | `SDK_VERSION` | The exact version of this release, identical to the installed package version |
242
+ | `BILLING_API_URL_PRODUCTION` | `'https://api.flopay.com'` |
243
+ | `BILLING_API_URL_STAGING` | `'https://api.stage.flopay.com'` |
244
+ | `DEFAULT_API_BASE_URL` | Alias of the staging billing API, `'https://api.stage.flopay.com'`. Prefer `resolveBillingApiUrl()`. |
223
245
  | `DEFAULT_API_VERSION` | `'2024-01-01'` |
224
246
  | `DEFAULT_APPEARANCE` | Default theme (primary `#4A49FF`, white background, Poppins font) |
225
247
  | `FLAT_APPEARANCE` | Flat theme (minimal borders, 4px radius) |
226
248
  | `NIGHT_APPEARANCE` | Dark theme (dark background `#1A1A2E`, light text) |
227
- | `MODERN_LIGHT_APPEARANCE` | Clean & airy light — Inter, FloPay-blue primary `#1785E0`, slate text, `14px` radius, soft input shadows |
228
- | `MODERN_DARK_APPEARANCE` | Clean & airy dark — Inter, sky-blue primary `#60A5FA`, deep-blue surface `#0B1220`, `14px` radius |
229
- | `BOLD_LIGHT_APPEARANCE` | Bold & vibrant light — Inter, saturated FloPay blue `#1785E0`, 2px borders, 600+ font weight, gradient-ready |
230
- | `BOLD_DARK_APPEARANCE` | Bold & vibrant dark — Inter, sky `#60A5FA` on deep-blue `#0C2B4D`, accented borders |
231
- | `GLASS_LIGHT_APPEARANCE` | Glass / elevated light — translucent inputs with backdrop blur, FloPay-blue primary `#1785E0`, `16px` radius |
232
- | `GLASS_DARK_APPEARANCE` | Glass / elevated dark — translucent inputs with backdrop blur, `#60A5FA` on slate `#0F172A` |
233
- | `BUTTONS_LAYOUT_DEFAULT` / `_MINIMAL` / `_ROUNDED` / `_DARK` | Original buttons-layout style presets (back-compat) |
234
- | `BUTTONS_LAYOUT_MODERN_LIGHT` / `_MODERN_DARK` | Wrapper styles paired with the modern appearance bundles |
235
- | `BUTTONS_LAYOUT_BOLD_LIGHT` / `_BOLD_DARK` | Wrapper styles paired with the bold appearance bundles |
236
- | `BUTTONS_LAYOUT_GLASS_LIGHT` / `_GLASS_DARK` | Wrapper styles paired with the glass appearance bundles |
237
- | `THEMES` | `Record<ThemeBundleId, ThemeBundle>` — pick `THEMES['glass-dark']` to get `{ appearance, buttonsLayout }` |
238
- | `resolveTheme(id)` | Resolve a `ThemeId` (`'classic'` ∪ `ThemeBundleId`) to its `ThemeBundle`; `'classic'` and missing ids return `undefined` |
239
- | `resolveButtonsLayoutTheme(name)` | Map a `ButtonsLayoutTheme` name to its `ButtonsLayoutStyles` preset |
240
- | `PAYPAL_VAULTED_PAYMENT_METHOD_TYPE` | `paypal_vaulted` — in-band payment-method channel that opts a session intent/process request into Flo-owned PayPal Order and setup-token continuations; legacy `paypal` keeps provider-managed semantics. |
241
- | `SUPPORTED_CARD_BRANDS` | `['visa', 'mastercard', 'mastercard_debit', 'amex', 'discover']` |
242
- | `STRIPE_EXPRESS_METHODS` | `['apple_pay', 'google_pay', 'paypal', 'link', 'amazon_pay', 'klarna']` — Stripe method type identifiers supported by `ExpressCheckoutElement` (everything else returned in `gateways.stripe.enabledPaymentMethods` falls into the `PaymentElement` accordion in the React SDK) |
243
- | `partitionStripeMethods(enabledMethods, { excludePaypal? })` | Split the backend's enabled list into `{ expressMethods, paymentElementMethods }`. Drops `card`; also drops `paypal` when `excludePaypal: true`. |
244
- | `stripeExpressMethodToOptionKey(method)` | Map Stripe wire identifiers (`apple_pay`, `google_pay`, `amazon_pay`) to the camelCase keys (`applePay`, `googlePay`, `amazonPay`) expected by `ExpressCheckoutElement`'s `paymentMethods` options map. |
249
+ | `MODERN_LIGHT_APPEARANCE` | Clean and airy light: Inter, FloPay-blue primary `#1785E0`, slate text, 14px radius, soft input shadows |
250
+ | `MODERN_DARK_APPEARANCE` | Clean and airy dark: Inter, sky-blue primary `#60A5FA`, deep-blue surface `#0B1220`, 14px radius |
251
+ | `BOLD_LIGHT_APPEARANCE` | Bold and vibrant light: Inter, saturated FloPay blue `#1785E0`, 2px borders, heavier font weight |
252
+ | `BOLD_DARK_APPEARANCE` | Bold and vibrant dark: Inter, sky `#60A5FA` on deep-blue `#0C2B4D`, accented borders |
253
+ | `GLASS_LIGHT_APPEARANCE` | Glass light: translucent inputs with backdrop blur, FloPay-blue primary `#1785E0`, 16px radius |
254
+ | `GLASS_DARK_APPEARANCE` | Glass dark: translucent inputs with backdrop blur, `#60A5FA` on slate `#0F172A` |
255
+ | `BUTTONS_LAYOUT_DEFAULT` | Original default buttons-layout styles |
256
+ | `BUTTONS_LAYOUT_MINIMAL` | Original minimal buttons-layout styles |
257
+ | `BUTTONS_LAYOUT_ROUNDED` | Original rounded buttons-layout styles |
258
+ | `BUTTONS_LAYOUT_DARK` | Original dark buttons-layout styles |
259
+ | `BUTTONS_LAYOUT_MODERN_LIGHT` | Buttons-layout styles paired with `MODERN_LIGHT_APPEARANCE` |
260
+ | `BUTTONS_LAYOUT_MODERN_DARK` | Buttons-layout styles paired with `MODERN_DARK_APPEARANCE` |
261
+ | `BUTTONS_LAYOUT_BOLD_LIGHT` | Buttons-layout styles paired with `BOLD_LIGHT_APPEARANCE` |
262
+ | `BUTTONS_LAYOUT_BOLD_DARK` | Buttons-layout styles paired with `BOLD_DARK_APPEARANCE` |
263
+ | `BUTTONS_LAYOUT_GLASS_LIGHT` | Buttons-layout styles paired with `GLASS_LIGHT_APPEARANCE` |
264
+ | `BUTTONS_LAYOUT_GLASS_DARK` | Buttons-layout styles paired with `GLASS_DARK_APPEARANCE` |
265
+ | `THEMES` | `Record<ThemeBundleId, ThemeBundle>`: `THEMES['glass-dark']` is `{ appearance, buttonsLayout }` |
266
+ | `PAYPAL_VAULTED_PAYMENT_METHOD_TYPE` | `'paypal_vaulted'`: the payment-method channel that opts a session intent or process request into Flo-owned PayPal Order and setup-token continuations; legacy `paypal` keeps provider-managed semantics |
267
+ | `SUPPORTED_CARD_BRANDS` | `'visa'`, `'mastercard'`, `'mastercard_debit'`, `'amex'`, `'discover'` |
268
+ | `STRIPE_EXPRESS_METHODS` | `'apple_pay'`, `'google_pay'`, `'paypal'`, `'link'`, `'amazon_pay'`, `'klarna'`: Stripe methods rendered as express buttons. Every other enabled Stripe method appears in the React SDK's payment-method accordion. |
245
269
  | `CURRENCY_MAP` | Country code to `CurrencyInfo` mapping (EU, GB, US, CA, NZ, AU) |
246
- | `DEFAULT_CURRENCY` | USD fallback |
247
-
248
- ### Validation Helpers
249
-
250
- | Export | Description |
251
- |--------|-------------|
252
- | `getCurrencyByCountry(countryCode)` | Returns `CurrencyInfo` for a country code, falls back to USD |
270
+ | `DEFAULT_CURRENCY` | The USD fallback `CurrencyInfo` |
271
+ | `IDEMPOTENCY_KEY_HEADER` | `'Idempotency-Key'`, sent on `POST /v1/checkouts/sessions` |
272
+ | `MAX_IDEMPOTENCY_KEY_LENGTH` | `255`, the longest key the SDK accepts |
273
+ | `IDEMPOTENCY_IN_PROGRESS_CODE` | `'IdempotencyKeyInProgress'`: the billing API's retryable in-progress replay code. Retry with the same key. |
274
+ | `TELEMETRY_LOG_NAMES` | The complete SDK-authored lifecycle log catalog |
275
+ | `TELEMETRY_FAILURE_CATEGORIES` | `'server_error'`, `'transport_error'`, `'invalid_response'`, `'provider_runtime'` |
276
+
277
+ ### Theme and payment-method helpers
278
+
279
+ | Function | Description |
280
+ |----------|-------------|
281
+ | `resolveTheme(id)` | Resolves a `ThemeId` to its `ThemeBundle`; `'classic'` and missing ids return `undefined` |
282
+ | `resolveButtonsLayoutTheme(name)` | Maps a `ButtonsLayoutTheme` name to its `ButtonsLayoutStyles` preset |
283
+ | `partitionStripeMethods(enabledMethods, options?)` | Splits the billing API's enabled Stripe methods into `{ expressMethods, paymentElementMethods }`. Drops `card`, and also `paypal` when `excludePaypal` is true. |
284
+ | `stripeExpressMethodToOptionKey(method)` | Maps Stripe wire identifiers (`apple_pay`, `google_pay`, `amazon_pay`) to the camelCase option keys Stripe's express checkout expects |
285
+
286
+ ### Validation helpers
287
+
288
+ | Function | Description |
289
+ |----------|-------------|
290
+ | `getCurrencyByCountry(countryCode)` | `CurrencyInfo` for a country code, falling back to USD |
253
291
  | `isCardHolderNamePlaceholder(...nameParts)` | Rejects only the normalized `Card Holder Name` fallback sentinel |
254
- | `isValidPublishableKey(key)` | Returns `true` if the key matches `pk_(test|live)_...` |
255
- | `isValidSecretKey(key)` | Returns `true` if the key matches `sk_(test|live)_...` |
256
- | `isCardSetupCheckoutSession(session)` | Returns `true` only for a session read with `checkoutMode: 'setup'`, zero amount, and no products. Use it before mounting a no-charge card-setup surface. |
257
-
258
- ### Postal Code Helpers
292
+ | `isValidEmail(email)` | Browser-safe email validation matching the billing API; `false` for nullish, blank, padded, or malformed values |
293
+ | `isValidPublishableKey(key)` | `true` for keys shaped like `pk_test_...` or `pk_live_...` |
294
+ | `isValidSecretKey(key)` | `true` for keys shaped like `sk_test_...` or `sk_live_...` |
295
+ | `isCardSetupCheckoutSession(session)` | `true` only for a session read with `checkoutMode: 'setup'`, zero amount, and no products. Use it before mounting a no-charge card-setup surface. |
296
+ | `isValidPostalCode(country, postalCode)` | `true` when the trimmed postcode matches the country's format; fails open for a blank postcode, an unresolvable country, or an unsupported country |
297
+ | `isPostalCodeSupported(country)` | `true` when a postcode rule exists for the country |
298
+ | `getPostalCodeExample(country)` | The billing API's example postcode for the country (US `12345 or 12345-6789`, GB `SW1A 1AA`, CA `A1A 1A1`), or `undefined` |
299
+ | `getPostalCodeLabel(countryCode)` | Country-appropriate field label (`ZIP Code`, `Postcode`, `Eircode`), defaulting to `Postal Code` |
300
+ | `getStateFromPostalCode(country, postalCode)` | Derives a US state or Canadian province code from a postcode for AVS enrichment; `null` for other countries or a malformed code |
259
301
 
260
- Country-aware postcode format validation, a **faithful mirror of the billing API's own validator** — `is-valid-postal-code.validator.ts` (TeamFloPay/backend#895), same pinned [`validator`](https://www.npmjs.com/package/validator)`@13.15.35` `isPostalCode`, same country→ISO-2 normalization, same fail-open rules — so the SDK never blocks a postcode the server accepts nor opens its submit gate on one the server's `PATCH …/account` will reject with a 400. `@flopay/react`'s AVS block uses these to block a malformed postcode before the card is captured and to show the expected format inline.
302
+ ### Idempotency helpers
261
303
 
262
- | Export | Description |
263
- |--------|-------------|
264
- | `getPostalCodeLabel(countryCode)` | Country-appropriate field label (`'ZIP Code'`, `'Postcode'`, `'Eircode'`, …); defaults to `'Postal Code'` |
265
- | `isPostalCodeSupported(country)` | `true` when `validator` has a postcode pattern for the country. Normalization matches the backend: a **2-letter code is passed straight through** (so `'UK'`, not a `validator` locale, → `false`); non-2-letter names resolve via the backend's `COUNTRY_ALIASES` (`'United Kingdom'` → `GB`). Unresolvable / no-postcode countries return `false` |
266
- | `isValidPostalCode(country, postalCode)` | `true` when the (trimmed) postcode matches the country's format. **Fails open** in exactly the backend's three cases: blank/whitespace postcode, unresolvable country, or an unsupported `validator` locale. Callers distinguish "required" (empty) from "malformed" by checking emptiness + `isPostalCodeSupported` themselves — an empty value fails open here, as it does server-side |
267
- | `isValidEmail(email)` | Shared browser-safe email validation. Returns `false` for nullish, blank, padded, or malformed values; callers should trim intentionally before validation when surrounding whitespace is acceptable. |
268
- | `getPostalCodeExample(country)` | The example postcode the backend embeds in its 400 message (US `12345 or 12345-6789`, GB `SW1A 1AA`, CA `A1A 1A1`, …), or `undefined` when there is none. Kept verbatim in sync with the backend's `POSTAL_CODE_EXAMPLES` so the SDK's inline hint and the server's rejection message never contradict |
269
- | `getStateFromPostalCode(country, postalCode)` | Derives a US state / CA province code from a postcode for AVS enrichment; `null` for other countries or a malformed code |
304
+ | Function | Description |
305
+ |----------|-------------|
306
+ | `generateIdempotencyKey()` | A cryptographically random key, or `undefined` when no secure random source exists |
307
+ | `resolveIdempotencyKey(supplied?)` | Returns a supplied key unchanged after validating it is non-empty and at most `MAX_IDEMPOTENCY_KEY_LENGTH` characters, or a freshly generated key when none is supplied |
270
308
 
271
- ### Idempotency Helpers
272
-
273
- Primitives for the stable checkout-create `Idempotency-Key` (TeamFloPay/backend#972). `@flopay/js` and `@flopay/react` use these automatically; import them only if you build your own create flow.
309
+ ### Error helpers
274
310
 
275
311
  | Export | Description |
276
312
  |--------|-------------|
277
- | `IDEMPOTENCY_KEY_HEADER` | The header name (`'Idempotency-Key'`) the SDK sends on `POST /v1/checkouts/sessions`. Optional on the backend — absent means the legacy, non-idempotent path. |
278
- | `MAX_IDEMPOTENCY_KEY_LENGTH` | `255` — the documented max length a supplied key may have. |
279
- | `IDEMPOTENCY_IN_PROGRESS_CODE` | Backend `code` (`'IdempotencyKeyInProgress'`) marking a **retryable** in-progress replay: retry with the *same* key. Distinct from a payload-conflict (`409`), which is non-retryable. |
280
- | `generateIdempotencyKey()` | Returns a cryptographically random, high-entropy key (`crypto.randomUUID()`, else 16 random bytes). Returns `undefined` when no secure RNG exists, so callers omit the header rather than emit a weak key. Never derived from request data. |
281
- | `resolveIdempotencyKey(supplied?)` | Returns a supplied key unchanged after validating it is non-empty and ≤ `MAX_IDEMPOTENCY_KEY_LENGTH` (throws `FloPayError('validation_error')` otherwise, never echoing the value), or a freshly generated key when none is supplied. |
282
-
283
- ```ts
284
- import { isPostalCodeSupported, isValidPostalCode, getPostalCodeExample } from '@flopay/shared';
285
-
286
- isPostalCodeSupported('US'); // true
287
- isPostalCodeSupported('AE'); // false — UAE has no postcodes
288
- isPostalCodeSupported('United Kingdom'); // true — full name → GB
289
- isPostalCodeSupported('UK'); // false — 2-letter passthrough (not a validator locale)
290
- isValidPostalCode('GB', 'SW1A 1AA'); // true
291
- isValidPostalCode('GB', '12345'); // false — US shape in GB
292
- isValidPostalCode('AE', ''); // true — fail open, never blocks
293
- isValidPostalCode('US', ''); // true — blank fails open (enforce "required" separately)
294
- getPostalCodeExample('CA'); // 'A1A 1A1'
295
- ```
296
-
297
- > **Runtime dependency.** Unlike the rest of `@flopay/shared`, these helpers pull in `validator` (imported via the `validator/lib/isPostalCode.js` submodule so bundlers only include the one check). It installs transitively when you depend on `@flopay/shared`.
298
-
299
- ### Gateway decisions belong to the billing API
300
-
301
- `@flopay/shared` exposes no helper for declaring, selecting, or filtering payment gateways or gateway capabilities, and none should be added. Which gateway processes a session (Stripe, PayPal, Worldpay Corporate Gateway, or a future provider) is decided by the billing API from the merchant's configured gateways and their approved capability records (TeamFloPay/backend#1641). Clients that build `POST /v1/checkouts/sessions` themselves send the same provider-neutral body the SDK sends and render whatever `gateways` the response advertises.
302
-
313
+ | `FloPayError` | Error class with `type`, `code?`, `declineCode?`, `param?`, `statusCode?`, and `cause?` |
314
+ | `FloPayErrorType` | `'validation_error'` / `'api_error'` / `'authentication_error'` / `'rate_limit_error'` / `'network_error'` |
315
+ | `validationError(message, param?)` | Creates a `validation_error` |
316
+ | `apiError(message, code?, statusCode?)` | Creates an `api_error` |
317
+ | `authenticationError(message)` | Creates an `authentication_error` |
318
+ | `rateLimitError(message)` | Creates a `rate_limit_error` |
319
+ | `networkError(message)` | Creates a `network_error` |
320
+ | `normalizeCheckoutFailureOutcome(value)` | Narrows an unknown value to a `CheckoutFailureOutcome`, or `undefined` |
321
+
322
+ ### Telemetry and Sentry helpers
323
+
324
+ | Function | Description |
325
+ |----------|-------------|
326
+ | `buildTelemetryErrorEvent(input)` | Builds a closed technical-error event; the only builder that accepts a `failureCategory` |
327
+ | `buildTelemetryLogEvent(input)` | Builds a closed lifecycle log event |
328
+ | `buildTelemetryPerformanceEvent(input)` | Builds a closed performance measurement |
329
+ | `buildTelemetryTerminalEvent(input)` | Builds a closed terminal-outcome event |
330
+ | `serializeTelemetryBatch(events, options)` | Serializes events into the billing API's v1 telemetry envelope |
331
+ | `dropThirdPartyOnlyError(event)` | Sentry `beforeSend` predicate that drops recognized third-party-only exception stacks and otherwise returns the original event |
332
+
333
+ ## Errors
334
+
335
+ Errors the FloPay SDK creates or normalizes are `FloPayError`s. Other failures can still surface unchanged, such as an underlying `fetch()` rejection, a Stripe error, or a native `Error` from a payload builder, so check `error instanceof FloPayError` before reading `type` or `code`. Branch on its stable `type` and `code`, never on `message`.
336
+
337
+ | `type` | Meaning |
338
+ |--------|---------|
339
+ | `validation_error` | The input was rejected before or by the billing API. `param` names the offending field when known. |
340
+ | `api_error` | The billing API or a payment provider returned an error. `code` carries the billing API's stable code and `statusCode` the HTTP status. |
341
+ | `authentication_error` | A key or session credential was rejected. |
342
+ | `rate_limit_error` | The request was rate limited; retry later. |
343
+ | `network_error` | No usable response arrived. |
344
+
345
+ Codes raised by this package's helpers:
346
+
347
+ - `InvalidIdempotencyKey` (`validation_error`, `param: 'idempotencyKey'`) from `resolveIdempotencyKey()` when a supplied key is blank or longer than `MAX_IDEMPOTENCY_KEY_LENGTH`. The message never echoes the key.
348
+ - `CaptureMethodUnsupportedForSubscription` (`validation_error`, `param: 'captureMethod'`) when manual capture is requested for a cart that contains a subscription.
349
+
350
+ Billing API outcomes described by shared types:
351
+
352
+ - `IDEMPOTENCY_IN_PROGRESS_CODE` marks a retryable in-progress replay of the same idempotency key; retry with the same key. A `409` payload conflict for a reused key is not retryable.
353
+ - `CheckoutFailureOutcome` classifies an authorization that did not complete: `authorization_declined`, `authentication_required`, `authorization_expired`, or `capture_failed`.
354
+ - `PaymentMethodRemovalError` covers saved-method removal from your server: `payment_method_not_found` (404, also returned for a method you cannot access), the `409` conflicts `payment_method_has_active_payment`, `payment_method_in_use`, and `payment_method_reassignment_requires_payer_action`, and `payment_method_deletion_failed` (502, retry-safe, with a `reason` and `resolution`).