@flopay/js 1.9.16 → 1.9.18
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 +280 -485
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @flopay/js
|
|
2
2
|
|
|
3
|
-
Browser-side FloPay SDK.
|
|
3
|
+
Browser-side FloPay SDK. `loadFloPay()` initializes a `FloPay` instance for hosted card capture and session access, `PaymentAPI` is the billing API client, and `createCheckoutSession()` creates a checkout session and redirects to the hosted checkout page.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Providers sit behind the `PaymentProviderAdapter` interface (implemented today by `StripeAdapter`), so checkout code does not change when a session is routed to a different gateway.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -10,9 +10,11 @@ Currently backed by Stripe via the `StripeAdapter`. The adapter pattern (`Paymen
|
|
|
10
10
|
pnpm add @flopay/js
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
`@stripe/stripe-js`
|
|
13
|
+
`@stripe/stripe-js` and `@flopay/shared` install automatically as dependencies; you do not need to add them. Choose the billing API environment once at startup with `configureFlopay()` from `@flopay/shared`, or the `NEXT_PUBLIC_FLOPAY_ENV` environment variable.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
For React applications, `@flopay/react` builds on this package and renders the complete checkout.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
16
18
|
|
|
17
19
|
### Initialize the SDK
|
|
18
20
|
|
|
@@ -20,394 +22,114 @@ pnpm add @flopay/js
|
|
|
20
22
|
import { loadFloPay } from '@flopay/js';
|
|
21
23
|
|
|
22
24
|
const flopay = await loadFloPay('pk_test_...', {
|
|
23
|
-
billingApiUrl: 'https://billing.example.com', //
|
|
25
|
+
billingApiUrl: 'https://billing.example.com', // enables retrieveSession() and retrieveUnifiedSession()
|
|
24
26
|
});
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
`loadFloPay` caches by publishable key and behavior-affecting options.
|
|
28
|
-
calls with the same key and options share one in-flight provider initialization
|
|
29
|
-
and resolve to the same `FloPay` instance. A failed initialization is evicted so
|
|
30
|
-
a later call can retry. The optional second argument accepts
|
|
31
|
-
`Omit<FloPayConfig, 'publishableKey'>`, including `billingApiUrl`, `locale`,
|
|
32
|
-
`appearance`, and `telemetry`; set `telemetry: false` to opt out of privacy-safe
|
|
33
|
-
operational telemetry.
|
|
34
|
-
|
|
35
|
-
When telemetry is enabled, a Stripe loader rejection is reported only with the
|
|
36
|
-
bounded `provider_runtime` category, while a null Stripe loader result uses the
|
|
37
|
-
distinct `invalid_response` category. Both use `PROVIDER_LOAD_FAILED` at
|
|
38
|
-
`provider_load` with request category `provider_sdk`; neither changes the
|
|
39
|
-
publicly thrown error or failed-load retry behavior, and neither transmits the
|
|
40
|
-
publishable key, rejection, provider message, stack, URL, or provider payload.
|
|
41
|
-
|
|
42
|
-
`FloPayCheckout` in `@flopay/react` also classifies failures around the session
|
|
43
|
-
and provider boundary without exposing raw errors. An existing session-read
|
|
44
|
-
transport failure uses `transport_error`; malformed JSON uses
|
|
45
|
-
`invalid_response`. A provider failure after the session resolves reuses the
|
|
46
|
-
provider-load tuple above rather than collapsing into `INTERNAL_SDK_ERROR` at
|
|
47
|
-
`checkout_mount`. See the React package README for its hosted-card fallback and
|
|
48
|
-
bounded provider retry behavior.
|
|
49
|
-
|
|
50
|
-
### Retrieve a Session
|
|
29
|
+
The optional second argument accepts every `FloPayConfig` field except `publishableKey`: `billingApiUrl`, `locale`, `appearance`, and `telemetry`. `loadFloPay` caches by publishable key and behavior-affecting options: concurrent calls with the same key and options share one in-flight provider initialization and resolve to the same `FloPay` instance. A failed initialization is evicted, so a later call can retry.
|
|
51
30
|
|
|
52
|
-
|
|
53
|
-
// Using billingApiUrl from config (set at init time):
|
|
54
|
-
const session = await flopay.retrieveSession('session_uuid');
|
|
55
|
-
console.log(session.amount, session.currency, session.status);
|
|
31
|
+
### Retrieve a session
|
|
56
32
|
|
|
57
|
-
|
|
58
|
-
const session2 = await flopay.retrieveSession('session_uuid', 'https://billing.example.com');
|
|
59
|
-
|
|
60
|
-
// For provider-specific data (Stripe clientSecret, publishableKey, etc.):
|
|
61
|
-
const unified = await flopay.retrieveUnifiedSession('session_uuid');
|
|
62
|
-
console.log(unified.data.stripe?.clientSecret);
|
|
63
|
-
```
|
|
33
|
+
`flopay.retrieveSession(sessionId)` returns the normalized `CheckoutSession` (amount, currency, status, customer). `flopay.retrieveUnifiedSession(sessionId)` returns a `NormalizedCheckoutSession`: every gateway the session advertises is listed in `providers`, and gateway data such as the Stripe client secret is under `data`. Both use the `billingApiUrl` from `loadFloPay`, or a URL you pass as the second argument.
|
|
64
34
|
|
|
65
35
|
`retrieveUnifiedSession()` exposes provider-specific session data without
|
|
66
36
|
mounting provider UI. Use `FloPayCheckout` or `SplitCardForm` from
|
|
67
37
|
`@flopay/react` for hosted-vault card, wallet, and APM rendering.
|
|
68
38
|
|
|
69
|
-
|
|
39
|
+
### Call the billing API directly
|
|
70
40
|
|
|
71
|
-
|
|
41
|
+
`PaymentAPI` wraps the billing API routes the checkout uses. Session reads and continuation calls take the session **nonce**, the session-bound checkout token returned when the session was created, and send it as `x-checkout-session-token`.
|
|
72
42
|
|
|
73
|
-
|
|
74
|
-
|
|
43
|
+
- `getUnifiedCheckoutSession(sessionId, nonce)` reads and normalizes a session.
|
|
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
|
+
- `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.
|
|
47
|
+
- `getPaymentsByEmail(email)` looks up prior payments for a saved-payment experience.
|
|
75
48
|
|
|
76
|
-
|
|
49
|
+
### Create a hosted checkout session
|
|
77
50
|
|
|
78
|
-
|
|
79
|
-
// checkout token returned from session creation — required by post-#640
|
|
80
|
-
// backends, which match it against `checkout_session.nonce` before returning
|
|
81
|
-
// the row.
|
|
82
|
-
const session = await api.getUnifiedCheckoutSession('session_uuid', sessionNonce);
|
|
83
|
-
// session.provider === 'stripe' | 'chargebee' | 'recurly'
|
|
84
|
-
// session.data.session contains the normalized CheckoutSession
|
|
85
|
-
// session.data.session.clientSecret carries the same nonce so downstream
|
|
86
|
-
// code can re-use it.
|
|
87
|
-
|
|
88
|
-
// Create an Apple Pay intent. Direct-card requests are not part of this union.
|
|
89
|
-
const intent = await api.createSessionIntent(
|
|
90
|
-
'session_uuid',
|
|
91
|
-
sessionNonce,
|
|
92
|
-
{
|
|
93
|
-
provider: 'stripe',
|
|
94
|
-
paymentMethodCategory: 'wallet',
|
|
95
|
-
paymentMethodType: 'apple_pay',
|
|
96
|
-
paymentMethodId: 'pm_xxx',
|
|
97
|
-
intentKind: 'payment',
|
|
98
|
-
},
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// Report a client-observed non-card decline using a provider classification
|
|
102
|
-
// code only. Messages, identifiers, card data, credentials, and PII are rejected.
|
|
103
|
-
await api.reportSessionIntentDecline('session_uuid', sessionNonce, {
|
|
104
|
-
provider: 'stripe',
|
|
105
|
-
paymentMethodCategory: 'wallet',
|
|
106
|
-
paymentMethodType: 'apple_pay',
|
|
107
|
-
providerDeclineReason: 'payment_failed',
|
|
108
|
-
});
|
|
51
|
+
`createCheckoutSession(options)` posts to `POST /v1/checkouts/sessions` and redirects the browser:
|
|
109
52
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// and otherwise POSTs to `/v1/checkouts/sessions/<id>/process` with the
|
|
113
|
-
// header set.
|
|
114
|
-
const processResponse = await api.processPayment('user_id', {
|
|
115
|
-
sessionId: 'session_uuid',
|
|
116
|
-
nonce: sessionNonce,
|
|
117
|
-
tokenizedData: { id: 'pm_xxx', type: 'card' },
|
|
118
|
-
accountData: { userId: 'user_id', email: 'user@example.com', firstName: 'John', lastName: 'Doe' },
|
|
119
|
-
});
|
|
120
|
-
// If the backend returns `202 checkout_processing`, the SDK keeps polling the
|
|
121
|
-
// checkout session until it completes or times out, then resolves/rejects.
|
|
53
|
+
- On `201`, the browser goes to the hosted checkout page and the result is `{ status: 201, redirectUrl, nonce }`. A matching `flopay_checkout_token` cookie is written to the same parent domain as `checkout_data`, so the hosted page can read the token without putting it in the URL, history, or `Referer`.
|
|
54
|
+
- On `204`, a payment method is already on file and the browser goes to `successUrl`.
|
|
122
55
|
|
|
123
|
-
|
|
124
|
-
const payments = await api.getPaymentsByEmail('user@example.com');
|
|
125
|
-
```
|
|
56
|
+
`createCheckoutSessionWithRetries(options)` adds automatic retry with jittered exponential backoff on transient transport, timeout, and idempotency-in-progress failures: at most two retries (three attempts), all inside one 12-second total deadline you can change with `timeoutMs`.
|
|
126
57
|
|
|
127
|
-
###
|
|
58
|
+
### Checkout metadata
|
|
128
59
|
|
|
129
|
-
|
|
130
|
-
import { createCheckoutSession } from '@flopay/js';
|
|
60
|
+
Pass `checkoutMetadata?: Record<string, string> | null` to redirect, retrying, one-shot inline, or detached creation. A present map, an explicit `null`, and `{}` are sent unchanged; omitting the field leaves the request body unchanged. Session reads expose a returned snapshot as `normalized.checkoutMetadata` and `normalized.data.session.checkoutMetadata`; an omitted or `null` create value is absent from reads, while `{}` is returned as `{}`. The SDK performs no duplicate size validation: billing API rejections keep their `code`, message, and HTTP `statusCode`.
|
|
131
61
|
|
|
132
|
-
|
|
133
|
-
billingApiUrl: 'https://billing.example.com',
|
|
134
|
-
checkoutBaseUrl: 'https://checkout.example.com',
|
|
135
|
-
clientId: 'client_123',
|
|
136
|
-
items: [{
|
|
137
|
-
providerItemId: 'prod_abc',
|
|
138
|
-
providerItemName: 'Pro Plan',
|
|
139
|
-
totalAmount: 49.99,
|
|
140
|
-
overrideAmount: 24.99,
|
|
141
|
-
}],
|
|
142
|
-
account: { userId: 'user_1', email: 'user@example.com' },
|
|
143
|
-
successUrl: '/success',
|
|
144
|
-
cancelUrl: '/cancel',
|
|
145
|
-
checkoutMetadata: { merchantOrderId: 'order-123', campaign: 'summer' },
|
|
146
|
-
redirectParams: { email: 'user@example.com', bg: 'courses', mode: 'confirm' },
|
|
147
|
-
});
|
|
148
|
-
// On 201: browser redirects to checkout page. The resolved
|
|
149
|
-
// `CheckoutSessionResult` is `{ status: 201, redirectUrl, nonce }` — `nonce` is
|
|
150
|
-
// the session-bound checkout token returned by the billing API. A matching
|
|
151
|
-
// `flopay_checkout_token` cookie is written to the same parent domain as
|
|
152
|
-
// `checkout_data`, so a hosted-checkout page can read the token from
|
|
153
|
-
// `document.cookie` and forward it on continuation calls without having to
|
|
154
|
-
// pull it from the URL (kept out of history/Referer).
|
|
155
|
-
// On 204: browser redirects to successUrl (payment method on file)
|
|
156
|
-
```
|
|
62
|
+
### Authorise now, capture later
|
|
157
63
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
Pass `checkoutMetadata?: Record<string, string> | null` to redirect, retrying,
|
|
161
|
-
one-shot inline, or detached creation. A present map, explicit `null`, and `{}`
|
|
162
|
-
are sent unchanged; omitting the field leaves the request body unchanged.
|
|
163
|
-
Session reads expose a returned snapshot as
|
|
164
|
-
`normalized.checkoutMetadata` and `normalized.data.session.checkoutMetadata`.
|
|
165
|
-
An omitted or `null` create value is absent from reads, while an explicit `{}`
|
|
166
|
-
is returned as `{}`. This field is separate from the legacy session `metadata`
|
|
167
|
-
member.
|
|
168
|
-
|
|
169
|
-
The SDK deliberately performs no duplicate size or length validation. Backend
|
|
170
|
-
rejections remain `FloPayError` instances carrying the backend `code`, message,
|
|
171
|
-
and HTTP `statusCode`. Retrying creation replays the same metadata and
|
|
172
|
-
idempotency key.
|
|
173
|
-
|
|
174
|
-
For an item-only card checkout that should place a hold instead of capturing
|
|
175
|
-
immediately, add `captureMethod: 'manual'`. The same option is supported by
|
|
176
|
-
`createCheckoutSessionWithRetries`, `PaymentAPI.createAndFetchSession`, and
|
|
177
|
-
`PaymentAPI.createDetachedSession`. Omitting it keeps the request and result
|
|
178
|
-
behaviour unchanged.
|
|
179
|
-
|
|
180
|
-
An accepted hold surfaces as `status: 'authorized'` on normalized sessions and
|
|
181
|
-
payment results, with `paymentId`, `sessionId`, and
|
|
182
|
-
`authorizationExpiresAt`. Treat it as checkout-terminal but not paid. Capture
|
|
183
|
-
is a merchant-authenticated REST operation; this browser package contains no
|
|
184
|
-
merchant credential or capture capability. See the repository's
|
|
185
|
-
[pre-authorisation guide](../../docs/PREAUTHORIZATION.md).
|
|
186
|
-
|
|
187
|
-
Use `createCheckoutSessionWithRetries` for automatic retry with jittered
|
|
188
|
-
exponential backoff on transient transport, timeout, and idempotency-in-progress
|
|
189
|
-
errors. By default it makes at most two retries (three total attempts). A single
|
|
190
|
-
12-second total deadline (customizable with `timeoutMs`) covers the initial
|
|
191
|
-
request, every retry, and all backoff; all attempts receive the same
|
|
192
|
-
`AbortSignal`.
|
|
193
|
-
|
|
194
|
-
#### Idempotent checkout creation
|
|
195
|
-
|
|
196
|
-
When a secure RNG is available, every checkout-session create sends a stable
|
|
197
|
-
`Idempotency-Key` header so a timeout or lost response cannot mint a second
|
|
198
|
-
session. The behavior is fully additive — existing integrations need no changes:
|
|
199
|
-
|
|
200
|
-
- **Automatic (default):** the SDK generates one cryptographically random,
|
|
201
|
-
high-entropy key per logical `createCheckoutSession` call and reuses it for
|
|
202
|
-
every transport retry of that same operation. Two independent calls get
|
|
203
|
-
different keys, even when their request bodies are identical. The generated
|
|
204
|
-
key is never derived from customer data and is never logged or returned.
|
|
205
|
-
Automatic generation requires a secure RNG (`crypto.randomUUID` /
|
|
206
|
-
`crypto.getRandomValues`); in an environment without one the SDK omits the
|
|
207
|
-
header rather than emit a weak key — supply a valid `idempotencyKey` yourself
|
|
208
|
-
to stay idempotent there.
|
|
209
|
-
- **Merchant-supplied:** pass `idempotencyKey` to control the key yourself. It
|
|
210
|
-
is sent unchanged and reused across retries, so you can keep it stable across
|
|
211
|
-
React remounts, multiple SDK instances, or server retries you control. It
|
|
212
|
-
identifies **exactly one** logical checkout creation and must not be reused
|
|
213
|
-
for a new purchase. It must be non-empty and at most 255 characters (an
|
|
214
|
-
invalid value throws a `FloPayError('validation_error')` before any request).
|
|
64
|
+
For an item-only card checkout that should place a hold instead of capturing immediately, add `captureMethod: 'manual'` to `createCheckoutSession`, `createCheckoutSessionWithRetries`, `PaymentAPI.createAndFetchSession`, or `PaymentAPI.createDetachedSession`. An accepted hold surfaces as `status: 'authorized'` on normalized sessions and payment results, with `paymentId`, `sessionId`, and `authorizationExpiresAt`. Treat it as checkout-terminal but not paid. Capture is a merchant-authenticated REST operation, never available to browser code; see the [payments and authorisations reference](https://flopay.com/docs/technical-reference/rest/payments).
|
|
215
65
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
```
|
|
66
|
+
### Idempotent checkout creation
|
|
67
|
+
|
|
68
|
+
Every checkout-session create sends a stable `Idempotency-Key` header when a secure random source is available, so a timeout or lost response cannot mint a second session. Existing integrations need no changes.
|
|
220
69
|
|
|
221
|
-
`
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
without retrying.
|
|
225
|
-
API clients that omit it keep the legacy, unkeyed path. The same automatic and
|
|
226
|
-
merchant-supplied behavior applies to inline session creation via
|
|
227
|
-
`PaymentAPI.createAndFetchSession` (used by `@flopay/react`). A one-shot inline
|
|
228
|
-
create uses the same 12-second total deadline and jitter policy, with one shared
|
|
229
|
-
three-attempt budget across network failures and idempotency-in-progress
|
|
230
|
-
responses. A detached inline create applies `timeoutMs` afresh to its shell POST
|
|
231
|
-
and initial catalogue-claim PATCH; each phase has its own signal and retry
|
|
232
|
-
budget. Pass `timeoutMs` in the inline session params to choose a different
|
|
233
|
-
per-phase ceiling.
|
|
234
|
-
|
|
235
|
-
`@flopay/shared` also exports the primitives directly if you build your own
|
|
236
|
-
create flow: `generateIdempotencyKey()`, `resolveIdempotencyKey(supplied?)`,
|
|
237
|
-
`IDEMPOTENCY_KEY_HEADER`, and `MAX_IDEMPOTENCY_KEY_LENGTH`.
|
|
70
|
+
- **Automatic (default):** the SDK generates one cryptographically random key per logical `createCheckoutSession` call and reuses it for every retry of that operation. Independent calls get different keys even when their bodies are identical. The key is never derived from customer data and is never logged or returned. Without `crypto.randomUUID` or `crypto.getRandomValues` the header is omitted; supply your own key to stay idempotent there.
|
|
71
|
+
- **Merchant-supplied:** pass `idempotencyKey` to keep the key stable across remounts, SDK instances, or your own server retries. It identifies exactly one logical checkout and must never be reused for a new purchase.
|
|
72
|
+
|
|
73
|
+
`createCheckoutSessionWithRetries` resolves the key once, before its retry loop, so timeouts, lost responses, and in-progress replies all replay the same key; a `409` payload conflict is surfaced without retrying. `PaymentAPI.createAndFetchSession` (used by `@flopay/react`) follows the same rules, with one shared three-attempt budget across network failures and in-progress replies.
|
|
238
74
|
|
|
239
75
|
### Detached session creation (default)
|
|
240
76
|
|
|
241
|
-
Inline session creation is
|
|
242
|
-
Instead of one request that resolves buyer identity and the catalog inline, the
|
|
243
|
-
SDK issues two:
|
|
244
|
-
|
|
245
|
-
1. `POST /v1/checkouts/sessions` with `deferDataAttachment: true` — a
|
|
246
|
-
lightweight *shell*. It runs no catalog validation and takes none of the
|
|
247
|
-
buyer-identity advisory locks that serialise concurrent checkouts for the
|
|
248
|
-
same customer. The backend routes a gateway for it from `currency` and the
|
|
249
|
-
buyer's `country`, so the response already carries `gateways` and the hosted
|
|
250
|
-
`vault` block: **the card form can mount from it.** Checkout metadata is sent
|
|
251
|
-
on this initial request.
|
|
252
|
-
2. `PATCH /v1/checkouts/sessions/{id}/claim` — attaches products and coupons in
|
|
253
|
-
the background. When the draft already has an email, the same request also
|
|
254
|
-
attaches the buyer. An email-less draft sends no `accountData` block, so the
|
|
255
|
-
returned session has its full catalog, totals and eligible method metadata
|
|
256
|
-
while `buyerIdentified` remains `false`. Checkout metadata is never repeated
|
|
257
|
-
or edited by either the catalog or late-buyer claim.
|
|
258
|
-
|
|
259
|
-
For detached creation, `timeoutMs` is a fresh per-phase budget: the shell POST
|
|
260
|
-
gets the full value, and after it succeeds the initial claim PATCH gets the full
|
|
261
|
-
value again under a different `AbortSignal`. The default is 12 seconds for each
|
|
262
|
-
phase, so time spent creating a usable shell never starves its catalogue claim.
|
|
263
|
-
One-shot creation still has one total deadline.
|
|
264
|
-
|
|
265
|
-
`PaymentAPI.createDetachedSession(params)` exposes both phases. When the draft
|
|
266
|
-
already has an email, the combined catalog-and-buyer claim starts immediately.
|
|
267
|
-
When email is missing, a catalog-only claim still starts immediately; the
|
|
268
|
-
caller can attach the buyer later with the separate `claim(account)` operation:
|
|
77
|
+
Inline session creation is detached by default. Instead of one request that resolves buyer identity and the catalog together, the SDK issues two:
|
|
269
78
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
...params,
|
|
273
|
-
account: { userId: 'buyer_123' },
|
|
274
|
-
});
|
|
79
|
+
1. `POST /v1/checkouts/sessions` with `deferDataAttachment: true` returns a lightweight **shell**. It runs no catalog validation and takes none of the buyer-identity locks that serialise concurrent checkouts for the same customer, yet already carries `gateways` and the hosted `vault` block: **the card form can mount from it.** Checkout metadata is sent on this request.
|
|
80
|
+
2. `PATCH /v1/checkouts/sessions/{id}/claim` attaches products and coupons in the background. When the draft has an email it also attaches the buyer; an email-less draft omits the buyer, and the returned session has its full catalog, totals, and methods while `buyerIdentified` stays `false`.
|
|
275
81
|
|
|
276
|
-
|
|
277
|
-
const session = await detached.claimed; // catalog, totals, methods; buyerIdentified === false
|
|
82
|
+
`PaymentAPI.createDetachedSession(params)` exposes both phases as `{ shell, sessionId, nonce, claimed, claim }`. `claimed` settles when the catalog is attached; `claim(account)` attaches a buyer identity later. Both are independently replay-safe: repeated valid `claim(account)` calls return the same promise, and a local invalid-email rejection does not consume the operation. `createAndFetchSession` also accepts an email-less draft and resolves with the catalog-attached anonymous session.
|
|
278
83
|
|
|
279
|
-
|
|
280
|
-
email: 'buyer@example.com',
|
|
281
|
-
firstName: 'Jane',
|
|
282
|
-
lastName: 'Doe',
|
|
283
|
-
}); // buyerIdentified === true
|
|
284
|
-
```
|
|
84
|
+
Nothing may be charged before `claimed` settles: the billing API rejects process, intent, and decline calls on an unclaimed session with `409 checkout_session_data_attachment_required`, and holds an unclaimed hosted card charge with a retryable `503`. `@flopay/react` gates the card form's submit button for exactly this window.
|
|
285
85
|
|
|
286
|
-
|
|
287
|
-
valid `claim(account)` calls return the same buyer-attachment promise; a local
|
|
288
|
-
invalid-email rejection does not consume the operation. Blank/null identity
|
|
289
|
-
fields are omitted. `createAndFetchSession` also accepts an email-less draft and
|
|
290
|
-
resolves with the catalog-attached anonymous session.
|
|
86
|
+
For detached creation, `timeoutMs` is a fresh per-phase budget: the shell POST gets the full value, and after it succeeds the initial claim PATCH gets the full value again under a different `AbortSignal` (12 seconds each by default), so time spent creating a usable shell never starves its catalog claim. Detached creation requires billing API v1.7.12 or newer and `checkoutMode: 'full'`, and is skipped when `tokenizedData` is supplied. There is no fallback to the one-shot create; pass `deferDataAttachment: false` to force it. Because catalog validation moves to the claim, an invalid product or coupon surfaces as the same `422`, later in the flow. The claim body is fingerprinted by the billing API, so the SDK replays a byte-identical payload across retries, and a materially different claim for an already-claimed session returns `409` without retrying.
|
|
291
87
|
|
|
292
|
-
|
|
293
|
-
process / intent / decline calls on an unclaimed session with
|
|
294
|
-
`409 checkout_session_data_attachment_required`, and holds an unclaimed vault
|
|
295
|
-
charge with a retryable `503`. `@flopay/react` gates the card widget's submit
|
|
296
|
-
button for exactly this window.
|
|
88
|
+
### Server-side card gateways (Worldpay)
|
|
297
89
|
|
|
298
|
-
|
|
299
|
-
awaits the combined claim, so its resolved value and request contract are
|
|
300
|
-
unchanged. Detached creation requires billing API `v1.7.12` or newer,
|
|
301
|
-
`checkoutMode: 'full'`, is skipped when `tokenizedData` is supplied, and there
|
|
302
|
-
is **no fallback** to the one-shot create: it needs a billing API with the claim
|
|
303
|
-
endpoint. Pass `deferDataAttachment: false` to force the one-shot create.
|
|
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:
|
|
304
91
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
payload across transport retries; a materially different claim for an
|
|
309
|
-
already-claimed session returns `409` and is surfaced without retrying.
|
|
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
|
+
- 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.
|
|
310
95
|
|
|
311
|
-
|
|
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.
|
|
312
97
|
|
|
313
|
-
|
|
314
|
-
- `CouponCurrencyUnsupported` — an amount-based coupon has no price for the cart currency.
|
|
98
|
+
### Hosted card capture
|
|
315
99
|
|
|
316
|
-
|
|
317
|
-
import { FloPayError } from '@flopay/shared';
|
|
100
|
+
`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.
|
|
318
101
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
102
|
+
- 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()`.
|
|
104
|
+
- 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
|
+
- 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
|
+
- `capture.applyTheme(theme)` re-skins the mounted form without remounting; `applyTheme(null)` is the explicit classic reset. Theming is cosmetic and never blocks capture.
|
|
107
|
+
- `capture.setSubmitLabel(label)` sets the hosted submit button's wording live (billing API v1.9.35+), without remounting or clearing entered card data. Pass final, already-localized plain text; the hosted form shows its session default (`CONFIRM PAYMENT`, or `ADD CARD` for card setup) for blank or invalid text. `capture.setSubmitLabel(null)` is the explicit reset to that default, and a `submitLabel` mount option sets the initial wording.
|
|
108
|
+
|
|
109
|
+
The adapter retains the latest label and replays it
|
|
110
|
+
every time the widget reports an authenticated `ready`, so a late or remounted
|
|
111
|
+
form still receives it. Because the directive carries merchant text, it is
|
|
112
|
+
posted only when the vault block supplied both `messageToken` and
|
|
113
|
+
`expectedOrigin`, bound to the mounted `sessionId`, with the exact
|
|
114
|
+
`expectedOrigin` as the `postMessage` target — never `'*'`:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
{ source: 'flopay-vault-host', type: 'submitLabel', sessionId, messageToken, label }
|
|
330
118
|
```
|
|
331
119
|
|
|
332
|
-
|
|
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.
|
|
333
121
|
|
|
334
|
-
|
|
335
|
-
Corporate Gateway entirely server-side (TeamFloPay/backend#1563). Whether a
|
|
336
|
-
session can use it is the billing API's decision, made from the merchant's
|
|
337
|
-
active gateways and their Operations-approved capability record
|
|
338
|
-
(TeamFloPay/backend#1641). `PaymentAPI` sends no capability token, gateway
|
|
339
|
-
hint, or provider selection on any create, so the same request works for every
|
|
340
|
-
SDK version and integration style. Everything stays provider-neutral, so
|
|
341
|
-
existing integrations change nothing:
|
|
342
|
-
|
|
343
|
-
- Cards still go through the hosted vault widget, from the embedded `vault`
|
|
344
|
-
block or `getVaultCapture()`. There is no Worldpay browser SDK, key, or
|
|
345
|
-
credential: `gateways.worldpayCorporateGateway` carries
|
|
346
|
-
`publishableKey: null` and no enabled methods, and it never appears in the
|
|
347
|
-
normalized `providers` list.
|
|
348
|
-
- Outcomes arrive as the same `complete` / `decline` / `error` widget events.
|
|
349
|
-
Declines carry Flo's decline taxonomy, never a raw refusal code. An ambiguous
|
|
350
|
-
submission stays pending until the billing API reconciles it; the hosted form
|
|
351
|
-
reports that as an `error`, never a success, and a later session read returns
|
|
352
|
-
the reconciled status.
|
|
353
|
-
- In the merged billing API version, Worldpay takes one-time item carts only
|
|
354
|
-
and offers no 3DS authentication, stored credentials, or card setup; the
|
|
355
|
-
billing API keeps subscription carts on another card gateway. No
|
|
356
|
-
client-visible authentication continuation exists for Worldpay.
|
|
357
|
-
|
|
358
|
-
Auto, Confirm, and pre-tokenized sessions charge a stored payment method, which
|
|
359
|
-
the Worldpay route does not accept; the billing API keeps those on the released
|
|
360
|
-
Stripe and PayPal routing by itself. Do not add gateway selection, capability
|
|
361
|
-
headers, or provider-specific branches to a create request, whether through
|
|
362
|
-
this SDK or a request you build yourself: the billing API owns that decision,
|
|
363
|
-
and a client-side copy of it would become a second routing authority that
|
|
364
|
-
drifts from the merchant's real gateway state.
|
|
365
|
-
|
|
366
|
-
## API Reference
|
|
367
|
-
|
|
368
|
-
## Privacy-safe operational telemetry
|
|
369
|
-
|
|
370
|
-
Browser telemetry is enabled by default and is best-effort. It reports only
|
|
371
|
-
the closed SDK error/lifecycle/performance taxonomy through a Flo-owned API;
|
|
372
|
-
it never bundles Sentry, reads Resource Timing, persists a queue, or transmits
|
|
373
|
-
payment/customer/form data, credentials, URLs, provider payloads, arbitrary
|
|
374
|
-
messages, or stacks. Existing merchant callbacks remain available with their
|
|
375
|
-
existing arguments and invocation order. Callback exceptions and rejected
|
|
376
|
-
promises are contained and logged only to the merchant console; they are not
|
|
377
|
-
uploaded as SDK telemetry.
|
|
122
|
+
### Privacy-safe operational telemetry
|
|
378
123
|
|
|
379
|
-
|
|
380
|
-
const flopay = await loadFloPay('pk_live_…', {
|
|
381
|
-
telemetry: false, // merchant opt-out
|
|
382
|
-
});
|
|
124
|
+
Browser telemetry is on by default and best-effort. It reports only the closed SDK error, lifecycle, and performance taxonomy to a FloPay-owned API; it never bundles Sentry, reads Resource Timing, persists a queue, or transmits payment, customer, or form data, credentials, URLs, provider payloads, arbitrary messages, or stacks. Opt out with `telemetry: false` on `loadFloPay`, `PaymentAPI`, `PciVaultCardCapture`, `createCheckoutSession`, and `createCheckoutSessionWithRetries`. Merchant callbacks keep their arguments and invocation order; exceptions they throw are contained and logged only to your console.
|
|
383
125
|
|
|
384
|
-
|
|
385
|
-
const vault = new PciVaultCardCapture({ telemetry: false });
|
|
386
|
-
await createCheckoutSession({ /* normal checkout fields… */ telemetry: false });
|
|
387
|
-
```
|
|
126
|
+
Genuine technical failures carry one bounded category: `server_error` for billing API 5xx responses, `transport_error` when no usable response arrives, `invalid_response` for malformed responses, and `provider_runtime` for provider SDK or hosted-form runtime failures. Payment declines and request validation failures are reported as non-error `payment_declined` and `validation_rejected` outcomes.
|
|
388
127
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
The same default-on behavior and boolean opt-out apply to direct `FloPay`,
|
|
395
|
-
`PaymentAPI`, `createCheckoutSession`, `createCheckoutSessionWithRetries`, and
|
|
396
|
-
`PciVaultCardCapture` usage.
|
|
397
|
-
|
|
398
|
-
Genuine payment failures carry only a bounded technical category:
|
|
399
|
-
`server_error` for backend 5xx failures, `transport_error` when no usable
|
|
400
|
-
response arrives, `invalid_response` for malformed closed response shapes, and
|
|
401
|
-
`provider_runtime` for provider SDK or hosted-widget runtime failures. Hosted
|
|
402
|
-
vault widget errors use that category both before and after submission without
|
|
403
|
-
including widget messages or session data. Categories are never derived from
|
|
404
|
-
messages, response bodies, decline text, stacks, or identifiers. Backend payment
|
|
405
|
-
declines and request validation failures use the non-error `payment_declined`
|
|
406
|
-
and `validation_rejected` outcomes and carry no category. Uncategorized technical
|
|
407
|
-
events remain serializable for backend rollout compatibility.
|
|
408
|
-
|
|
409
|
-
Hosts with their own Sentry client can filter recognized third-party-only
|
|
410
|
-
browser errors without suppressing app-owned failures:
|
|
128
|
+
When telemetry is enabled, a Stripe loader rejection is reported only with the bounded `provider_runtime` category, while a null Stripe loader result uses the distinct `invalid_response` category. Neither changes the thrown error or the failed-load retry behavior, and neither transmits the publishable key, rejection, provider message, stack, URL, or provider payload.
|
|
129
|
+
|
|
130
|
+
`FloPayCheckout` in `@flopay/react` also classifies failures around the session and provider boundary without exposing raw errors. An existing session-read transport failure uses `transport_error`; malformed JSON uses `invalid_response`.
|
|
131
|
+
|
|
132
|
+
### Filter third-party errors in your own Sentry client
|
|
411
133
|
|
|
412
134
|
```ts
|
|
413
135
|
import * as Sentry from '@sentry/browser';
|
|
@@ -416,163 +138,236 @@ import { dropThirdPartyOnlyError } from '@flopay/js';
|
|
|
416
138
|
Sentry.init({ beforeSend: dropThirdPartyOnlyError });
|
|
417
139
|
```
|
|
418
140
|
|
|
419
|
-
This
|
|
420
|
-
integration. It drops only complete exception stacks with no resolvable app or
|
|
421
|
-
`@flopay/*` frame and at least one browser-extension URL or Stripe controller
|
|
422
|
-
marker (`dahlia/stripe.js` path segment or `dahlia/stripe` module). Anonymous
|
|
423
|
-
frames may accompany those markers. Mixed, unrelated, incomplete, and
|
|
424
|
-
anonymous-only errors are returned unchanged. Detection ignores `in_app`,
|
|
425
|
-
which can be rewritten to `true` for third-party frames by host tooling.
|
|
141
|
+
This pure predicate drops only complete exception stacks with no app or `@flopay/*` frame and at least one browser-extension URL or Stripe `dahlia/stripe.js` controller frame. Mixed, unrelated, incomplete, and anonymous-only errors are returned unchanged, and Sentry's `in_app` flag is ignored.
|
|
426
142
|
|
|
427
|
-
|
|
143
|
+
## Examples
|
|
428
144
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
| `StripeAdapter` | `PaymentProviderAdapter` implementation for Stripe |
|
|
434
|
-
| `PciVaultCardCapture` | `CardCaptureAdapter` implementation that injects the backend-served hosted vault card widget. See [Vault card capture](#vault-card-capture). |
|
|
435
|
-
| `PaymentAPI` | Billing API client. Includes session retrieval/creation, vault recovery, `/process`, `createSessionIntent()` (including PayPal payment/setup continuations), `reportSessionIntentDecline()`, completion polling, and saved-payment lookup. Normalized direct-PayPal config preserves the backend's bounded `providerObjectType` and `approvalPresentation`; it never accepts a return URL. |
|
|
436
|
-
| `createCheckoutSession(options)` | Creates a checkout session and redirects. Returns `CheckoutSessionResult`. |
|
|
437
|
-
| `createCheckoutSessionWithRetries(options)` | Same as above with two retries by default (three total attempts) under one total deadline with jittered exponential backoff. |
|
|
438
|
-
| `dropThirdPartyOnlyError(event)` | Host-owned Sentry `beforeSend` predicate. Drops recognized third-party-only exception stacks and otherwise returns the original event. |
|
|
439
|
-
| `toStripeAppearance(appearance)` | Normalizes a `FloPayAppearance` into a `StripeSafeAppearance` by mapping FloPay's public `theme` token (`'default' \| 'flat' \| 'night' \| 'none'`) onto Stripe's accepted set (`'stripe' \| 'flat' \| 'night'`). Use at any site that hands an appearance to `@stripe/react-stripe-js`'s `<Elements>` so `theme: 'default'` never reaches Stripe.js. `variables`/`rules` pass through untouched. |
|
|
440
|
-
| `toStripeAppearanceTheme(theme)` | Lower-level helper backing `toStripeAppearance`: maps a single FloPay `theme` token to Stripe's. |
|
|
145
|
+
Initialize the SDK and read a session's gateways:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { loadFloPay } from '@flopay/js';
|
|
441
149
|
|
|
442
|
-
|
|
150
|
+
const flopay = await loadFloPay('pk_test_...', { billingApiUrl: 'https://billing.example.com' });
|
|
443
151
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
152
|
+
const session = await flopay.retrieveSession('session_uuid');
|
|
153
|
+
console.log(session.amount, session.currency, session.status);
|
|
154
|
+
|
|
155
|
+
const unified = await flopay.retrieveUnifiedSession('session_uuid');
|
|
156
|
+
if (unified.providers.includes('stripe')) {
|
|
157
|
+
console.log('Stripe publishable key:', unified.data.stripe?.publishableKey);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
451
160
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
`flopay.cardCapture()` returns a `CardCaptureAdapter` that injects a
|
|
455
|
-
**backend-served, self-contained hosted vault widget**
|
|
456
|
-
(TeamFloPay/backend#823, Model A). The widget owns the PCI
|
|
457
|
-
card fields, its own submit button, card tokenization, the PaymentIntent (created
|
|
458
|
-
**and** confirmed server-side), **3DS**, and the result — so **no Stripe.js runs
|
|
459
|
-
on the card path** and PAN / CVC never enter the SDK runtime. The SDK's only job
|
|
460
|
-
is to inject the widget HTML and relay its terminal `postMessage` outcome.
|
|
461
|
-
|
|
462
|
-
For a merchant-server-created no-charge setup session, `@flopay/react` uses
|
|
463
|
-
the same adapter with `operation: 'card_setup'`. A valid terminal `complete`
|
|
464
|
-
message may include `paymentMethod`, a reconstructed display-only object with
|
|
465
|
-
FloPay id, brand, last four, expiry, and status. Unknown or malformed nested
|
|
466
|
-
fields are dropped. During a mixed backend rollout the field may be absent; the
|
|
467
|
-
result stays bound to the setup session and the SDK never invents an id. Setup
|
|
468
|
-
terminal telemetry uses `card_setup_succeeded` / `card_setup_declined`, not
|
|
469
|
-
payment-success outcomes.
|
|
161
|
+
Create a hosted checkout session with metadata and a merchant-controlled idempotency key:
|
|
470
162
|
|
|
471
163
|
```ts
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
164
|
+
import { createCheckoutSessionWithRetries } from '@flopay/js';
|
|
165
|
+
import { FloPayError } from '@flopay/shared';
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = await createCheckoutSessionWithRetries({
|
|
169
|
+
billingApiUrl: 'https://billing.example.com',
|
|
170
|
+
checkoutBaseUrl: 'https://checkout.example.com',
|
|
171
|
+
clientId: 'client_123',
|
|
172
|
+
currency: 'USD',
|
|
173
|
+
items: [{ providerItemId: 'prod_abc', providerItemName: 'Pro Plan', totalAmount: 49.99 }],
|
|
174
|
+
account: { userId: 'user_1', email: 'user@example.com' },
|
|
175
|
+
successUrl: '/success',
|
|
176
|
+
cancelUrl: '/cancel',
|
|
177
|
+
couponCodes: ['SAVE10'],
|
|
178
|
+
checkoutMetadata: { merchantOrderId: 'order-123', campaign: 'summer' },
|
|
179
|
+
idempotencyKey: 'checkout:order-123',
|
|
180
|
+
});
|
|
181
|
+
console.log('Checkout session created with status', result.status);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error instanceof FloPayError && error.code === 'CouponLimitExceeded') {
|
|
184
|
+
console.warn('Too many coupon codes.');
|
|
185
|
+
} else if (error instanceof FloPayError && error.code === 'CouponCurrencyUnsupported') {
|
|
186
|
+
console.warn('A coupon is not valid for this currency.');
|
|
187
|
+
} else {
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Create a detached session, mount the card form from the shell, and identify the buyer later:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { PaymentAPI } from '@flopay/js';
|
|
197
|
+
|
|
198
|
+
const api = new PaymentAPI('https://billing.example.com');
|
|
199
|
+
|
|
200
|
+
const detached = await api.createDetachedSession({
|
|
201
|
+
clientId: 'client_123',
|
|
202
|
+
currency: 'EUR',
|
|
203
|
+
items: [{ providerItemId: 'prod_abc', providerItemName: 'Widget', totalAmount: 29.99 }],
|
|
204
|
+
account: { userId: 'buyer_123' },
|
|
205
|
+
successUrl: '/success',
|
|
206
|
+
cancelUrl: '/cancel',
|
|
485
207
|
});
|
|
208
|
+
|
|
209
|
+
console.log('Card form can mount from', detached.shell.data.session?.vault);
|
|
210
|
+
const anonymous = await detached.claimed; // catalog, totals, and methods; buyerIdentified is false
|
|
211
|
+
const identified = await detached.claim({ email: 'buyer@example.com', firstName: 'Jane', lastName: 'Doe' });
|
|
212
|
+
console.log(anonymous.data.session?.buyerIdentified, identified.data.session?.buyerIdentified);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Mount the hosted card form and relay its outcome:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { loadFloPay, PaymentAPI } from '@flopay/js';
|
|
219
|
+
import type { PaymentResult } from '@flopay/shared';
|
|
220
|
+
|
|
221
|
+
const billingApiUrl = 'https://billing.example.com';
|
|
222
|
+
const sessionId = 'session_uuid';
|
|
223
|
+
const nonce = 'session_nonce';
|
|
224
|
+
|
|
225
|
+
function onComplete(result: PaymentResult) {
|
|
226
|
+
console.log('Checkout finished:', result.status, result.paymentId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const flopay = await loadFloPay('pk_test_...', { billingApiUrl });
|
|
230
|
+
const session = await flopay.retrieveSession(sessionId);
|
|
231
|
+
const vault = session.vault ?? (await new PaymentAPI(billingApiUrl).getVaultCapture(sessionId, nonce));
|
|
232
|
+
|
|
233
|
+
const capture = flopay.cardCapture({ sessionId, captureMethod: session.captureMethod });
|
|
486
234
|
capture.on('complete', (e) => {
|
|
487
235
|
if (e.outcome === 'authorized' || session.captureMethod === 'manual') {
|
|
488
236
|
onComplete({
|
|
489
237
|
status: 'authorized',
|
|
490
|
-
paymentIntentId: e.intentId,
|
|
491
238
|
paymentId: e.paymentId,
|
|
492
239
|
sessionId: e.sessionId,
|
|
493
240
|
authorizationExpiresAt: e.authorizationExpiresAt,
|
|
494
241
|
});
|
|
495
242
|
return;
|
|
496
243
|
}
|
|
497
|
-
onComplete({ status: 'succeeded',
|
|
244
|
+
onComplete({ status: 'succeeded', paymentId: e.paymentId, sessionId: e.sessionId });
|
|
498
245
|
});
|
|
499
|
-
capture.on('decline', (e) =>
|
|
500
|
-
capture.on('error', (e) =>
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
246
|
+
capture.on('decline', (e) => console.warn('Card declined:', e.declineReason));
|
|
247
|
+
capture.on('error', (e) => console.error('Card form failed:', e.message));
|
|
248
|
+
|
|
249
|
+
const container = document.getElementById('card-form');
|
|
250
|
+
if (container && vault.html) {
|
|
251
|
+
await capture.mount(container, {
|
|
252
|
+
html: vault.html,
|
|
253
|
+
messageToken: vault.messageToken,
|
|
254
|
+
expectedOrigin: vault.expectedOrigin,
|
|
255
|
+
submitLabel: 'Pay €29.99',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
504
258
|
```
|
|
505
259
|
|
|
506
|
-
|
|
507
|
-
Stripe PaymentIntent (or SetupIntent) id only when Stripe took the card; when
|
|
508
|
-
the session advertises a server-side card gateway such as
|
|
509
|
-
`gateways.worldpayCorporateGateway`, it may be an opaque processor order
|
|
510
|
-
reference instead. Reconcile by `paymentId` and the session rather than
|
|
511
|
-
treating it as a Stripe object. `FloPayCheckout` and `SplitCardForm` omit
|
|
512
|
-
`paymentIntentId` from `onComplete` for those sessions.
|
|
513
|
-
|
|
514
|
-
`capture.applyTheme(theme)` re-skins the mounted widget without remounting it.
|
|
515
|
-
Pass the `VaultCardThemeColors` the React components compute: the flat keys
|
|
516
|
-
every hosted asset understands plus the optional `submitButton` snapshot
|
|
517
|
-
(billing API v1.9.6+) — an older hosted asset ignores the snapshot and keeps the
|
|
518
|
-
solid `primaryColor` approximation, so both sides of a rolling deployment stay
|
|
519
|
-
capture-safe. `capture.applyTheme(null)` is the explicit classic reset: the
|
|
520
|
-
hosted form restores every historical neutral value, and the adapter retains the
|
|
521
|
-
directive so a widget that becomes ready later replays it. Theming is cosmetic
|
|
522
|
-
and failure-tolerant — an unsupported member never blocks capture or touches
|
|
523
|
-
card data.
|
|
524
|
-
|
|
525
|
-
`capture.setSubmitLabel(label)` sets the hosted card form's final submit
|
|
526
|
-
wording live (billing API v1.9.35+), for example `'Pay $10'`, without
|
|
527
|
-
remounting the widget or clearing entered card data. Pass final,
|
|
528
|
-
already-localized plain text; the hosted form normalizes it, renders it as
|
|
529
|
-
inert text, and shows its session default — `CONFIRM PAYMENT`, or `ADD CARD`
|
|
530
|
-
for card setup — for blank or invalid text. `capture.setSubmitLabel(null)` is
|
|
531
|
-
the explicit reset to that default, and a `submitLabel` mount option sets the
|
|
532
|
-
initial wording. The adapter retains the latest label and replays it
|
|
533
|
-
every time the widget reports an authenticated `ready`, so a late or remounted
|
|
534
|
-
form still receives it. Because the directive carries merchant text, it is
|
|
535
|
-
posted only when the vault block supplied both `messageToken` and
|
|
536
|
-
`expectedOrigin`, bound to the mounted `sessionId`, with the exact
|
|
537
|
-
`expectedOrigin` as the `postMessage` target — never `'*'`:
|
|
260
|
+
Create a wallet intent and report a client-observed decline:
|
|
538
261
|
|
|
539
262
|
```ts
|
|
540
|
-
{
|
|
263
|
+
import { PaymentAPI } from '@flopay/js';
|
|
264
|
+
|
|
265
|
+
const api = new PaymentAPI('https://billing.example.com', { telemetry: false });
|
|
266
|
+
|
|
267
|
+
const intent = await api.createSessionIntent('session_uuid', 'session_nonce', {
|
|
268
|
+
provider: 'stripe',
|
|
269
|
+
paymentMethodCategory: 'wallet',
|
|
270
|
+
paymentMethodType: 'apple_pay',
|
|
271
|
+
paymentMethodId: null,
|
|
272
|
+
intentKind: 'payment',
|
|
273
|
+
});
|
|
274
|
+
if (intent.provider === 'stripe') {
|
|
275
|
+
console.log('Confirm the wallet payment with', intent.clientSecret);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
await api.reportSessionIntentDecline('session_uuid', 'session_nonce', {
|
|
279
|
+
provider: 'stripe',
|
|
280
|
+
paymentMethodCategory: 'wallet',
|
|
281
|
+
paymentMethodType: 'apple_pay',
|
|
282
|
+
providerDeclineReason: 'payment_failed',
|
|
283
|
+
});
|
|
541
284
|
```
|
|
542
285
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
`
|
|
554
|
-
|
|
555
|
-
`
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
`
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
286
|
+
## Public API
|
|
287
|
+
|
|
288
|
+
### Exports
|
|
289
|
+
|
|
290
|
+
| Export | Description |
|
|
291
|
+
|--------|-------------|
|
|
292
|
+
| `loadFloPay(publishableKey, options?)` | Initializes the SDK and resolves a `FloPay`. Shares successful and in-flight work by key and options; failed work stays retryable. |
|
|
293
|
+
| `FloPay` | Main SDK class, created by `loadFloPay()` |
|
|
294
|
+
| `PaymentAPI` | Billing API client: `new PaymentAPI(billingApiUrl, { telemetry })` |
|
|
295
|
+
| `createCheckoutSession(options)` | Creates a checkout session and redirects. Resolves a `CheckoutSessionResult`. |
|
|
296
|
+
| `createCheckoutSessionWithRetries(options)` | Like `createCheckoutSession`, with up to two retries (three attempts) inside one total deadline, using jittered exponential backoff |
|
|
297
|
+
| `PciVaultCardCapture` | `CardCaptureAdapter` implementation that injects the billing API's hosted card form |
|
|
298
|
+
| `CardCaptureOperation` | `'checkout'` / `'card_setup'` |
|
|
299
|
+
| `PciVaultCardCaptureConfig` | Adapter configuration: `sessionId?`, `captureMethod?`, `operation?`, `expectedOrigin?`, `telemetry?` |
|
|
300
|
+
| `StripeAdapter` | `PaymentProviderAdapter` implementation for Stripe |
|
|
301
|
+
| `toStripeAppearance(appearance)` | Converts a `FloPayAppearance` into a `StripeSafeAppearance`, mapping FloPay's `theme` token (`'default'`, `'flat'`, `'night'`, `'none'`) onto Stripe's accepted set. Use it wherever you hand an appearance to Stripe Elements yourself. |
|
|
302
|
+
| `toStripeAppearanceTheme(theme)` | Lower-level helper that maps a single FloPay `theme` token to Stripe's |
|
|
303
|
+
| `StripeSafeAppearance` | An appearance Stripe Elements accepts |
|
|
304
|
+
| `cacheSessionDisplayData(sessionId, data, options?)` | Stashes display-only session data client-side so it survives the redirect to a checkout page |
|
|
305
|
+
| `getSessionDisplayData(sessionId)` | Reads cached display data, or `null` when none is cached or it expired |
|
|
306
|
+
| `clearSessionDisplayData(sessionId)` | Drops cached display data, for example on the success page |
|
|
307
|
+
| `SessionDisplayCacheData` | Display-only data stored by `cacheSessionDisplayData()` |
|
|
308
|
+
| `dropThirdPartyOnlyError(event)` | Sentry `beforeSend` predicate that drops recognized third-party-only exception stacks and otherwise returns the original event |
|
|
309
|
+
|
|
310
|
+
### FloPay methods
|
|
311
|
+
|
|
312
|
+
| Method | Returns | Description |
|
|
313
|
+
|--------|---------|-------------|
|
|
314
|
+
| `cardCapture(options?)` | `CardCaptureAdapter` | Creates a hosted card-form adapter bound to `sessionId` and `captureMethod` |
|
|
315
|
+
| `retrieveSession(sessionId, billingApiUrl?)` | `Promise<CheckoutSession>` | Reads a session through `GET /v1/checkouts/sessions/{id}` |
|
|
316
|
+
| `retrieveUnifiedSession(sessionId, billingApiUrl?)` | `Promise<NormalizedCheckoutSession>` | Reads and normalizes a session, including gateway data |
|
|
317
|
+
| `getRawProvider()` | `unknown` | The underlying provider instance, such as the Stripe object |
|
|
318
|
+
| `destroy()` | `void` | Tears down telemetry and the provider |
|
|
319
|
+
|
|
320
|
+
### PaymentAPI methods
|
|
321
|
+
|
|
322
|
+
| Method | Returns | Description |
|
|
323
|
+
|--------|---------|-------------|
|
|
324
|
+
| `getUnifiedCheckoutSession(sessionId, nonce?)` | `Promise<NormalizedCheckoutSession>` | Reads and normalizes a session |
|
|
325
|
+
| `getVaultCapture(sessionId, nonce?)` | `Promise<VaultCaptureBlock>` | Fetches the hosted card form when the session has no embedded `vault` block |
|
|
326
|
+
| `createAndFetchSession(params)` | `Promise<NormalizedCheckoutSession>` | Creates an inline session and resolves once its catalog (and any supplied buyer) is attached |
|
|
327
|
+
| `createDetachedSession(params)` | `Promise<DetachedCheckoutSession>` | Creates an inline session in two phases and exposes both |
|
|
328
|
+
| `claimCheckoutSession(sessionId, nonce, payload)` | `Promise<NormalizedCheckoutSession>` | Sends a `PATCH /v1/checkouts/sessions/{id}/claim` |
|
|
329
|
+
| `createSessionIntent(sessionId, nonce, request, options?)` | `Promise<SessionIntent>` | Creates a Stripe wallet/APM or direct-PayPal intent |
|
|
330
|
+
| `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 |
|
|
332
|
+
| `getPaymentsByEmail(email, options?)` | `Promise<{ data, total, page, limit }>` | Lists prior payments for an email |
|
|
333
|
+
| `destroy()` | `void` | Tears down the client's telemetry |
|
|
334
|
+
|
|
335
|
+
### PciVaultCardCapture methods
|
|
336
|
+
|
|
337
|
+
| Method | Description |
|
|
338
|
+
|--------|-------------|
|
|
339
|
+
| `mount(container, { html, messageToken?, expectedOrigin?, submitLabel? })` | Injects the hosted card form |
|
|
340
|
+
| `on(event, handler)` | Subscribes to `ready`, `complete`, `decline`, or `error` |
|
|
341
|
+
| `unmount()` | Removes the form and its listeners |
|
|
342
|
+
| `applyTheme(theme)` | Re-skins the mounted form; `null` is the classic reset |
|
|
343
|
+
| `setSubmitGate(blocked)` | While `blocked`, the hosted form cancels its next submit and emits `blocked` so the host can validate its own fields (such as AVS) first |
|
|
344
|
+
| `setCardFieldOrder(order, autoFocus)` | Reorders the hosted card rows (`name`, `number`, `expiry`); the first row is focused unless `autoFocus` is false |
|
|
345
|
+
| `setSubmitLabel(label)` | Sets the hosted submit wording live; `null` restores the session default |
|
|
346
|
+
|
|
347
|
+
## Errors
|
|
348
|
+
|
|
349
|
+
Failures reject with `FloPayError` from `@flopay/shared`. Branch on `type` and `code`, never on `message`. When the billing API returns a structured error body, the SDK keeps its safe `message`, `code`, and HTTP `statusCode`; a body-less response falls back to `code: 'http_<status>'`.
|
|
350
|
+
|
|
351
|
+
| Code | Type | When |
|
|
352
|
+
|------|------|------|
|
|
353
|
+
| `CurrencyRequired` | `validation_error` | Session creation could not resolve a currency from the session, items, subscriptions, or products. Thrown before any request. |
|
|
354
|
+
| `CouponLimitExceeded` | `validation_error` or `api_error` | More than five coupon codes were supplied: a `validation_error` before the request, or an `api_error` from the billing API. |
|
|
355
|
+
| `CouponCurrencyUnsupported` | `api_error` | An amount-based coupon has no price in the cart currency. |
|
|
356
|
+
| `CaptureMethodUnsupportedForSubscription` | `validation_error` | Manual capture was requested for a cart with a subscription. |
|
|
357
|
+
| `InvalidIdempotencyKey` | `validation_error` | A supplied `idempotencyKey` is blank or longer than 255 characters. Thrown before any request. |
|
|
358
|
+
| `IdempotencyKeyInProgress` | `api_error` | The same key is still being processed. Retried automatically with the same key within the deadline. |
|
|
359
|
+
| `MissingCheckoutSessionToken` | `validation_error` or `api_error` | A continuation call was made without the session nonce, or the billing API created a session without returning one. |
|
|
360
|
+
| `BuyerEmailCollectionRequired` | `validation_error` | A buyer email supplied for creation or a late claim is not a valid email address. |
|
|
361
|
+
| `checkout_session_data_attachment_required` | `api_error` | A process, intent, or decline call was made before the detached session's catalog claim settled (HTTP 409). Wait for `claimed`. |
|
|
362
|
+
| `InvalidSessionIntentRequest` | `validation_error` | An intent request is not a supported non-card or PayPal request. |
|
|
363
|
+
| `InvalidAuthorizationAttemptId` | `validation_error` | An intent request's authorization attempt id is not a valid UUID. |
|
|
364
|
+
| `InvalidSessionIntentDeclineRequest` | `validation_error` | A decline report carries anything other than safe classification codes. |
|
|
365
|
+
| `InvalidSessionIntentResponse` | `api_error` | The billing API returned an intent that does not match the request. |
|
|
366
|
+
| `InvalidCheckoutSessionResponse` | `api_error` | The billing API returned a malformed session or no session id. |
|
|
367
|
+
| `session_auto_completed` | `api_error` | Inline creation found a payment method already on file and completed the session without a checkout. |
|
|
368
|
+
| `checkout_processing_timeout` | `api_error` | `processPayment()` stopped polling while the checkout was still processing. Read the session again shortly. |
|
|
369
|
+
| `vault_capture_timeout` | `api_error` | `getVaultCapture()` did not return the hosted card form within ten seconds. |
|
|
370
|
+
| `card_capture_no_window` | `api_error` | `PciVaultCardCapture.mount()` ran outside a browser. |
|
|
371
|
+
| `card_capture_no_widget_html` | `api_error` | `PciVaultCardCapture.mount()` was called without hosted form HTML. |
|
|
372
|
+
|
|
373
|
+
Card declines are not thrown: they arrive as the card form's `decline` event with a `declineReason`, and technical failures as its `error` event. A product or coupon rejected during the detached catalog claim surfaces as the billing API's `422` error from `claimed`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flopay/js",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@stripe/stripe-js": "^9.8.0",
|
|
35
|
-
"@flopay/shared": "1.9.
|
|
35
|
+
"@flopay/shared": "1.9.18"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"jsdom": "^29.1.1",
|