@flopay/react 1.9.15 → 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 +466 -1104
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @flopay/react
|
|
2
2
|
|
|
3
|
-
React bindings for the FloPay SDK
|
|
3
|
+
React bindings for the FloPay SDK: a drop-in `FloPayCheckout`, a no-charge `FloPayCardSetup`, a one-click `FloPayAutomaticPaymentButton`, composable checkout components, and hooks. Cards are collected by the billing API's hosted PCI card form; wallets, alternative payment methods, and PayPal render beside it from the session's gateways.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,14 +8,11 @@ React bindings for the FloPay SDK. Provides a context provider, drop-in checkout
|
|
|
8
8
|
pnpm add @flopay/react @flopay/js @flopay/shared react react-dom
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Peer dependencies: `react >= 18.0.0
|
|
11
|
+
Peer dependencies: `react >= 18.0.0` and `react-dom >= 18.0.0`. Install the same version of every `@flopay/*` package.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
Configure which billing API the SDK uses. Choose one:
|
|
13
|
+
Configure which billing API the SDK uses, once. Either set an environment variable:
|
|
16
14
|
|
|
17
15
|
```bash
|
|
18
|
-
# Option A: Environment variable (recommended)
|
|
19
16
|
# .env.production
|
|
20
17
|
NEXT_PUBLIC_FLOPAY_ENV=production
|
|
21
18
|
|
|
@@ -23,1253 +20,618 @@ NEXT_PUBLIC_FLOPAY_ENV=production
|
|
|
23
20
|
NEXT_PUBLIC_FLOPAY_ENV=staging
|
|
24
21
|
```
|
|
25
22
|
|
|
23
|
+
or call `configureFlopay()` at startup:
|
|
24
|
+
|
|
26
25
|
```ts
|
|
27
|
-
// Option B: Call at app startup
|
|
28
26
|
import { configureFlopay } from '@flopay/shared';
|
|
27
|
+
|
|
29
28
|
configureFlopay({ environment: 'production' });
|
|
30
29
|
```
|
|
31
30
|
|
|
32
|
-
If neither is set, the SDK
|
|
31
|
+
If neither is set, the SDK uses the staging billing API.
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
**Payment-method logos and CSP.** Brand logos load from the versioned, FloPay-owned URL `https://cdn.flopay.com/sdk-logos/v1/`. Plain absolute HTTPS URLs work in Vite with no SDK middleware or `optimizeDeps` override, and behave the same in webpack 5, Rollup, Next, and esbuild. Logos stay out of the package and your JavaScript bundle, and card-only and hosted-card checkouts request none. With a strict Content Security Policy, allow the host in your image directive, for example `img-src 'self' https://cdn.flopay.com;`. A failed or CSP-blocked logo request falls back to the method's inline monogram, so the tile stays branded without another request.
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
## Usage
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
### FloPayCheckout
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
import { FloPayCheckout } from '@flopay/react';
|
|
39
|
+
`FloPayCheckout` is the recommended integration. Give it a session and it fetches the session, initializes the right payment providers, and renders the hosted card form plus the session's supported wallets, alternative payment methods, and PayPal. Customer data (email, user id, name) comes from the session.
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
onError={(err) => console.error(err)}
|
|
51
|
-
/>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
41
|
+
```tsx
|
|
42
|
+
<FloPayCheckout
|
|
43
|
+
sessionId="sess_abc123"
|
|
44
|
+
nonce={sessionNonce}
|
|
45
|
+
onComplete={(result) => router.push('/success')}
|
|
46
|
+
onError={(error) => console.error(error)}
|
|
47
|
+
/>
|
|
54
48
|
```
|
|
55
49
|
|
|
56
|
-
`
|
|
57
|
-
|
|
58
|
-
#### Buyer identity collection
|
|
50
|
+
Pass `nonce`, the session-bound checkout token returned when the session was created, whenever you pass an existing `sessionId`.
|
|
59
51
|
|
|
60
|
-
|
|
61
|
-
but `email`, `firstName`, and `lastName` may be omitted. `FloPayCheckout`
|
|
62
|
-
creates a mountable session shell first, immediately attaches the known catalog,
|
|
63
|
-
and collects buyer identity at the method-appropriate point:
|
|
64
|
-
|
|
65
|
-
- Hosted card checkout renders a required email field first, above name, AVS,
|
|
66
|
-
and the hosted card widget. Submit stays gated until the email passes the
|
|
67
|
-
shared `isValidEmail()` validator and a name is present.
|
|
68
|
-
- Apple Pay, Google Pay, other Stripe wallets, and PayPal use provider-returned
|
|
69
|
-
email/name automatically when available. Their provider UI starts without a
|
|
70
|
-
Flo identity preflight.
|
|
71
|
-
- A wallet, PayPal, or APM that does not return the missing fields opens a
|
|
72
|
-
single required fallback pane after that provider miss (or before submission
|
|
73
|
-
for a method that cannot return identity).
|
|
74
|
-
|
|
75
|
-
The SDK never attaches a blank email. The merchant-supplied `userId` is not
|
|
76
|
-
replaced by provider data. A legitimate merchant- or session-supplied buyer
|
|
77
|
-
name remains authoritative over a different wallet name. The exact generic
|
|
78
|
-
placeholder `Card Holder Name` is instead treated as missing after case and
|
|
79
|
-
whitespace normalization, so an authorised wallet name can replace it. Name
|
|
80
|
-
selection is atomic: first and last name always come from one source, and are
|
|
81
|
-
omitted when neither source is usable. Email precedence is unchanged.
|
|
82
|
-
|
|
83
|
-
#### Wallet confirmation recovery
|
|
84
|
-
|
|
85
|
-
Google Pay and Apple Pay authentication may legitimately outlive the SDK's
|
|
86
|
-
provider observation threshold. The SDK does not cancel that provider promise.
|
|
87
|
-
Instead, it retrieves the original PaymentIntent or SetupIntent with the same
|
|
88
|
-
client secret and routes the provider promise and status read through one
|
|
89
|
-
settlement gate:
|
|
90
|
-
|
|
91
|
-
- an authoritative success continues to `/process` exactly once;
|
|
92
|
-
- `canceled` or `requires_payment_method` is treated as authoritative,
|
|
93
|
-
non-chargeable terminal evidence and exposes wallet retry;
|
|
94
|
-
- `requires_action`, an unavailable/mismatched status, or a failed/bounded-out
|
|
95
|
-
status read keeps payment submission locked and exposes only **Check payment
|
|
96
|
-
status**.
|
|
97
|
-
|
|
98
|
-
Rejected confirmations, returned technical errors, and malformed fulfilled
|
|
99
|
-
results use this same recovery contract. A lost intent-create response also
|
|
100
|
-
reuses the authorization-attempt UUID on deliberate retry, so provider
|
|
101
|
-
idempotency cannot mint a second intent. A bounded pending-confirmation handoff
|
|
102
|
-
keeps teardown/remount from exposing another wallet surface while the first
|
|
103
|
-
intent may still complete. Existing component props and merchant callbacks are
|
|
104
|
-
unchanged.
|
|
52
|
+
#### Checkout modes
|
|
105
53
|
|
|
106
|
-
|
|
54
|
+
`checkoutMode` matches the billing API's `checkoutMode` and overrides the session's value:
|
|
107
55
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
and `gateway` is included as `stripe` or `paypal` only when the event is
|
|
112
|
-
attributable to that payment provider. Hosted-vault events may omit `gateway`,
|
|
113
|
-
and `card_expanded` always does when the session can route cards to a
|
|
114
|
-
[server-side card gateway](#server-side-card-gateways-worldpay).
|
|
56
|
+
- **`full`** (default): the hosted card form plus supported non-card methods.
|
|
57
|
+
- **`confirm`**: hides the payment form and shows a confirm button that pays with the buyer's saved payment method. If the saved card needs re-authentication, the 3DS challenge runs in place and the same attempt completes. `confirmLabel` labels that button.
|
|
58
|
+
- **`auto`**: pays with the saved payment method as soon as the session loads, falling back to `full` if that cannot complete.
|
|
115
59
|
|
|
116
|
-
|
|
117
|
-
import type { FloInstrumentEvent } from '@flopay/react';
|
|
118
|
-
|
|
119
|
-
function forwardCheckoutInstrument(event: FloInstrumentEvent) {
|
|
120
|
-
window.analytics?.track(event.name, event);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
<FloPayCheckout sessionId={sessionId} onInstrument={forwardCheckoutInstrument} />;
|
|
124
|
-
```
|
|
60
|
+
#### Buyer identity collection
|
|
125
61
|
|
|
126
|
-
|
|
127
|
-
`card_expanded`, `tokenize`, `process_attempt`, and `3ds_challenge`.
|
|
128
|
-
`checkout_mount`, `sdk_loaded`, `form_rendered`, and `card_expanded` arrive at
|
|
129
|
-
most once per logical checkout. `tokenize`, `process_attempt`, and
|
|
130
|
-
`3ds_challenge` may repeat for each attempt.
|
|
131
|
-
`checkout_error` adds one of four phases: `session_create`, `sdk_load`,
|
|
132
|
-
`process`, or `wallets`.
|
|
62
|
+
For inline session creation, `createSession.account.userId` is required but `email`, `firstName`, and `lastName` may be omitted. The checkout then collects identity at the right point for each method:
|
|
133
63
|
|
|
134
|
-
The
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
backend telemetry setting, so `telemetry={false}` does not disable it.
|
|
64
|
+
- The hosted card checkout shows a required email field above name, AVS, and the card form. Submit stays gated until the email passes `isValidEmail()` and a name is present.
|
|
65
|
+
- Apple Pay, Google Pay, other Stripe wallets, and PayPal use the email and name the provider returns. Their provider UI opens without a FloPay identity step first.
|
|
66
|
+
- A method that does not return the missing fields opens one required fallback pane after the provider returns (or before submission, for methods that cannot return identity).
|
|
138
67
|
|
|
139
|
-
|
|
68
|
+
The SDK never attaches a blank email and never replaces your `userId` with provider data. A buyer name you or the session supplied stays authoritative over a different wallet name, except the placeholder `Card Holder Name`, which an authorised wallet name can replace. First and last name always come from one source.
|
|
140
69
|
|
|
141
|
-
|
|
70
|
+
#### Inline session creation
|
|
142
71
|
|
|
143
|
-
-
|
|
144
|
-
- **`confirm`** — Hides the payment form and shows a "Confirm Purchase" button. Uses a saved payment method on the backend. When the saved card needs re-authentication, the 3DS challenge runs in place and the same payment attempt completes — the button is not replaced by the card form.
|
|
145
|
-
- **`auto`** — Auto-submits with a saved payment method after the session loads. Falls back to `full` mode on failure.
|
|
72
|
+
Skip your own session-creation route and create the session inside the component with `createSession`:
|
|
146
73
|
|
|
147
74
|
```tsx
|
|
148
|
-
// Confirm mode — one-click purchase with saved card
|
|
149
|
-
<FloPayCheckout
|
|
150
|
-
sessionId="sess_abc123"
|
|
151
|
-
checkoutMode="confirm"
|
|
152
|
-
confirmLabel="Complete Purchase"
|
|
153
|
-
onComplete={(result) => router.push('/success')}
|
|
154
|
-
/>
|
|
155
|
-
|
|
156
|
-
// Auto mode — instant checkout, falls back to full form
|
|
157
75
|
<FloPayCheckout
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
76
|
+
createSession={{
|
|
77
|
+
clientId: 'your-client-id',
|
|
78
|
+
currency: 'EUR',
|
|
79
|
+
items: [{ providerItemId: 'product-1', providerItemName: 'Widget', totalAmount: 29.99 }],
|
|
80
|
+
account: { userId: 'user_1' }, // email and name are collected in checkout
|
|
81
|
+
successUrl: '/success',
|
|
82
|
+
cancelUrl: '/cancel',
|
|
83
|
+
checkoutMetadata: { merchantOrderId: 'order-123' },
|
|
84
|
+
}}
|
|
85
|
+
onComplete={() => router.push('/success')}
|
|
162
86
|
/>
|
|
163
87
|
```
|
|
164
88
|
|
|
165
|
-
|
|
89
|
+
- **Currency is required.** Pass `currency` on `createSession`, or on the first item, subscription, or product. Without one the SDK throws a `CurrencyRequired` validation error before sending anything.
|
|
90
|
+
- **Detached creation (default).** The checkout creates a lightweight session *shell* first, then attaches the catalog in a background claim. The hosted card form mounts from the shell without waiting for the claim or Stripe.js; wallets, alternative payment methods, and PayPal load in parallel and appear when the claim lands. The card form's submit button stays gated until the claim settles, because the billing API refuses to charge an unclaimed session; a buyer who clicks during that window is asked to press pay again. Catalog and coupon errors surface as a checkout load error. `createSession.timeoutMs` applies separately to the shell and the claim (12 seconds each by default). Detached creation requires billing API v1.7.12 or newer and `checkoutMode: 'full'`, and is skipped when `tokenizedData` is supplied; pass `deferDataAttachment: false` to force a single request.
|
|
91
|
+
- **Late buyer identity.** Any still-anonymous session the checkout renders (including an existing session passed as `sessionId` and `nonce` with `buyerIdentified: false`) attaches the buyer email the checkout collects, or a wallet or PayPal approval supplies, through the session claim before payment.
|
|
92
|
+
- **Checkout metadata.** `checkoutMetadata` is sent unchanged on the initial request and never repeated in a claim. Omitting it keeps the request shape; an explicit `null` and `{}` stay distinct. Returned metadata is on the normalized session and its `data.session`. The billing API alone enforces size limits.
|
|
93
|
+
- **Idempotency.** The create request sends a stable `Idempotency-Key` whenever a secure random source is available. `FloPayCheckout` resolves the key once per logical checkout, stores it with the inline-session cache in `sessionStorage`, and reuses it across retries, effect reruns, and remounts, so StrictMode double-mounts or navigation flicker cannot create two sessions or two charges. A logical checkout is identified by merchant, buyer, cart and pricing, coupons, and checkout metadata (keys canonicalized, raw metadata not stored); checkout-mode, token, and analytics prop changes do not rotate it. Pass `createSession.idempotencyKey` to control the key yourself; it must be non-empty, at most 255 characters, and never reused for a new purchase.
|
|
94
|
+
- **Just-in-time updates.** With `layout="buttons"`, an `InlineSessionPatch` returned from `onBeforeButtonClick` is merged into the draft before the selected flow continues. A returned `checkoutMetadata` replaces the previous map; return `null` to clear it.
|
|
95
|
+
|
|
96
|
+
#### Authorisation-only checkout
|
|
166
97
|
|
|
167
|
-
`
|
|
168
|
-
the full checkout's hosted card button, use
|
|
169
|
-
[`submitLabel`](#hosted-card-submit-label).
|
|
98
|
+
Set `captureMethod: 'manual'` in `createSession` for an eligible item-only card checkout. The buyer still reaches the success experience, but the overlay reads `PAYMENT AUTHORISED` and `onComplete` receives `status: 'authorized'` with `paymentId`, `sessionId`, and `authorizationExpiresAt`. `authorized` means funds are held, not paid. Capture is a merchant-authenticated REST operation that browser code can never perform; see the [payments and authorisations reference](https://flopay.com/docs/technical-reference/rest/payments). Manual capture is rejected for subscription carts.
|
|
170
99
|
|
|
171
100
|
#### Hosted card submit label
|
|
172
101
|
|
|
173
|
-
In full checkout, the button that pays by card lives inside the hosted
|
|
174
|
-
widget. Set `submitLabel` on `FloPayCheckout` (or `SplitCardForm`) to replace
|
|
175
|
-
its wording, for example with the amount the buyer is about to pay:
|
|
102
|
+
In full checkout, the button that pays by card lives inside the hosted card form. Set `submitLabel` on `FloPayCheckout` or `SplitCardForm` to replace its wording, for example with the amount the buyer is about to pay.
|
|
176
103
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
```
|
|
104
|
+
- **Your wording.** Pass final, already-localized plain text from your own order summary. The SDK does not calculate, format, or translate amounts.
|
|
105
|
+
- **Live updates.** Changing the prop updates the button in place without remounting the card form, so entered card details, focus, and caret are kept. The SDK re-sends the label whenever the hosted form becomes ready again.
|
|
106
|
+
- **Defaults and reset.** Omit the prop (or pass `null`) to keep the hosted default, `CONFIRM PAYMENT`. Clearing it after a label was shown restores that default, and so does blank text: the hosted form trims the label, allows up to 64 characters, rejects control and hidden formatting characters, and shows the default for anything it rejects. Card setup (`FloPayCardSetup`) keeps its own `ADD CARD` wording.
|
|
107
|
+
- **Ownership.** `submitLabel` changes only the hosted card button. `confirmLabel` labels the saved-payment button in `checkoutMode="confirm"`, `cardButtonContent` renders the buttons-layout card tile, and wallet, PayPal, processing, and retry wording is unchanged.
|
|
108
|
+
- **Custom layouts.** `FloPayCheckout` injects `submitLabel`, like `sessionId` and `nonce`, into its direct custom-layout children; a child's own `submitLabel` wins.
|
|
109
|
+
- **Compatibility.** Custom labels need billing API v1.9.35 or newer, whose card form carries the `messageToken` and `expectedOrigin` that authenticate the label; the SDK sends it only to that exact origin. Older billing APIs keep the default wording.
|
|
184
110
|
|
|
185
|
-
|
|
186
|
-
from your own order summary. The SDK does not calculate, format, or translate
|
|
187
|
-
the amount; it sends your string unchanged.
|
|
188
|
-
- **Live updates.** Changing the prop updates the hosted button in place,
|
|
189
|
-
without remounting the card form, so the buyer's entered card details, focus,
|
|
190
|
-
and caret are kept. The SDK re-sends the latest label whenever the hosted
|
|
191
|
-
form becomes ready again.
|
|
192
|
-
- **Defaults and reset.** Omit the prop (or pass `null`) to keep the hosted
|
|
193
|
-
default, `CONFIRM PAYMENT`. Clearing it after a label was shown restores that
|
|
194
|
-
default, and so does blank text: the hosted form trims the label, allows up to 64
|
|
195
|
-
characters, rejects control and hidden formatting characters, and shows the
|
|
196
|
-
default for anything it rejects. Card setup (`FloPayCardSetup`) keeps its own
|
|
197
|
-
`ADD CARD` wording.
|
|
198
|
-
- **Prop ownership.** `submitLabel` only changes the hosted card button.
|
|
199
|
-
`confirmLabel` labels the saved-payment button in `checkoutMode="confirm"`,
|
|
200
|
-
`cardButtonContent` renders the buttons-layout card tile, and wallet, PayPal,
|
|
201
|
-
processing, and retry wording is unchanged.
|
|
202
|
-
- **Custom layouts.** Like `sessionId` and `nonce`, `FloPayCheckout` injects
|
|
203
|
-
`submitLabel` into its direct custom-layout children; a child's own
|
|
204
|
-
`submitLabel` wins.
|
|
205
|
-
- **Compatibility.** Custom labels need billing API v1.9.35+, whose vault block
|
|
206
|
-
carries the `messageToken` and `expectedOrigin` that authenticate the label.
|
|
207
|
-
The SDK sends the label only to that exact origin, bound to the checkout
|
|
208
|
-
session. Against an older backend the hosted form keeps its default.
|
|
111
|
+
#### Wallets and alternative payment methods
|
|
209
112
|
|
|
210
|
-
|
|
113
|
+
The payment-method list is gateway-driven: the billing API sends `gateways.stripe.enabledPaymentMethods` for each session, derived from the payment methods enabled on the merchant's Stripe account. Apple Pay, Google Pay, PayPal, Link, Amazon Pay, and Klarna render as express buttons; every other enabled method (for example Cash App Pay, Affirm, iDEAL, Bancontact, SEPA Debit) renders in an accordion. Enabling a new method in Stripe needs no SDK or application change. Wallets render only on supported devices.
|
|
211
114
|
|
|
212
|
-
|
|
213
|
-
Gateway (TeamFloPay/backend#1563). It processes those payments entirely
|
|
214
|
-
server-side behind the same hosted vault card form, so your checkout code does
|
|
215
|
-
not change and never needs to know which gateway took the card:
|
|
216
|
-
|
|
217
|
-
- **Opt-in on create.** An inline Full-mode checkout (`createSession`, or a
|
|
218
|
-
session created with `PaymentAPI.createAndFetchSession` /
|
|
219
|
-
`createDetachedSession`) declares the `worldpay-corporate-gateway-v1`
|
|
220
|
-
capability. Auto, Confirm, and pre-tokenized sessions do not, and neither do
|
|
221
|
-
older SDKs, so their routing is unchanged. See the `@flopay/js` README for the
|
|
222
|
-
wire contract.
|
|
223
|
-
- **Same card surface.** `FloPayCheckout` and `SplitCardForm` mount the hosted
|
|
224
|
-
form with no Stripe runtime and no Worldpay browser SDK or credentials. When
|
|
225
|
-
a session read omitted the embedded vault block, an open session that
|
|
226
|
-
advertises `gateways.worldpayCorporateGateway` recovers the form from
|
|
227
|
-
`POST /v1/checkouts/sessions/{id}/vault/capture` instead of failing to mount.
|
|
228
|
-
A completed or authorized session is never offered the card form again.
|
|
229
|
-
- **Same callbacks.** A success reaches `onComplete` as
|
|
230
|
-
`{ status: 'succeeded', checkoutMethod: 'card' }`, and a manual-capture hold
|
|
231
|
-
as `status: 'authorized'` with `paymentId`, `sessionId`, and
|
|
232
|
-
`authorizationExpiresAt`. `paymentIntentId` is omitted for these sessions
|
|
233
|
-
because the processor's order reference is not a Stripe object. A refusal
|
|
234
|
-
reaches `onDecline` with the stable `authorization_declined` code. A payment
|
|
235
|
-
the billing API is still reconciling when the hosted form stops waiting
|
|
236
|
-
reaches `onError`, never `onComplete`; a later session read reports the
|
|
237
|
-
settled status through `onComplete` or `onSessionCompleted`.
|
|
238
|
-
- **Unattributed card funnel.** `card_expanded` omits `gateway` for these
|
|
239
|
-
sessions, because the billing API, not the browser, chooses the card's
|
|
240
|
-
gateway.
|
|
241
|
-
|
|
242
|
-
### Secure saved-card setup
|
|
243
|
-
|
|
244
|
-
Use `FloPayCardSetup` to let a customer add or verify a card without creating a
|
|
245
|
-
purchase or charge. Your trusted server must first call the
|
|
246
|
-
merchant-authenticated card-setup endpoint and pass only its opaque `sessionId`
|
|
247
|
-
and bound `nonce` to the browser. Never send Client Basic credentials, OAuth
|
|
248
|
-
tokens, or a customer identifier to this component.
|
|
115
|
+
`card` in that list is a capability marker, not a Stripe-rendered method: it keeps **Credit / Debit Card** available and lets the SDK fetch the hosted card form on demand when the session has no embedded `vault` block.
|
|
249
116
|
|
|
250
|
-
|
|
251
|
-
|
|
117
|
+
- `showStripe` (default `true`) toggles Stripe wallets and alternative methods; `showPayPal` (default `true`) toggles PayPal. Turning both off with no PayPal gateway is reported through `onError` as a `validation_error` instead of rendering an empty form.
|
|
118
|
+
- In `layout="buttons"`, **Credit / Debit Card** opens the inline card form. Cancelling a wallet or PayPal silently returns to the chooser. A technical failure after the click (including popup blocking, or a provider closing without a result) restores the chooser, focuses a retry control for that method, shows safe method-specific copy such as `We couldn't open Google Pay. Try again or choose another payment method.`, and calls `onError` with a `FloPayError`. A method that is unavailable before the click just disappears.
|
|
119
|
+
- Wallet confirmation has one 25-second SDK budget inside the provider's 30-second callback deadline. If a wallet omits required buyer identity, the authorization is failed promptly, the SDK collects the missing email, and the buyer reopens the wallet to continue.
|
|
120
|
+
- **Confirmation recovery.** Google Pay and Apple Pay authentication can outlive the SDK's observation window. The SDK never cancels that provider promise: it reads the original intent's status and settles once. A successful intent continues to payment exactly once; a cancelled or failed intent offers a wallet retry; an intent still needing action, or a status that cannot be read, keeps payment locked and offers only **Check payment status**. A retry after a lost intent-create response reuses the same authorization attempt id, so no second intent can be created.
|
|
121
|
+
- `showApplePay` and `showGooglePay` are deprecated: wallet availability comes from the session's `enabledPaymentMethods`, and supplying them alongside it emits a one-time `console.warn`. Direct PayPal is resolved from `gateways.paypal`; the `SplitCardForm` `directPaypal` prop remains only for wiring PayPal manually.
|
|
252
122
|
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<FloPayCardSetup
|
|
256
|
-
sessionId={sessionId}
|
|
257
|
-
nonce={nonce}
|
|
258
|
-
onComplete={({ paymentMethod }) => {
|
|
259
|
-
// Display-only when supplied. Re-list from the server if absent during
|
|
260
|
-
// a mixed-version backend rollout.
|
|
261
|
-
console.log(paymentMethod?.brand, paymentMethod?.lastFour);
|
|
262
|
-
}}
|
|
263
|
-
onDecline={({ message }) => showCardError(message)}
|
|
264
|
-
onValidation={(message) => showCardValidation(message)}
|
|
265
|
-
onError={(error) => {
|
|
266
|
-
if (error.retryable) showRetry();
|
|
267
|
-
}}
|
|
268
|
-
onCancel={() => closeAddCard()}
|
|
269
|
-
/>
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
```
|
|
123
|
+
#### PayPal
|
|
273
124
|
|
|
274
|
-
The
|
|
275
|
-
hosted PCI card widget. It refuses purchase sessions, zero-amount non-setup
|
|
276
|
-
sessions, non-zero setup sessions, and setup sessions containing products. Bank
|
|
277
|
-
authentication is shown through the existing hosted challenge and does not
|
|
278
|
-
count as success while pending. Completion fires only after verification says
|
|
279
|
-
the card is usable; decline, validation, retryable technical failure, and
|
|
280
|
-
unmount cancellation remain separate outcomes.
|
|
281
|
-
|
|
282
|
-
Submission feedback matches checkout: the same processing overlay covers the
|
|
283
|
-
form from the buyer's submit ("SAVING CARD...") and flips to "CARD SAVED"
|
|
284
|
-
(held briefly before `onComplete`) or "CARD NOT SAVED" with the decline
|
|
285
|
-
message (cleared automatically so the buyer can retry in place).
|
|
286
|
-
|
|
287
|
-
#### Card-setup theming
|
|
288
|
-
|
|
289
|
-
`theme` accepts the same `ThemeId` as `FloPayCheckout` (default
|
|
290
|
-
`'modern-light'`) or raw `VaultCardThemeColors`. A named preset styles the
|
|
291
|
-
whole card-setup experience coherently:
|
|
292
|
-
|
|
293
|
-
- **Surface** — the container around the hosted form wears the preset's
|
|
294
|
-
complete credit-card pane treatment (`cardFormContainer`: background or
|
|
295
|
-
gradient, border, radius, shadow, padding), so `modern-dark` never places
|
|
296
|
-
dark hosted fields on a light, unthemed box.
|
|
297
|
-
- **Hosted form** — the widget receives the preset's field colors and
|
|
298
|
-
typography plus the complete hosted submit snapshot
|
|
299
|
-
(`VaultCardThemeColors.submitButton`, billing API v1.9.6+): label color,
|
|
300
|
-
solid or gradient enabled/hover/disabled fills, typography, spacing, shadow,
|
|
301
|
-
radius, disabled opacity, and blur. The legacy flat keys are always sent
|
|
302
|
-
alongside it, so an older hosted asset keeps its solid approximation during a
|
|
303
|
-
rolling deployment, and an older SDK against a newer asset keeps its
|
|
304
|
-
historical flat-only behavior. `modern-dark`'s dark label stays at or above
|
|
305
|
-
4.5:1 in the enabled, hover, and disabled states, disabled compositing
|
|
306
|
-
included.
|
|
307
|
-
- **Processing / success / error** — the overlay reuses the pane surface and
|
|
308
|
-
typography; processing wears the preset primary on its spinner, while success
|
|
309
|
-
and error keep semantic green/red accents chosen per color scheme so they
|
|
310
|
-
stay legible on every pane.
|
|
311
|
-
|
|
312
|
-
`'classic'` keeps the historical card-setup appearance exactly: no derived
|
|
313
|
-
surface, the neutral overlay, and no theme directive sent to a freshly mounted
|
|
314
|
-
hosted form. Raw `VaultCardThemeColors` objects — including partial ones — are
|
|
315
|
-
forwarded to the hosted form unchanged and add no surface or overlay treatment.
|
|
316
|
-
|
|
317
|
-
Changing `theme` at runtime updates the SDK-owned surface and overlay in place
|
|
318
|
-
and re-skins the mounted hosted form without remounting it, so entered card
|
|
319
|
-
details, focus, and caret are preserved. Switching back to `'classic'` sends
|
|
320
|
-
the hosted form's explicit reset (`theme: null`) so nothing from the previous
|
|
321
|
-
preset lingers.
|
|
322
|
-
|
|
323
|
-
##### `containerStyle` precedence
|
|
324
|
-
|
|
325
|
-
`containerStyle` is merged over the preset surface with deterministic rules:
|
|
326
|
-
|
|
327
|
-
1. Each `containerStyle` property wins over the preset value for the same
|
|
328
|
-
property.
|
|
329
|
-
2. Declarations are applied preset-first, then `containerStyle` in your own key
|
|
330
|
-
order, so a longhand refines a preset shorthand of the same family the way
|
|
331
|
-
CSS cascades: `borderColor: 'red'` recolors the preset border and keeps its
|
|
332
|
-
width and style; `paddingTop: 0` adjusts one side.
|
|
333
|
-
3. Background is the exception: any `background*` member in `containerStyle`
|
|
334
|
-
replaces the preset's background treatment entirely, so a color never shows
|
|
335
|
-
a glass preset's gradient through it.
|
|
336
|
-
4. With `'classic'` or a raw theme there is no preset surface: `containerStyle`
|
|
337
|
-
applies alone, exactly as before.
|
|
125
|
+
The SDK renders PayPal one of two ways, chosen per session from its gateways:
|
|
338
126
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
sessionId={sessionId}
|
|
342
|
-
nonce={nonce}
|
|
343
|
-
theme="glass-dark"
|
|
344
|
-
containerStyle={{ borderColor: '#60A5FA', padding: 16 }} // refines the glass-dark pane
|
|
345
|
-
/>
|
|
346
|
-
```
|
|
127
|
+
- **Direct PayPal** (preferred, and works inside Facebook, Instagram, and other in-app browsers): when the session has `gateways.paypal`, PayPal renders through the official PayPal JS SDK with the client id and `environment` (`sandbox` or `live`) from the billing API. If only PayPal is advertised, `FloPayCheckout` skips Stripe entirely.
|
|
128
|
+
- **Stripe-rendered PayPal**: when only `gateways.stripe` is configured, PayPal renders through Stripe's express checkout. It cannot render in Facebook or Instagram in-app browsers.
|
|
347
129
|
|
|
348
|
-
|
|
349
|
-
merchant REST calls. Replacement is composition: complete setup for the new
|
|
350
|
-
card, then ask your server to delete the old card. `FloPayCardSetup` deliberately
|
|
351
|
-
has no customer-id, merchant-credential, list, delete, purchase, or Stripe
|
|
352
|
-
Elements prop.
|
|
130
|
+
Direct PayPal never hides its button behind a FloPay identity step: after approval the SDK reads the payer's email and name, and asks once for anything missing. `gateways.paypal.providerObjectType` selects the operation: `order` (a one-time Order, even for a subscription-mode session), `subscription` (a PayPal-managed subscription), or `setup_token` (vault the account; the approval token is exchanged by the billing API). `approvalPresentation` selects the bounded `app_switch` mode only for a one-time Order in an in-app browser, where it enables PayPal's app switch; ordinary browsers, subscriptions, and setup tokens keep the popup flow. It is a mode, never a URL. If an in-app popup is blocked, `onTechnicalFailure` on `DirectPayPalButton` receives `popupBlocked: true` with guidance to open the page in the system browser, and PayPal stays visible for retry.
|
|
353
131
|
|
|
354
|
-
|
|
355
|
-
checkout:
|
|
132
|
+
**Initialization recovery.** A Direct PayPal startup failure hides PayPal, waits one second, and remounts once, without creating a session, intent, or charge; other payment methods stay usable throughout. If Stripe-rendered PayPal is also available, it takes over during recovery. If both attempts fail in a mixed checkout, PayPal stays hidden and neither `onError` nor `onDecline` fires. In a PayPal-only checkout the component shows an accessible retrying status, then a **Retry PayPal** button, and calls `onError` once with `paypal_init_timeout`.
|
|
356
133
|
|
|
357
|
-
|
|
358
|
-
import { FloPayCardSetup } from '@flopay/react/card-setup';
|
|
359
|
-
```
|
|
134
|
+
#### Server-side card gateways (Worldpay)
|
|
360
135
|
|
|
361
|
-
The
|
|
362
|
-
eagerly loads Stripe.js (a deliberate checkout preload); the `card-setup`
|
|
363
|
-
entry's graph never reaches Stripe, so saved-card pages stay free of Stripe
|
|
364
|
-
network activity by construction.
|
|
136
|
+
The billing API can route a checkout's cards to an approved Worldpay Corporate Gateway, processed entirely server-side behind the same hosted card form. Your checkout code does not change:
|
|
365
137
|
|
|
366
|
-
|
|
138
|
+
- **No opt-in.** The billing API decides from the merchant's configured gateways whether a session can use Worldpay. The SDK sends no capability token or gateway hint, so routing is the same for every SDK version and create style. Auto, Confirm, and pre-tokenized sessions stay on the released routing because the billing API keeps them there, not because the SDK asks.
|
|
139
|
+
- **Same card surface.** No Stripe runtime, Worldpay SDK, or browser credential is involved. An open session that advertises `gateways.worldpayCorporateGateway` but omits the embedded card form recovers it on demand; a completed or authorized session is never shown the card form again.
|
|
140
|
+
- **Same callbacks.** Success reaches `onComplete` as `status: 'succeeded'`, and a manual-capture hold as `status: 'authorized'` with `paymentId`, `sessionId`, and `authorizationExpiresAt`; `paymentIntentId` is omitted because the processor's reference is not a Stripe object. A refusal reaches `onDecline` with `authorization_declined`. A payment still reconciling when the form stops waiting reaches `onError`, never `onComplete`, and a later session read reports the settled status.
|
|
141
|
+
- The merchant instrument feed's `card_expanded` event omits `gateway` for these sessions, because the billing API chooses the card's gateway.
|
|
367
142
|
|
|
368
|
-
|
|
143
|
+
#### Themes and layouts
|
|
369
144
|
|
|
370
|
-
|
|
145
|
+
`FloPayCheckout`, `FloPayAutomaticPaymentButton`, `SplitCardForm`, and `FloPayCardSetup` accept one `theme` prop, a `ThemeId`:
|
|
371
146
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
147
|
+
| Theme | Aesthetic |
|
|
148
|
+
|-------|-----------|
|
|
149
|
+
| `classic` | The historic FloPay look (no bundle applied); the default for checkout components |
|
|
150
|
+
| `modern-light` / `modern-dark` | Clean and airy: Inter, soft shadows, FloPay-blue accents |
|
|
151
|
+
| `bold-light` / `bold-dark` | Saturated FloPay blue with a gradient pill submit and heavy borders |
|
|
152
|
+
| `glass-light` / `glass-dark` | Translucent surfaces with backdrop blur over a blue gradient |
|
|
375
153
|
|
|
376
|
-
|
|
154
|
+
One value styles non-card Stripe Elements, the SDK-rendered wrapper and AVS fields, the hosted card form, and the automatic payment button's fallback checkout. `appearance` (Stripe-side overrides) and `buttonsStyles` (wrapper overrides) still win over the bundle for the fields they touch. The legacy `buttonsTheme` prop is deprecated in favor of `theme`; see the [ButtonsLayoutStyles reference](https://flopay.com/docs/technical-reference/react/flopay-checkout#buttonslayoutstyles-reference) for every override field.
|
|
377
155
|
|
|
378
|
-
|
|
379
|
-
<FloPayCheckout
|
|
380
|
-
sessionId={sessionId}
|
|
381
|
-
onComplete={() => router.push('/success')}
|
|
382
|
-
onError={(error) => {
|
|
383
|
-
if (error.code === 'checkout_session_expired') {
|
|
384
|
-
router.push('/checkout/expired');
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
156
|
+
`layout="buttons"` stacks the payment methods as buttons with an expandable card form, and can change at runtime without recreating the session. In that layout, `onButtonClick` reports every method click, `onDecline` reports declines and cancellations, and `onBeforeButtonClick` runs before a method continues: return an `InlineSessionPatch` to update the inline draft, or `false` to stop. Keep it fast for PayPal, Apple Pay, and Google Pay, because it runs during the provider's button handshake.
|
|
387
157
|
|
|
388
|
-
|
|
389
|
-
}}
|
|
390
|
-
/>
|
|
391
|
-
```
|
|
158
|
+
#### Merchant instrument feed
|
|
392
159
|
|
|
393
|
-
|
|
160
|
+
`onInstrument` on `FloPayCheckout` or `FloPayProvider` forwards a stable, privacy-safe checkout funnel to your own analytics as `FloInstrumentEvent` values. `schemaVersion` is always `1`, and `gateway` (`stripe` or `paypal`) is included only when an event is attributable to that provider.
|
|
394
161
|
|
|
395
|
-
|
|
162
|
+
- Lifecycle names are `checkout_mount`, `sdk_loaded`, `form_rendered`, `card_expanded`, `tokenize`, `process_attempt`, and `3ds_challenge`. `checkout_mount`, `sdk_loaded`, `form_rendered`, and `card_expanded` arrive at most once per logical checkout; `tokenize`, `process_attempt`, and `3ds_challenge` may repeat for each attempt.
|
|
163
|
+
- `checkout_error` adds one of four phases: `session_create`, `sdk_load`, `process`, or `wallets`.
|
|
396
164
|
|
|
397
|
-
|
|
398
|
-
- lets the backend resolve and charge the customer's most recently vaulted payment method (across providers)
|
|
399
|
-
- shows the shared processing / success / failure modal
|
|
400
|
-
- emits the same success and decline events without rendering a full checkout form
|
|
165
|
+
The callback is an allowlisted projection with no card data, tokens, provider object IDs, secrets, or raw PII. A throwing `onInstrument` consumer never breaks checkout, and `telemetry={false}` does not disable this merchant-owned feed.
|
|
401
166
|
|
|
402
|
-
|
|
167
|
+
#### Privacy-safe operational telemetry
|
|
403
168
|
|
|
404
|
-
- `
|
|
405
|
-
- or the same session-creation props you would normally post to the backend: `clientId`, `items`, `subscriptions`, `account`, `successUrl`, `cancelUrl`, `couponCodes`, `tagsData`, `utmMetadata`
|
|
169
|
+
`FloPayCheckout` reports the same closed FloPay-owned lifecycle, error, and performance contract as `@flopay/js`, including render, interactivity, and provider readiness milestones. No endpoint, custom tag, user context, message, stack, or metadata can be configured, and callback exceptions are only logged to your console. Opt out with `telemetry={false}`.
|
|
406
170
|
|
|
407
|
-
|
|
171
|
+
If Stripe.js cannot initialize after the session resolves, a session that can use the hosted card form still shows it, and Direct PayPal still renders when advertised; only Stripe wallets and alternative methods are omitted. One **Retry payment methods** action retries provider initialization alone, never session creation, claims, intents, or payments. When neither the hosted card form nor Direct PayPal can render, `onError` and the `error` render prop receive `CheckoutInitializationFailed` with fixed buyer-safe copy.
|
|
408
172
|
|
|
409
|
-
|
|
173
|
+
Hosts with their own Sentry client can use the re-exported filter as `beforeSend`:
|
|
410
174
|
|
|
411
|
-
```
|
|
412
|
-
import
|
|
175
|
+
```ts
|
|
176
|
+
import * as Sentry from '@sentry/browser';
|
|
177
|
+
import { dropThirdPartyOnlyError } from '@flopay/react';
|
|
413
178
|
|
|
414
|
-
|
|
415
|
-
return (
|
|
416
|
-
<FloPayAutomaticPaymentButton
|
|
417
|
-
clientId="your-client-id"
|
|
418
|
-
account={{ userId: 'user_1', email: 'user@example.com' }}
|
|
419
|
-
items={[
|
|
420
|
-
{
|
|
421
|
-
providerItemId: 'upsell_1',
|
|
422
|
-
providerItemName: 'AI Supercharger Pack',
|
|
423
|
-
totalAmount: 249,
|
|
424
|
-
overrideAmount: 80,
|
|
425
|
-
currency: 'EUR',
|
|
426
|
-
quantity: 1,
|
|
427
|
-
},
|
|
428
|
-
]}
|
|
429
|
-
successUrl={`${window.location.origin}/success`}
|
|
430
|
-
cancelUrl={`${window.location.origin}/success`}
|
|
431
|
-
theme="bold-dark"
|
|
432
|
-
onClick={() => {
|
|
433
|
-
window.dataLayer?.push({ event: 'automatic_payment_button_click' });
|
|
434
|
-
}}
|
|
435
|
-
onSuccess={() => {
|
|
436
|
-
console.log('automatic payment succeeded');
|
|
437
|
-
}}
|
|
438
|
-
onError={(error) => {
|
|
439
|
-
console.error(error.message);
|
|
440
|
-
}}
|
|
441
|
-
onDecline={(decline) => {
|
|
442
|
-
console.log('automatic payment declined', decline);
|
|
443
|
-
}}
|
|
444
|
-
>
|
|
445
|
-
Purchase Item
|
|
446
|
-
</FloPayAutomaticPaymentButton>
|
|
447
|
-
);
|
|
448
|
-
}
|
|
179
|
+
Sentry.init({ beforeSend: dropThirdPartyOnlyError });
|
|
449
180
|
```
|
|
450
181
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
#### Wallet Buttons & alternative payment methods
|
|
454
|
-
|
|
455
|
-
The payment-method list is now **gateway-driven**: the billing API ships
|
|
456
|
-
`gateways.stripe.enabledPaymentMethods` per session, derived from the Stripe
|
|
457
|
-
Payment Method Configurations enabled on the merchant account. The SDK
|
|
458
|
-
partitions that list into two regions inside `SplitCardForm`:
|
|
459
|
-
|
|
460
|
-
- `ExpressCheckoutElement` — `apple_pay`, `google_pay`, `paypal`, `link`,
|
|
461
|
-
`amazon_pay`, `klarna` (everything supported as a native big button).
|
|
462
|
-
- `PaymentElement` (accordion) — every other enabled method, e.g. `cashapp`,
|
|
463
|
-
`affirm`, `ideal`, `bancontact`, `sepa_debit`.
|
|
464
|
-
|
|
465
|
-
`card` is a capability marker, not a Stripe-rendered method. It keeps the
|
|
466
|
-
**Credit / Debit Card** path available and tells SDK 1.4.9+ that the hosted
|
|
467
|
-
vault form can be requested lazily when `session.vault` is absent.
|
|
468
|
-
|
|
469
|
-
Enabling Cash App Pay (or any future Stripe method) in the Stripe dashboard is
|
|
470
|
-
the entire integration change — no SDK redeploy or consumer code update is
|
|
471
|
-
required. Wallets only render on supported devices regardless of dashboard
|
|
472
|
-
state (Apple Pay on Safari/macOS/iOS, Google Pay on Chrome).
|
|
473
|
-
|
|
474
|
-
> **Payment-logo CDN and CSP:** APM brand logos load from the versioned,
|
|
475
|
-
> FloPay-owned URL `https://cdn.flopay.com/sdk-logos/v1/`. Plain absolute HTTPS
|
|
476
|
-
> URLs work in Vite with no SDK middleware or `optimizeDeps` override, and retain
|
|
477
|
-
> the same behavior in webpack 5, Rollup, Next, and esbuild. Logos stay out of
|
|
478
|
-
> the package and JavaScript bundle; card-only and vault checkouts never request
|
|
479
|
-
> one. Merchants with a strict Content Security Policy must allow the host in
|
|
480
|
-
> the image directive, for example `img-src 'self' https://cdn.flopay.com;`.
|
|
481
|
-
> A failed or CSP-blocked logo request falls back to the method's existing
|
|
482
|
-
> inline monogram, so the tile remains branded without another network request.
|
|
483
|
-
|
|
484
|
-
In `layout="buttons"`, the **Credit / Debit Card** button always opens the
|
|
485
|
-
inline card form. External wallets and PayPal keep their own lifecycle:
|
|
486
|
-
buyer cancellation silently returns to the payment-method chooser with all
|
|
487
|
-
eligible options available, while post-click technical failures, popup
|
|
488
|
-
blocking, or a provider surface returning focus without a terminal callback
|
|
489
|
-
restore the chooser, focus a visible retry control for the failed method, show safe method-specific copy such as
|
|
490
|
-
`We couldn't open Google Pay. Try again or choose another payment method.`, and
|
|
491
|
-
call `onError` with a structured `FloPayError`. Passive pre-click load or
|
|
492
|
-
eligibility failures still collapse only the unavailable method and do not show
|
|
493
|
-
a checkout error.
|
|
494
|
-
|
|
495
|
-
Wallet confirmation has one 25-second SDK budget inside the provider's
|
|
496
|
-
30-second callback deadline. Flo-owned intent requests are aborted when that
|
|
497
|
-
budget expires, and every unsuccessful authorization settles the provider
|
|
498
|
-
callback once. Payment and setup wallets are confirmed through the active
|
|
499
|
-
`ExpressCheckoutElement` Elements instance. If a wallet omits required buyer
|
|
500
|
-
identity, the current authorization is failed promptly, the SDK collects the
|
|
501
|
-
missing email outside the wallet callback, and the buyer then reopens the
|
|
502
|
-
wallet to continue. The resolved identity is kept unchanged through the late
|
|
503
|
-
buyer claim, confirmation checkpoint, retry/resume, and final `/process`
|
|
504
|
-
request.
|
|
505
|
-
|
|
506
|
-
Toggle the whole Stripe region or the PayPal region independently with the
|
|
507
|
-
two gateway-level props:
|
|
182
|
+
It drops only complete exception stacks with no app or `@flopay/*` frame and at least one recognized browser-extension or Stripe controller frame; every other error passes through unchanged.
|
|
508
183
|
|
|
509
|
-
|
|
510
|
-
<FloPayCheckout
|
|
511
|
-
sessionId="sess_abc123"
|
|
512
|
-
showStripe={true} // default: true — Stripe wallets/APMs only
|
|
513
|
-
showPayPal={true} // default: true — DirectPayPal when gateway present,
|
|
514
|
-
// Stripe-rendered PayPal otherwise
|
|
515
|
-
onComplete={handleSuccess}
|
|
516
|
-
/>
|
|
517
|
-
```
|
|
184
|
+
### FloPayCardSetup
|
|
518
185
|
|
|
519
|
-
|
|
520
|
-
bootstrap-time validation error — `onError` fires with a
|
|
521
|
-
`FloPayError({ type: 'validation_error' })` so the misconfiguration surfaces
|
|
522
|
-
during integration instead of silently rendering an empty form.
|
|
186
|
+
`FloPayCardSetup` lets a customer add or verify a card without a purchase or charge. Your trusted server first calls the merchant-authenticated card-setup endpoint and passes only its opaque `sessionId` and bound `nonce` to the browser; never send API credentials or a customer identifier to this component. See the [payment methods reference](https://flopay.com/docs/technical-reference/rest/payment-methods) for listing, setup, and deletion.
|
|
523
187
|
|
|
524
|
-
|
|
188
|
+
- The component reads and validates the setup session before injecting the hosted card form. It refuses purchase sessions, non-zero or product-bearing setup sessions, and zero-amount sessions that are not setup sessions.
|
|
189
|
+
- Bank authentication is shown in the hosted challenge and does not count as success while pending. `onComplete` fires only once verification says the card is usable; decline, validation, retryable technical failure, and unmount cancellation are separate callbacks.
|
|
190
|
+
- Submission feedback matches checkout: a processing overlay reads `SAVING CARD...`, then `CARD SAVED` before `onComplete`, or `CARD NOT SAVED` with the decline message, cleared automatically so the buyer can retry.
|
|
191
|
+
- Replacing a card is composition: complete setup for the new card, then ask your server to delete the old one. The component has no customer-id, credential, list, delete, purchase, or Stripe Elements prop.
|
|
192
|
+
- Import it from `@flopay/react/card-setup` on pages that never run a checkout. The main entry re-exports it, but its module graph eagerly loads Stripe.js; the `card-setup` entry never reaches Stripe.
|
|
525
193
|
|
|
526
|
-
`
|
|
527
|
-
emit a one-time `console.warn` when supplied alongside the new
|
|
528
|
-
`enabledPaymentMethods` payload. They are no-ops once the backend ships the
|
|
529
|
-
field — Apple Pay / Google Pay are dashboard-controlled at Stripe, and direct
|
|
530
|
-
PayPal is auto-resolved from `gateways.paypal` on the session response. The
|
|
531
|
-
legacy props will be removed in `2.0`.
|
|
194
|
+
**Theming.** `theme` accepts a `ThemeId` (default `modern-light`) or raw `VaultCardThemeColors`. A named preset styles the container surface, the hosted form (including its complete submit-button snapshot, billing API v1.9.6+), and the processing, success, and error overlay coherently. `classic` keeps the historic appearance, and raw colors are forwarded to the hosted form unchanged with no surface treatment. Changing `theme` re-skins everything in place without remounting the form.
|
|
532
195
|
|
|
533
|
-
|
|
196
|
+
**`containerStyle` precedence.** Each `containerStyle` property wins over the preset's value for the same property, applied preset-first so a longhand refines a preset shorthand (for example `borderColor` keeps the preset border's width). Any `background*` member replaces the preset background entirely. With `classic` or raw colors, `containerStyle` applies alone.
|
|
534
197
|
|
|
535
|
-
|
|
198
|
+
### FloPayAutomaticPaymentButton
|
|
536
199
|
|
|
537
|
-
|
|
538
|
-
|---|---|
|
|
539
|
-
| `'classic'` | Historic FloPay look (no bundle applied — `#EDEDFF` wrapper, indigo submit). |
|
|
540
|
-
| `'modern-light'` / `'modern-dark'` | Clean & airy, Inter, soft shadows, FloPay-blue accents. |
|
|
541
|
-
| `'bold-light'` / `'bold-dark'` | Saturated FloPay blue with gradient pill submit and heavy borders. |
|
|
542
|
-
| `'glass-light'` / `'glass-dark'` | Translucent surfaces with backdrop blur over a blue gradient. |
|
|
200
|
+
`FloPayAutomaticPaymentButton` is a single reusable button that creates an `auto` checkout session inline (or reuses a `sessionId`), lets the billing API charge the customer's most recently saved payment method across providers, and shows the shared processing, success, and failure modal. It accepts either `sessionId` and `nonce`, or the session-creation props `clientId`, `items`, `subscriptions`, `products`, `account`, `successUrl`, `cancelUrl`, `couponCodes`, `tagsData`, and `utmMetadata`. `children` render inside the button.
|
|
543
201
|
|
|
544
|
-
|
|
545
|
-
<FloPayCheckout
|
|
546
|
-
sessionId="sess_abc123"
|
|
547
|
-
theme="bold-dark"
|
|
548
|
-
onComplete={handleSuccess}
|
|
549
|
-
/>
|
|
202
|
+
When the saved payment method cannot complete silently, the button opens a `FloPayCheckout` with the same `theme`. The `paymentMethodId` and `checkoutMethod` props are deprecated and ignored: the billing API selects the payment method and gateway.
|
|
550
203
|
|
|
551
|
-
|
|
552
|
-
sessionId="sess_abc123"
|
|
553
|
-
theme="bold-dark" // applied to the button AND the fallback modal
|
|
554
|
-
onSuccess={handleSuccess}
|
|
555
|
-
/>
|
|
556
|
-
```
|
|
204
|
+
### Composing your own checkout
|
|
557
205
|
|
|
558
|
-
`
|
|
206
|
+
`FloPayProvider`, `SplitCardForm`, and the hooks let you compose the provider and session yourself. Card checkout still uses the hosted card form: supply the session (with its `vault` block) and nonce, or a session whose `gateways.stripe.enabledPaymentMethods` includes `card` so the form can be fetched on demand. A session with neither never calls the recovery endpoint; its other methods remain usable, and if none remain `onError` receives `UnsupportedBackendVaultCapability`. `FloPayCheckout` and `SplitCardForm` never fall back to Stripe card fields.
|
|
559
207
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
/>
|
|
565
|
-
```
|
|
208
|
+
- Wallets, alternative methods, and PayPal create intents through the nonce-protected session intent route, and client-observed failures use the session decline route without tokens, provider object ids, card data, or personal data.
|
|
209
|
+
- **Hosted card form.** The widget owns the card fields, its own submit button, tokenization, the charge, 3DS, and the result, so no Stripe.js runs on the card path. `SplitCardForm` renders no card fields or submit button of its own, and Host-collected AVS fields remain outside the PCI widget and gate its submit. After the widget reports it is submitting, the processing overlay stays up until `complete`, `decline`, or `error`. In `layout="buttons"`, the recovery request for the form waits until the buyer selects Card, and concurrent renders share one request.
|
|
210
|
+
- **AVS.** With `enableAVS`, the SDK collects the billing address outside the PCI form and gates the form's submit synchronously on it. The postcode is validated against the selected country with the same rules as the billing API: a supported country blocks an empty or malformed postcode with an inline message, and a country without postcodes makes the field optional. Email and street, city, and state fields gate submit the same way. On submit, the address is saved in parallel with the charge: a rejected address (`4xx`) shows inline and reaches `onError` for the next attempt, while a transient failure or a 10-second timeout falls back to the session's saved address and still completes.
|
|
211
|
+
- `useCheckout()` exposes `claimPending`, which stays true while a detached session shell is not yet safe to charge.
|
|
566
212
|
|
|
567
|
-
|
|
213
|
+
## Examples
|
|
568
214
|
|
|
569
|
-
|
|
215
|
+
A complete checkout page for an existing session:
|
|
570
216
|
|
|
571
217
|
```tsx
|
|
572
|
-
|
|
573
|
-
sessionId="sess_abc123"
|
|
574
|
-
layout="buttons"
|
|
575
|
-
theme="bold-dark"
|
|
576
|
-
onButtonClick={(method) => console.log('clicked:', method)}
|
|
577
|
-
onComplete={handleSuccess}
|
|
578
|
-
/>
|
|
579
|
-
```
|
|
580
|
-
|
|
581
|
-
`layout` can be changed at runtime. Switching a mounted `FloPayCheckout` from
|
|
582
|
-
the default layout to `"buttons"` keeps the resolved checkout session and its
|
|
583
|
-
gateway capabilities, so the **Credit / Debit Card** choice remains available
|
|
584
|
-
without recreating the session.
|
|
585
|
-
|
|
586
|
-
> **Deprecated**: the legacy `buttonsTheme` prop (`'default'` / `'minimal'` / `'rounded'` / `'dark'`) still works but new code should use `theme` so the same value drives both the buttons-layout wrapper and the auto-payment fallback. See [ButtonsLayoutStyles reference](https://flopay.com/docs/api-reference/react/flopay-checkout#buttonslayoutstyles-reference) for the underlying override fields.
|
|
587
|
-
|
|
588
|
-
#### Button Hooks
|
|
589
|
-
|
|
590
|
-
Use `onButtonClick` to track button interactions, `onDecline` to track declines/cancellations, and `onBeforeButtonClick` to enrich any buttons-layout payment method before it continues:
|
|
218
|
+
import { FloPayCheckout } from '@flopay/react';
|
|
591
219
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
tagsData: { sessionId: 'checkout_email_captured' },
|
|
618
|
-
};
|
|
619
|
-
}}
|
|
620
|
-
onButtonClick={(method) => {
|
|
621
|
-
window.dataLayer?.push({ event: 'checkout_button_click', payment_method: method });
|
|
622
|
-
}}
|
|
623
|
-
onDecline={(decline) => {
|
|
624
|
-
window.dataLayer?.push({ event: 'checkout_decline', ...decline });
|
|
625
|
-
}}
|
|
626
|
-
onComplete={handleSuccess}
|
|
627
|
-
/>
|
|
220
|
+
export function CheckoutPage({ sessionId, nonce }: { sessionId: string; nonce: string }) {
|
|
221
|
+
return (
|
|
222
|
+
<FloPayCheckout
|
|
223
|
+
sessionId={sessionId}
|
|
224
|
+
nonce={nonce}
|
|
225
|
+
theme="modern-light"
|
|
226
|
+
submitLabel="Pay $10"
|
|
227
|
+
onComplete={(result) => {
|
|
228
|
+
if (result.status === 'authorized') {
|
|
229
|
+
console.log('Funds held for payment', result.paymentId, 'until', result.authorizationExpiresAt);
|
|
230
|
+
}
|
|
231
|
+
window.location.assign('/success');
|
|
232
|
+
}}
|
|
233
|
+
onDecline={(decline) => console.warn(`${decline.method} declined: ${decline.message}`)}
|
|
234
|
+
onError={(error) => {
|
|
235
|
+
if (error.code === 'checkout_session_expired') {
|
|
236
|
+
window.location.assign('/checkout/expired');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
console.error(error.message);
|
|
240
|
+
}}
|
|
241
|
+
onSessionCompleted={(successUrl) => window.location.assign(successUrl)}
|
|
242
|
+
/>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
628
245
|
```
|
|
629
246
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
#### Inline Session Creation
|
|
633
|
-
|
|
634
|
-
Skip the backend API route — create the session directly in the component:
|
|
247
|
+
Create the session inline, with the buttons layout, a just-in-time email step for cards, and analytics:
|
|
635
248
|
|
|
636
249
|
```tsx
|
|
637
|
-
|
|
638
|
-
layout="buttons"
|
|
639
|
-
createSession={{
|
|
640
|
-
clientId: 'your-client-id',
|
|
641
|
-
currency: 'EUR',
|
|
642
|
-
items: [{ providerItemId: 'product-1', providerItemName: 'Widget', totalAmount: 29.99 }],
|
|
643
|
-
account: { userId: 'user_1' }, // email and name are collected in checkout
|
|
644
|
-
successUrl: '/success',
|
|
645
|
-
cancelUrl: '/cancel',
|
|
646
|
-
checkoutMetadata: { merchantOrderId: 'order-123', campaign: 'summer' },
|
|
647
|
-
}}
|
|
648
|
-
onComplete={(result) => window.location.href = '/success'}
|
|
649
|
-
/>
|
|
650
|
-
```
|
|
250
|
+
import { type FloInstrumentEvent, FloPayCheckout } from '@flopay/react';
|
|
651
251
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
`checkoutMetadata?: Record<string, string> | null` is sent unchanged on that
|
|
657
|
-
initial shell request and never repeated in the catalog or late-buyer claim.
|
|
658
|
-
Omission keeps the previous request shape; explicit `null` and `{}` remain
|
|
659
|
-
distinct wire values. Returned metadata is available on the normalized result
|
|
660
|
-
and its `data.session`. Backend validation errors retain their structured
|
|
661
|
-
`FloPayError` code, message, and HTTP status; React does not duplicate backend
|
|
662
|
-
size or length limits.
|
|
252
|
+
function trackInstrument(event: FloInstrumentEvent) {
|
|
253
|
+
console.log('checkout instrument', event.name, event);
|
|
254
|
+
}
|
|
663
255
|
|
|
664
|
-
|
|
256
|
+
export function InlineCheckout() {
|
|
257
|
+
return (
|
|
258
|
+
<FloPayCheckout
|
|
259
|
+
layout="buttons"
|
|
260
|
+
theme="bold-dark"
|
|
261
|
+
createSession={{
|
|
262
|
+
clientId: 'your-client-id',
|
|
263
|
+
currency: 'EUR',
|
|
264
|
+
items: [{ providerItemId: 'product-1', providerItemName: 'Widget', totalAmount: 29.99 }],
|
|
265
|
+
account: { userId: 'user_1' },
|
|
266
|
+
successUrl: '/success',
|
|
267
|
+
cancelUrl: '/cancel',
|
|
268
|
+
checkoutMetadata: { merchantOrderId: 'order-123' },
|
|
269
|
+
idempotencyKey: 'checkout:order-123',
|
|
270
|
+
}}
|
|
271
|
+
onBeforeButtonClick={async ({ method, createSession }) => {
|
|
272
|
+
if (method !== 'card') {
|
|
273
|
+
return { checkoutMetadata: { merchantOrderId: 'order-123', selectedMethod: method } };
|
|
274
|
+
}
|
|
275
|
+
const email = window.prompt('Email for your receipt', createSession?.account.email ?? '');
|
|
276
|
+
return email ? { account: { email } } : false;
|
|
277
|
+
}}
|
|
278
|
+
onButtonClick={(method) => console.log('clicked', method)}
|
|
279
|
+
onInstrument={trackInstrument}
|
|
280
|
+
onComplete={() => window.location.assign('/success')}
|
|
281
|
+
/>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
665
285
|
|
|
666
|
-
|
|
667
|
-
item-only card checkout:
|
|
286
|
+
Authorise a one-time card payment now and capture it later from your server:
|
|
668
287
|
|
|
669
288
|
```tsx
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
289
|
+
import { FloPayCheckout } from '@flopay/react';
|
|
290
|
+
|
|
291
|
+
export function AuthorisationCheckout() {
|
|
292
|
+
return (
|
|
293
|
+
<FloPayCheckout
|
|
294
|
+
createSession={{
|
|
295
|
+
clientId: 'your-client-id',
|
|
296
|
+
captureMethod: 'manual',
|
|
297
|
+
currency: 'EUR',
|
|
298
|
+
items: [{ providerItemId: 'order_123', totalAmount: 29.99 }],
|
|
299
|
+
account: { userId: 'user_1', email: 'user@example.com' },
|
|
300
|
+
successUrl: '/success',
|
|
301
|
+
cancelUrl: '/cancel',
|
|
302
|
+
}}
|
|
303
|
+
onComplete={(result) => {
|
|
304
|
+
if (result.status === 'authorized') {
|
|
305
|
+
void fetch('/api/authorisations', {
|
|
306
|
+
method: 'POST',
|
|
307
|
+
headers: { 'Content-Type': 'application/json' },
|
|
308
|
+
body: JSON.stringify({
|
|
309
|
+
paymentId: result.paymentId,
|
|
310
|
+
sessionId: result.sessionId,
|
|
311
|
+
expiresAt: result.authorizationExpiresAt,
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}}
|
|
316
|
+
onSessionCompleted={(successUrl) => window.location.assign(successUrl)}
|
|
317
|
+
/>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
691
320
|
```
|
|
692
321
|
|
|
693
|
-
|
|
694
|
-
overlay says `PAYMENT AUTHORISED`. `authorized` means funds are held, not paid.
|
|
695
|
-
Capture remains a merchant-authenticated REST operation and is never available
|
|
696
|
-
to browser code. Manual capture is rejected for subscription carts. See the
|
|
697
|
-
repository's [pre-authorisation guide](../../docs/PREAUTHORIZATION.md).
|
|
698
|
-
|
|
699
|
-
That create POST sends a stable `Idempotency-Key` header automatically whenever a
|
|
700
|
-
secure RNG is available. `FloPayCheckout` resolves the key once per logical
|
|
701
|
-
checkout, stores it with the inline-session cache in `sessionStorage`, and reuses
|
|
702
|
-
it across transport retries, effect reruns, and component remounts. Concurrent
|
|
703
|
-
mounts (StrictMode double-mount, Suspense remount, or navigation flicker) share
|
|
704
|
-
both the session create and the auto-mode payment attempt, so one purchase cannot
|
|
705
|
-
fan out into multiple creates or charges.
|
|
706
|
-
|
|
707
|
-
Logical checkout identity is based on the merchant, buyer, cart/pricing,
|
|
708
|
-
coupons, and checkout metadata. Metadata keys are canonicalized for identity,
|
|
709
|
-
so key-order-only changes reuse the current session while a material metadata
|
|
710
|
-
change creates a new one. Raw metadata is not stored in the inline-session
|
|
711
|
-
cache. Checkout-mode changes, refreshed payment tokens, and analytics/UTM prop
|
|
712
|
-
churn do not rotate the key while that purchase is active. A different cart or
|
|
713
|
-
buyer gets a different key; an email-less buyer is separated by `account.userId`.
|
|
714
|
-
|
|
715
|
-
To control the key yourself — for example to keep it stable across your own
|
|
716
|
-
server retries — pass `idempotencyKey` on `createSession`:
|
|
322
|
+
Add a card without charging it:
|
|
717
323
|
|
|
718
324
|
```tsx
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
325
|
+
import { FloPayCardSetup } from '@flopay/react/card-setup';
|
|
326
|
+
|
|
327
|
+
export function AddCard({ sessionId, nonce }: { sessionId: string; nonce: string }) {
|
|
328
|
+
return (
|
|
329
|
+
<FloPayCardSetup
|
|
330
|
+
sessionId={sessionId}
|
|
331
|
+
nonce={nonce}
|
|
332
|
+
theme="glass-dark"
|
|
333
|
+
containerStyle={{ borderColor: '#60A5FA', padding: 16 }}
|
|
334
|
+
onComplete={({ paymentMethod }) => {
|
|
335
|
+
// Display-only when present; re-list cards from your server when it is absent.
|
|
336
|
+
console.log(paymentMethod?.brand, paymentMethod?.lastFour);
|
|
337
|
+
}}
|
|
338
|
+
onDecline={({ message }) => console.warn(message)}
|
|
339
|
+
onValidation={(message) => console.log(message)}
|
|
340
|
+
onError={(error) => console.error(error.code, error.retryable ? 'retry' : 'do not retry')}
|
|
341
|
+
onCancel={() => console.log('Card setup closed before it finished')}
|
|
342
|
+
/>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
725
345
|
```
|
|
726
346
|
|
|
727
|
-
|
|
728
|
-
`FloPayError('validation_error')` before any request. Omit it and the SDK
|
|
729
|
-
generates a fresh cryptographically random key per logical create when a secure
|
|
730
|
-
RNG is available; without one it omits the header, and a merchant-supplied
|
|
731
|
-
`idempotencyKey` stays the way to remain idempotent. See the
|
|
732
|
-
[`@flopay/js` idempotency docs](../js/README.md#idempotent-checkout-creation)
|
|
733
|
-
for the full contract.
|
|
734
|
-
|
|
735
|
-
Session-level `currency` is now **required**. The backend (#760) enforces `@IsNotEmpty` on the field; the SDK pre-validates and throws `FloPayError({ type: 'validation_error', code: 'CurrencyRequired' })` before issuing the request when neither the session nor any item/subscription/product carries a currency. Pass `currency` at the top of `createSession` (preferred), or rely on the legacy fallback to the first item/subscription/product `currency`.
|
|
736
|
-
|
|
737
|
-
When you use `onBeforeButtonClick` with `createSession`, any returned
|
|
738
|
-
`InlineSessionPatch` is merged into the draft session params before the selected
|
|
739
|
-
buttons-layout flow continues. That lets you add tracking data or update account
|
|
740
|
-
fields just in time without pre-creating a separate backend session. A returned
|
|
741
|
-
`checkoutMetadata` map replaces the prior map as a whole; return `null` to clear
|
|
742
|
-
it before creation.
|
|
743
|
-
|
|
744
|
-
### Detached session creation (default)
|
|
745
|
-
|
|
746
|
-
`createSession` checkouts are created in two phases (TeamFloPay/backend#1099):
|
|
747
|
-
a lightweight session *shell* that skips catalog validation and the
|
|
748
|
-
buyer-identity advisory locks, then a background **claim** that attaches the
|
|
749
|
-
catalog. If the draft already contains an email, that claim also attaches the
|
|
750
|
-
buyer. Otherwise it deliberately omits `accountData` and returns a complete,
|
|
751
|
-
still-anonymous session (`buyerIdentified: false`).
|
|
752
|
-
|
|
753
|
-
The optional `createSession.timeoutMs` value is applied independently to both
|
|
754
|
-
phases (12 seconds each by default). The shell POST stops its deadline as soon
|
|
755
|
-
as the shell is usable, and the initial catalogue claim PATCH starts with a
|
|
756
|
-
fresh signal and the full budget. Opting out of detached creation keeps the
|
|
757
|
-
one-shot request's single total deadline.
|
|
758
|
-
|
|
759
|
-
`FloPayCheckout` renders from the shell, so the hosted card form mounts and
|
|
760
|
-
becomes interactive without waiting for either the claim or Stripe.js — Stripe
|
|
761
|
-
wallets, APMs and PayPal load in parallel with the catalog claim and appear
|
|
762
|
-
from its full `enabledPaymentMethods` list when it lands. The card widget's
|
|
763
|
-
submit button stays gated for that window, because the
|
|
764
|
-
billing API rejects a charge against an unclaimed session; if a buyer clicks
|
|
765
|
-
during it, the widget cancels the click and the form asks them to press pay
|
|
766
|
-
again. In practice the claim resolves while the card is still being filled in.
|
|
767
|
-
|
|
768
|
-
Nothing else changes for you: the same session, the same callbacks, the same
|
|
769
|
-
results. Catalog and coupon errors still surface as a checkout load error, just
|
|
770
|
-
from the claim rather than the create. Missing buyer identity never delays this
|
|
771
|
-
catalog claim: provider-capable methods identify the buyer during payment, and
|
|
772
|
-
`claim(account)` performs a separate buyer-only attachment when manual
|
|
773
|
-
collection is needed.
|
|
774
|
-
|
|
775
|
-
That buyer-only attachment is not specific to detached creation. Any
|
|
776
|
-
still-anonymous session the checkout renders — a one-shot create without an
|
|
777
|
-
email, or an existing session passed as `sessionId` + `nonce` that the billing
|
|
778
|
-
API returns with `buyerIdentified: false` — gets the same late-buyer claim
|
|
779
|
-
rebuilt from the session's nonce, so the email the SDK collects (or a wallet /
|
|
780
|
-
PayPal approval supplies) reaches `PATCH /v1/checkouts/sessions/{id}/claim`
|
|
781
|
-
before payment. The hosted vault widget's charge is refused server-side for a
|
|
782
|
-
session with no bound buyer, so without that claim an email-less session loaded
|
|
783
|
-
by id would never complete.
|
|
784
|
-
|
|
785
|
-
Opt out per session to restore the original single-request create:
|
|
347
|
+
Charge a saved payment method with one button:
|
|
786
348
|
|
|
787
349
|
```tsx
|
|
788
|
-
|
|
789
|
-
createSession={{
|
|
790
|
-
/* … */
|
|
791
|
-
deferDataAttachment: false, // one-shot create
|
|
792
|
-
}}
|
|
793
|
-
/>
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
Detached creation applies only to `checkoutMode: 'full'` (the billing API
|
|
797
|
-
rejects it for `auto` / `confirm`) and is skipped when `tokenizedData` is
|
|
798
|
-
supplied. There is **no fallback** to the one-shot create — it requires a
|
|
799
|
-
billing API `v1.7.12` or newer that exposes the independent catalog/buyer
|
|
800
|
-
`PATCH /v1/checkouts/sessions/{id}/claim` contract.
|
|
350
|
+
import { FloPayAutomaticPaymentButton } from '@flopay/react';
|
|
801
351
|
|
|
802
|
-
|
|
352
|
+
export function UpsellButton() {
|
|
353
|
+
return (
|
|
354
|
+
<FloPayAutomaticPaymentButton
|
|
355
|
+
clientId="your-client-id"
|
|
356
|
+
account={{ userId: 'user_1', email: 'user@example.com' }}
|
|
357
|
+
items={[
|
|
358
|
+
{
|
|
359
|
+
providerItemId: 'upsell_1',
|
|
360
|
+
providerItemName: 'AI Supercharger Pack',
|
|
361
|
+
totalAmount: 249,
|
|
362
|
+
overrideAmount: 80,
|
|
363
|
+
currency: 'EUR',
|
|
364
|
+
quantity: 1,
|
|
365
|
+
},
|
|
366
|
+
]}
|
|
367
|
+
successUrl="https://shop.example.com/success"
|
|
368
|
+
cancelUrl="https://shop.example.com/cancel"
|
|
369
|
+
theme="bold-dark"
|
|
370
|
+
onClick={() => console.log('automatic payment button clicked')}
|
|
371
|
+
onSuccess={({ sessionId, autoCompleted }) => console.log('paid', sessionId, autoCompleted)}
|
|
372
|
+
onDecline={(decline) => console.warn('declined', decline.message)}
|
|
373
|
+
onError={(error) => console.error(error.message)}
|
|
374
|
+
>
|
|
375
|
+
Purchase Item
|
|
376
|
+
</FloPayAutomaticPaymentButton>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
```
|
|
803
380
|
|
|
804
|
-
|
|
805
|
-
Card checkout uses the backend-hosted vault widget; Stripe remains limited to
|
|
806
|
-
wallets/APMs and saved-payment authentication. The session can supply the
|
|
807
|
-
widget eagerly in `session.vault`, or explicitly advertise `card` in
|
|
808
|
-
`gateways.stripe.enabledPaymentMethods` so the SDK recovers it on demand.
|
|
381
|
+
Compose the provider, a checkout surface, and the hooks yourself:
|
|
809
382
|
|
|
810
383
|
```tsx
|
|
811
|
-
import { FloPayProvider, SplitCardForm } from '@flopay/react';
|
|
812
384
|
import { loadFloPay } from '@flopay/js';
|
|
385
|
+
import type { CheckoutSession } from '@flopay/shared';
|
|
386
|
+
import { FloPayProvider, SplitCardForm, useCheckout, useFloPay } from '@flopay/react';
|
|
813
387
|
|
|
814
388
|
const floPayPromise = loadFloPay('pk_test_...');
|
|
815
389
|
|
|
816
|
-
function
|
|
390
|
+
function PaymentStatus() {
|
|
391
|
+
const flopay = useFloPay();
|
|
392
|
+
const { loading, error, claimPending } = useCheckout();
|
|
393
|
+
if (!flopay || loading) return <p>Loading payment methods…</p>;
|
|
394
|
+
if (error) return <p role="alert">{error.message}</p>;
|
|
395
|
+
return claimPending ? <p>Preparing your order…</p> : null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function ComposedCheckout({ session, nonce }: { session: CheckoutSession; nonce: string }) {
|
|
817
399
|
return (
|
|
818
|
-
<FloPayProvider
|
|
819
|
-
|
|
820
|
-
options={{
|
|
821
|
-
amount: 2999,
|
|
822
|
-
currency: 'usd',
|
|
823
|
-
}}
|
|
824
|
-
>
|
|
400
|
+
<FloPayProvider flopay={floPayPromise} options={{ billingApiUrl: 'https://billing.example.com' }}>
|
|
401
|
+
<PaymentStatus />
|
|
825
402
|
<SplitCardForm
|
|
826
|
-
sessionId=
|
|
827
|
-
nonce={
|
|
403
|
+
sessionId={session.id}
|
|
404
|
+
nonce={nonce}
|
|
828
405
|
session={session}
|
|
829
|
-
billingApiUrl="https://billing.example.com"
|
|
830
406
|
email="user@example.com"
|
|
831
407
|
userId="user_1"
|
|
832
|
-
totalAmount={2999}
|
|
408
|
+
totalAmount={2999}
|
|
833
409
|
currency="usd"
|
|
410
|
+
enableAVS
|
|
834
411
|
enabledPaymentMethods={session.gateways?.stripe?.enabledPaymentMethods}
|
|
835
412
|
onComplete={(result) => {
|
|
836
|
-
if (result.status === 'succeeded')
|
|
837
|
-
window.location.href = '/success';
|
|
838
|
-
}
|
|
413
|
+
if (result.status === 'succeeded') window.location.assign('/success');
|
|
839
414
|
}}
|
|
840
|
-
onError={(
|
|
841
|
-
onFirstNameChange={(name) => setFirstName(name)}
|
|
842
|
-
onLastNameChange={(name) => setLastName(name)}
|
|
415
|
+
onError={(error) => console.error(error.message)}
|
|
843
416
|
/>
|
|
844
417
|
</FloPayProvider>
|
|
845
418
|
);
|
|
846
419
|
}
|
|
847
420
|
```
|
|
848
421
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
1. Wallet buttons and supported APM tiles from the session gateway capability.
|
|
852
|
-
2. Direct or Stripe-hosted PayPal, selected from the session gateways.
|
|
853
|
-
3. The hosted vault widget for sessions with embedded vault HTML or an explicit
|
|
854
|
-
Stripe `card` capability.
|
|
855
|
-
|
|
856
|
-
Wallets, APMs, and PayPal create intents through
|
|
857
|
-
`POST /v1/checkouts/sessions/{id}/intents`. The discriminated request separates
|
|
858
|
-
provider, payment-method category/type/id, and intent kind. Direct-card intent
|
|
859
|
-
requests are unsupported. Client-observed non-card failures use the
|
|
860
|
-
nonce-protected session decline endpoint and contain no payment tokens,
|
|
861
|
-
provider object IDs, card data, credentials, or PII.
|
|
862
|
-
|
|
863
|
-
For SDK 1.4.9+, an explicit Stripe `card` capability keeps card checkout
|
|
864
|
-
visible even when the backend omits the vault block. In `layout="buttons"`,
|
|
865
|
-
`POST /v1/checkouts/sessions/{id}/vault/capture` is deferred until the buyer
|
|
866
|
-
selects Card. An embedded block remains the preferred fast path and causes no
|
|
867
|
-
recovery POST. Sessions that advertise neither an embedded block nor `card`
|
|
868
|
-
do not call the recovery endpoint; their non-card methods remain usable, and
|
|
869
|
-
if none remain, `onError` receives `UnsupportedBackendVaultCapability`.
|
|
870
|
-
|
|
871
|
-
#### AVS postcode validation
|
|
872
|
-
|
|
873
|
-
When AVS collection is enabled (`enableAVS`) and the postcode field is shown,
|
|
874
|
-
`SplitCardForm` validates the postcode **format against the live selected
|
|
875
|
-
country before the hosted vault captures the card**.
|
|
876
|
-
It reuses `@flopay/shared`'s country-aware
|
|
877
|
-
[postcode helpers](../shared/README.md#postal-code-helpers) (the same
|
|
878
|
-
`validator` rules the billing API applies), so client and server agree.
|
|
879
|
-
|
|
880
|
-
- **Supported country + malformed postcode** → the vault widget's submit is blocked
|
|
881
|
-
and an inline, country-specific message shows the expected format
|
|
882
|
-
(e.g. *"Enter a valid ZIP Code (e.g. 12345 or 12345-6789)"*). On the vault
|
|
883
|
-
path the inline message appears once the field is blurred, since the disabled
|
|
884
|
-
submit means it can't be reached by a submit attempt.
|
|
885
|
-
- **Supported country + empty postcode** → the same submit gate applies, and
|
|
886
|
-
once the field is blurred (or a submit is attempted) an inline *required*
|
|
887
|
-
message shows (e.g. *"ZIP Code is required"*) rather than the expected-format
|
|
888
|
-
copy.
|
|
889
|
-
- **Country with no postal system** (or a locale `validator` doesn't recognise)
|
|
890
|
-
→ the postcode field is shown but **optional**: empty and format checks are
|
|
891
|
-
both skipped, so these buyers are never blocked.
|
|
892
|
-
|
|
893
|
-
The collected email field and the AVS street, city and state fields follow the
|
|
894
|
-
same pattern: while empty (or, for email, malformed) they hold the submit gate
|
|
895
|
-
shut, and once the buyer leaves the field (or a submit is attempted) they show
|
|
896
|
-
the same red outline and inline `role="alert"` message (e.g. *"Street address is
|
|
897
|
-
required"*, *"Enter a valid email address"*).
|
|
898
|
-
|
|
899
|
-
On the **vault card path** the widget's submit button is gated **synchronously**
|
|
900
|
-
on the client-side AVS check above. Because the client postcode rules mirror the
|
|
901
|
-
backend validator (`validator@13.15.35`) verbatim, a postcode the server's
|
|
902
|
-
`PATCH /v1/checkouts/sessions/{id}/account` would reject is already blocked here —
|
|
903
|
-
before the button ever enables — with no network round-trip in the critical path.
|
|
904
|
-
The gate is **never** held pending an async request: the hosted widget silently
|
|
905
|
-
swallows a click that lands while the button is disabled and offers no way to
|
|
906
|
-
auto-resubmit on release, so an async gate would strand any buyer (or single-click
|
|
907
|
-
preview) who clicked before it resolved — the checkout would write no attempt and
|
|
908
|
-
time out (sdk#124).
|
|
909
|
-
|
|
910
|
-
When the buyer submits, `SplitCardForm` persists the billing snapshot
|
|
911
|
-
best-effort **in parallel** with the widget's charge:
|
|
912
|
-
|
|
913
|
-
- **Backend rejects the address (a `4xx`)** — a residual, non-postcode drift, since
|
|
914
|
-
the postcode is already client-validated — the specific message (e.g. *"Postal/ZIP
|
|
915
|
-
code is not valid for the selected country…"*) shows inline **and** fires `onError`
|
|
916
|
-
with the parsed `FloPayError`, and the processing overlay clears so the buyer can
|
|
917
|
-
correct and retry. The charge cannot be cancelled once submitted, so this surfaces
|
|
918
|
-
for the buyer's *next* attempt rather than stopping the in-flight one.
|
|
919
|
-
- **Transient failure (`5xx` / network)** → best-effort and buyer-silent: the
|
|
920
|
-
backend listener falls back to the session's baseline address while the
|
|
921
|
-
widget completes the charge.
|
|
922
|
-
- **Internal 10-second timeout** → the same functional fallback applies, and
|
|
923
|
-
successful widget completion still reaches `onComplete`. Telemetry records
|
|
924
|
-
the privacy-safe lifecycle event `operation.fallback` with
|
|
925
|
-
`processing` / `account_snapshot` / `timeout` dimensions instead of an
|
|
926
|
-
alert-level technical error. Direct and blocking timeout paths remain errors.
|
|
927
|
-
|
|
928
|
-
### Vault PCI card form
|
|
929
|
-
|
|
930
|
-
The card path renders a **backend-served, self-contained hosted vault widget**
|
|
931
|
-
(`VaultCardFields`) (TeamFloPay/backend#823, Model A). The billing API may
|
|
932
|
-
return it eagerly in `session.vault`, or advertise `card` under
|
|
933
|
-
`gateways.stripe.enabledPaymentMethods` and let SDK 1.4.9+ request it lazily.
|
|
934
|
-
It is fully **server-driven** — there is no consumer prop to toggle it. The
|
|
935
|
-
widget owns the card fields, its own submit button, card tokenization, the
|
|
936
|
-
PaymentIntent (created **and** confirmed server-side), **3DS**, and the result,
|
|
937
|
-
so **no Stripe.js runs on the card path** and PAN / CVC never enter the SDK
|
|
938
|
-
runtime. Wallets / PayPal / APMs render exactly as before.
|
|
939
|
-
|
|
940
|
-
Because the widget owns the form, `SplitCardForm` renders no SDK card-entry
|
|
941
|
-
fields or submit button of its own; `submitLabel` only sets the wording of the
|
|
942
|
-
widget's button (see [Hosted card submit label](#hosted-card-submit-label)).
|
|
943
|
-
Host-collected AVS fields remain outside the PCI
|
|
944
|
-
widget and gate its submit. The flow is:
|
|
945
|
-
|
|
946
|
-
```text
|
|
947
|
-
session embeds session.vault or advertises gateways.stripe.enabledPaymentMethods: ['card']
|
|
948
|
-
→ SDK uses embedded HTML, or requests POST /vault/capture when Card is needed
|
|
949
|
-
→ concurrent renders/remounts share that active request
|
|
950
|
-
→ buyer pays inside the widget (tokenize → charge → 3DS, all backend-owned)
|
|
951
|
-
→ widget postMessages its outcome → SDK fires onComplete / onDecline / onError
|
|
952
|
-
```
|
|
953
|
-
|
|
954
|
-
- Embedded `session.vault.html` is the preferred fast path during rollout and
|
|
955
|
-
never triggers a recovery request.
|
|
956
|
-
- Deferred capture uses
|
|
957
|
-
`POST /v1/checkouts/sessions/{id}/vault/capture`, with a ten-second timeout,
|
|
958
|
-
bounded transient-network retry, and one shared active request per
|
|
959
|
-
base URL/session/nonce.
|
|
960
|
-
- **Returning customers** with a card on file (`session.providerPaymentMethodId`)
|
|
961
|
-
are charged by the backend's auto-checkout cascade.
|
|
962
|
-
- **3DS** is handled inside the widget — there is no client-side `confirmCardPayment`.
|
|
963
|
-
|
|
964
|
-
**Processing feedback:** after the hosted widget emits `submitting`, the SDK's
|
|
965
|
-
full-checkout processing overlay remains visible continuously until the widget
|
|
966
|
-
reports `complete`, `decline`, or `error`. Intermediate status polling and
|
|
967
|
-
late card-field validation mutations cannot expose the completed form while a
|
|
968
|
-
charge is still in flight. A synchronous card-validation rejection still
|
|
969
|
-
returns the buyer to the form immediately, as does a `4xx` account-snapshot
|
|
970
|
-
validation failure.
|
|
971
|
-
|
|
972
|
-
**Failure modes:** `onDecline` / `onError` receive the mapped reason + message
|
|
973
|
-
so consumers can react, and the SDK leaves an inline retry message after its
|
|
974
|
-
terminal failure overlay clears.
|
|
975
|
-
|
|
976
|
-
> **Backend dependencies:** the hosted widget must emit the `flopay-vault`
|
|
977
|
-
> `postMessage` outcome the SDK listens for (otherwise it falls back to its own
|
|
978
|
-
> success redirect), handle the 3DS step internally, and use the session account
|
|
979
|
-
> snapshot for AVS. The SDK collects and validates those address fields outside
|
|
980
|
-
> the PCI widget and persists them through the session account endpoint; the
|
|
981
|
-
> widget owns only sensitive card-data capture. Because the widget is injected
|
|
982
|
-
> **same-window**, every terminal outcome must be bound to the session id, and —
|
|
983
|
-
> to fully defend against same-window forgery — the backend should mint a
|
|
984
|
-
> per-session `messageToken` on the vault block and echo it in each
|
|
985
|
-
> `postMessage`; the SDK threads it through and rejects outcomes that omit or
|
|
986
|
-
> mismatch it (TeamFloPay/backend#823).
|
|
987
|
-
|
|
988
|
-
### PayPal Handling
|
|
989
|
-
|
|
990
|
-
The SDK supports two PayPal paths, selected per-session based on what the billing API advertises under `gateways.*`:
|
|
991
|
-
|
|
992
|
-
- **Direct PayPal** (preferred where available — works inside Facebook, Instagram, and other in-app browsers): when the session exposes `gateways.paypal.publishableKey`, the SDK renders PayPal via the official PayPal JS SDK using `<DirectPayPalButton>`. The PayPal client ID and `environment` (`'sandbox'`/`'live'`) come directly from the backend.
|
|
993
|
-
- **Stripe-rendered PayPal**: when only `gateways.stripe` is configured, PayPal renders through Stripe's `ExpressCheckoutElement` in its own Elements wrapper. This path can't render in Facebook/Instagram in-app browsers.
|
|
994
|
-
|
|
995
|
-
Renderer selection is mutually exclusive per session — direct PayPal takes priority over Stripe-rendered PayPal. Consumer props such as `showPayPal` continue to gate visibility on the client side.
|
|
996
|
-
|
|
997
|
-
**PayPal-only sessions:** When the backend advertises only `gateways.paypal` (no `gateways.stripe`), `FloPayCheckout` skips Stripe Elements entirely and renders `<DirectPayPalButton>` as the sole payment surface. Sessions that advertise no supported gateway at all throw a `validation_error` explaining the expected shape.
|
|
998
|
-
|
|
999
|
-
**Provider-first buyer identity:** Direct PayPal never hides its Smart Button
|
|
1000
|
-
behind a Flo buyer-details preflight. An anonymous buyer starts PayPal normally;
|
|
1001
|
-
after approval the SDK reads the payer email/name, attaches valid returned
|
|
1002
|
-
details, and continues processing. If PayPal returns incomplete identity, the
|
|
1003
|
-
SDK shows exactly one required fallback pane after approval and resumes with
|
|
1004
|
-
the collected values. A checkout that supplied buyer details up front keeps its
|
|
1005
|
-
existing request bodies and callbacks.
|
|
1006
|
-
|
|
1007
|
-
**In-app presentation and recovery:** Direct PayPal classifies the browser once
|
|
1008
|
-
per mount into a bounded app family; it never records the raw user agent.
|
|
1009
|
-
`approvalPresentation` selects the bounded `app_switch` mode only for a
|
|
1010
|
-
one-time Order in an in-app browser. When the backend advertises
|
|
1011
|
-
`gateways.paypal.approvalPresentation: 'app_switch'`, that mode enables PayPal's
|
|
1012
|
-
`appSwitchWhenAvailable` Buttons option.
|
|
1013
|
-
Ordinary browsers, legacy sessions, subscriptions, and setup tokens preserve
|
|
1014
|
-
the popup flow. The PayPal button stays visible in every environment as a
|
|
1015
|
-
result of this choice. If an in-app popup is blocked and no supported non-popup
|
|
1016
|
-
mode is available, `onTechnicalFailure` receives `popupBlocked: true` with
|
|
1017
|
-
fixed guidance to open the page in the system browser; ordinary browsers keep
|
|
1018
|
-
the existing allow-popups guidance, and PayPal remains visible for retry.
|
|
1019
|
-
|
|
1020
|
-
`approvalPresentation` is a bounded mode, not a URL. Merchant code cannot
|
|
1021
|
-
inject a return URL or resume token. Although the shared response type reserves
|
|
1022
|
-
`'redirect'`, the classic Buttons adapter does not activate it: deterministic
|
|
1023
|
-
redirect remains gated on the backend-issued signed resume contract and a
|
|
1024
|
-
proven PayPal adapter that captures before merchant-success navigation. Until
|
|
1025
|
-
both land, a redirect advertisement safely preserves popup behavior. This
|
|
1026
|
-
instrumentation explains where approval attempts stop; it does not by itself
|
|
1027
|
-
attribute the historical conversion drop to a particular browser or provider
|
|
1028
|
-
failure.
|
|
1029
|
-
|
|
1030
|
-
**Flo-owned subscription continuations:** `gateways.paypal.providerObjectType`
|
|
1031
|
-
selects the direct PayPal operation. `'order'` loads the PayPal SDK with
|
|
1032
|
-
`intent=capture` and invokes `createOrder`, even when the checkout session mode
|
|
1033
|
-
is subscription. `'subscription'` preserves the provider-managed
|
|
1034
|
-
`intent=subscription` / `createSubscription` flow. `'setup_token'` invokes
|
|
1035
|
-
`createVaultSetupToken`; approval submits the returned `vaultSetupToken` to the
|
|
1036
|
-
nonce-bound session `/process` route for backend exchange and activation. An
|
|
1037
|
-
advertised `'order'`/`'setup_token'` opts the intent request and tokenized
|
|
1038
|
-
process body into the `paypal_vaulted` payment-method channel; the intent
|
|
1039
|
-
response must match the advertised object type. When the field is absent,
|
|
1040
|
-
legacy sessions continue to derive Order versus Subscription from
|
|
1041
|
-
`isSubscription` and keep the released `paypal` requests byte-identical. See
|
|
1042
|
-
the full [channel-selection and cutover contract](../../docs/PAYPAL_FLO_SUBSCRIPTION_CONTINUATIONS.md).
|
|
1043
|
-
|
|
1044
|
-
**Direct PayPal initialization recovery:** Any active-generation technical
|
|
1045
|
-
failure before PayPal becomes ready — script rejection, a namespace without a
|
|
1046
|
-
usable `Buttons` factory, an eligible pre-ready `onError`, a `render()`
|
|
1047
|
-
rejection, or the SDK's 11-second render watchdog — hides Direct PayPal, waits
|
|
1048
|
-
one second, and makes exactly one background remount. Recovery is based on
|
|
1049
|
-
lifecycle state, not provider error text. The retry uses a fresh render
|
|
1050
|
-
generation, and callbacks or promise settlements from the superseded generation
|
|
1051
|
-
are ignored. Startup remount does not create a checkout session, intent, or
|
|
1052
|
-
charge; the first recovered buyer click creates at most one intent under the
|
|
1053
|
-
existing per-click idempotency contract.
|
|
1054
|
-
|
|
1055
|
-
In a mixed checkout, card, wallets, APMs, and any other eligible methods remain
|
|
1056
|
-
interactive throughout recovery. If Direct PayPal is configured alongside
|
|
1057
|
-
Stripe-rendered PayPal, the Stripe-rendered path activates while Direct PayPal
|
|
1058
|
-
is retrying or exhausted. If both Direct PayPal attempts fail, PayPal stays
|
|
1059
|
-
hidden for that component configuration; the SDK emits one sanitized console
|
|
1060
|
-
diagnostic and calls neither `onError` nor `onDecline`. Empty configuration,
|
|
1061
|
-
`isEligible() === false`, cancellation, supersession, unmount, and recognized
|
|
1062
|
-
lifecycle teardown are not automatically retried. Ineligibility still notifies
|
|
1063
|
-
the parent that PayPal is unavailable so a fallback can appear, without
|
|
1064
|
-
technical telemetry.
|
|
1065
|
-
|
|
1066
|
-
In a PayPal-only checkout, `FloPayCheckout` shows an accessible retrying status
|
|
1067
|
-
instead of a blank surface. After automatic recovery is exhausted, it shows safe
|
|
1068
|
-
generic unavailable copy and a **Retry PayPal** button. `onError` fires once with
|
|
1069
|
-
a `FloPayError` whose `type` is `api_error` and whose stable `code` is
|
|
1070
|
-
`paypal_init_timeout`; `onDecline` does not fire and the raw provider message is
|
|
1071
|
-
never rendered. Manual retry reuses the current checkout session and performs
|
|
1072
|
-
one fresh render attempt without adding another automatic retry.
|
|
1073
|
-
|
|
1074
|
-
The SDK exposes the relevant pieces in three ways:
|
|
1075
|
-
|
|
1076
|
-
- **`SplitCardForm`** / **`FloPayCheckout`**: pick the renderer automatically based on `gateways.*`. No additional configuration needed.
|
|
1077
|
-
|
|
1078
|
-
- **`FloPayProvider`** (manual composition): accepts an optional `paypalFlopay` prop used to drive Stripe's PayPal redirect leg. Load a second `FloPay` instance yourself only if you need to render PayPal manually. `usePayPalFloPay()` exposes it to descendants.
|
|
1079
|
-
|
|
1080
|
-
- **`DirectPayPalButton`** (standalone): can be rendered outside `SplitCardForm` when you only need the PayPal button. Pass `clientId`, `currency`, `environment`, `isSubscription`, and the backend-advertised `providerObjectType` and `approvalPresentation` when present. The backing session is authoritative; `isSubscription` remains the compatibility fallback when the object type is absent.
|
|
1081
|
-
|
|
1082
|
-
### Using Hooks
|
|
1083
|
-
|
|
1084
|
-
```tsx
|
|
1085
|
-
import { useFloPay, useCheckout } from '@flopay/react';
|
|
1086
|
-
|
|
1087
|
-
function PaymentStatus() {
|
|
1088
|
-
const flopay = useFloPay(); // FloPay | null
|
|
1089
|
-
const checkout = useCheckout(); // { session, loading, error, claimPending }
|
|
1090
|
-
|
|
1091
|
-
if (!flopay) return <div>Loading SDK...</div>;
|
|
1092
|
-
// ...
|
|
1093
|
-
}
|
|
1094
|
-
```
|
|
1095
|
-
|
|
1096
|
-
## API Reference
|
|
422
|
+
## Public API
|
|
1097
423
|
|
|
1098
424
|
### Components
|
|
1099
425
|
|
|
1100
426
|
| Component | Description |
|
|
1101
427
|
|-----------|-------------|
|
|
1102
|
-
| `
|
|
1103
|
-
| `
|
|
1104
|
-
| `
|
|
1105
|
-
| `
|
|
1106
|
-
| `
|
|
1107
|
-
| `
|
|
428
|
+
| `FloPayCheckout` | Recommended self-contained checkout. Resolves gateways, mounts the hosted card form, and renders wallets, alternative methods, PayPal, and saved-payment flows. |
|
|
429
|
+
| `FloPayCardSetup` | No-charge card verification for a merchant-server-created setup session. Also exported from `@flopay/react/card-setup`. |
|
|
430
|
+
| `FloPayAutomaticPaymentButton` | One-click button that charges the customer's saved payment method, falling back to `FloPayCheckout` |
|
|
431
|
+
| `FloPayProvider` | Context provider for composed checkouts |
|
|
432
|
+
| `SplitCardForm` | Checkout surface combining the hosted card form with wallets, alternative methods, and PayPal |
|
|
433
|
+
| `VaultCardFields` | The hosted PCI card form. `SplitCardForm` uses it internally; it consumes a `CardCaptureAdapter` from `useFloPay().cardCapture()`. |
|
|
434
|
+
| `DirectPayPalButton` | Standalone direct PayPal Order, PayPal-managed subscription, or setup-token button using the session intent contract |
|
|
1108
435
|
|
|
1109
436
|
### Hooks
|
|
1110
437
|
|
|
1111
438
|
| Hook | Returns | Description |
|
|
1112
439
|
|------|---------|-------------|
|
|
1113
|
-
| `useFloPay()` | `FloPay \| null` |
|
|
1114
|
-
| `useCheckout()` | `CheckoutState` | `{ session, loading, error, claimPending }`
|
|
440
|
+
| `useFloPay()` | `FloPay \| null` | The current FloPay instance, or `null` while it loads |
|
|
441
|
+
| `useCheckout()` | `CheckoutState` | `{ session, loading, error, claimPending }` for the surrounding checkout |
|
|
442
|
+
| `usePayPalFloPay()` | `FloPay \| null` | The optional FloPay instance used for Stripe-rendered PayPal |
|
|
1115
443
|
|
|
1116
|
-
###
|
|
444
|
+
### Other exports
|
|
1117
445
|
|
|
1118
|
-
|
|
|
1119
|
-
|
|
1120
|
-
| `
|
|
1121
|
-
| `
|
|
1122
|
-
| `options.billingApiUrl` | `string?` | Billing API base URL exposed to child components. |
|
|
1123
|
-
| `onInstrument` | `(event: FloInstrumentEvent) => void` | Versioned, privacy-safe checkout funnel and phased error feed for merchant analytics. |
|
|
446
|
+
| Export | Description |
|
|
447
|
+
|--------|-------------|
|
|
448
|
+
| `FloPayCardSetupError` | Card-setup error class: a `FloPayError` with an explicit `retryable` flag |
|
|
449
|
+
| `dropThirdPartyOnlyError(event)` | Sentry `beforeSend` predicate re-exported from `@flopay/shared` |
|
|
1124
450
|
|
|
1125
|
-
###
|
|
451
|
+
### FloPayCheckoutProps
|
|
1126
452
|
|
|
1127
453
|
| Prop | Type | Description |
|
|
1128
454
|
|------|------|-------------|
|
|
1129
|
-
| `sessionId` | `string
|
|
1130
|
-
| `nonce` | `string
|
|
1131
|
-
| `
|
|
1132
|
-
| `
|
|
1133
|
-
| `
|
|
1134
|
-
| `
|
|
1135
|
-
| `
|
|
1136
|
-
| `
|
|
1137
|
-
| `
|
|
1138
|
-
| `
|
|
1139
|
-
| `
|
|
1140
|
-
| `
|
|
1141
|
-
| `
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
`
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
`
|
|
455
|
+
| `sessionId` | `string?` | Existing checkout session id. Required unless `createSession` is provided. |
|
|
456
|
+
| `nonce` | `string?` | Session-bound checkout token for `sessionId` |
|
|
457
|
+
| `createSession` | `InlineSessionDraft?` | Creates the session inline instead of using `sessionId` |
|
|
458
|
+
| `checkoutMode` | `CheckoutMode?` | Overrides the session's checkout mode |
|
|
459
|
+
| `billingApiUrl` | `string?` | Billing API base URL; defaults to the configured environment |
|
|
460
|
+
| `layout` | `'default' \| 'buttons'` | Payment form layout |
|
|
461
|
+
| `theme` | `ThemeId?` | Theme bundle for the whole checkout |
|
|
462
|
+
| `appearance` | `FloPayAppearance?` | Stripe-side appearance overrides |
|
|
463
|
+
| `buttonsStyles` | `ButtonsLayoutStyles?` | Wrapper style overrides layered on the theme |
|
|
464
|
+
| `locale` | `string?` | Locale for payment elements |
|
|
465
|
+
| `submitLabel` | `string \| null` | Live wording for the hosted card submit button |
|
|
466
|
+
| `confirmLabel` | `string?` | Saved-payment button label in confirm mode |
|
|
467
|
+
| `renderConfirmButton` | `function?` | Custom confirm-mode button renderer |
|
|
468
|
+
| `showStripe` | `boolean?` | Show Stripe wallets and alternative methods (default `true`) |
|
|
469
|
+
| `showPayPal` | `boolean?` | Show PayPal (default `true`) |
|
|
470
|
+
| `enabledPaymentMethods` | `string[]?` | Overrides the session's advertised Stripe methods |
|
|
471
|
+
| `enableAVS` | `boolean \| AVSFieldConfig` | Collect and validate the billing address |
|
|
472
|
+
| `avsLayout` | `'row' \| 'column'` | AVS field layout |
|
|
473
|
+
| `cardFieldOrder` | `VaultCardFieldKey[]?` | Order of the hosted card rows |
|
|
474
|
+
| `cardPreFormSlot` | `ReactNode?` | Content rendered above the hosted card form |
|
|
475
|
+
| `cardButtonContent` | `ReactNode?` | Card tile content in `layout="buttons"` |
|
|
476
|
+
| `cardBackButtonContent` | `ReactNode?` | Back-button label in `layout="buttons"` |
|
|
477
|
+
| `cardTitleContent` | `ReactNode?` | Card-form title |
|
|
478
|
+
| `className` | `string?` | Extra wrapper class |
|
|
479
|
+
| `loading` | `ReactNode?` | Custom loading UI |
|
|
480
|
+
| `error` | `(error: FloPayError) => ReactNode` | Custom error UI |
|
|
481
|
+
| `initialErrorMessage` | `string \| null` | Seeds an initial error message |
|
|
482
|
+
| `children` | `ReactNode?` | Custom layout rendered instead of the default `SplitCardForm` |
|
|
483
|
+
| `telemetry` | `boolean?` | Set `false` to opt out of operational telemetry |
|
|
484
|
+
| `onComplete` | `(result: PaymentResult) => void` | Payment succeeded or was authorized |
|
|
485
|
+
| `onDecline` | `(decline: DeclineEvent) => void` | A payment was declined or authentication failed |
|
|
486
|
+
| `onError` | `(error: FloPayError) => void` | A checkout error occurred |
|
|
487
|
+
| `onSessionCompleted` | `(successUrl: string) => void` | The session was already completed |
|
|
488
|
+
| `onButtonClick` | `(method: CheckoutButtonMethod) => void` | A payment method button was clicked |
|
|
489
|
+
| `onBeforeButtonClick` | `(event: BeforeButtonClickEvent) => ...` | Runs before a buttons-layout method continues; return a patch or `false` |
|
|
490
|
+
| `onInstrument` | `(event: FloInstrumentEvent) => void` | Merchant instrument feed |
|
|
491
|
+
| `onCountryChange` / `onZipChange` | `(value: string) => void` | AVS country or postcode changed |
|
|
492
|
+
| `showApplePay` / `showGooglePay` / `buttonsTheme` | deprecated | Superseded by the session's methods and `theme` |
|
|
1152
493
|
|
|
1153
|
-
|
|
1154
|
-
<FloPayCheckout sessionId={sessionId} nonce={nonce} telemetry={false} />
|
|
1155
|
-
```
|
|
1156
|
-
|
|
1157
|
-
No endpoint, custom tag, user context, message, stack, or metadata can be
|
|
1158
|
-
configured. See [`docs/TELEMETRY.md`](../../docs/TELEMETRY.md) for the complete
|
|
1159
|
-
privacy and retention contract.
|
|
494
|
+
### FloPayProviderProps
|
|
1160
495
|
|
|
1161
|
-
|
|
1162
|
-
|
|
496
|
+
| Prop | Type | Description |
|
|
497
|
+
|------|------|-------------|
|
|
498
|
+
| `flopay` | `FloPay \| Promise<FloPay> \| null` | A FloPay instance or the promise from `loadFloPay()` |
|
|
499
|
+
| `paypalFlopay` | `FloPay \| Promise<FloPay> \| null` | Optional instance for the Stripe-rendered PayPal leg |
|
|
500
|
+
| `options.billingApiUrl` | `string?` | Billing API base URL exposed to child components |
|
|
501
|
+
| `onInstrument` | `(event: FloInstrumentEvent) => void` | Merchant instrument feed |
|
|
502
|
+
| `children` | `ReactNode` | Checkout content |
|
|
1163
503
|
|
|
1164
|
-
|
|
1165
|
-
import * as Sentry from '@sentry/browser';
|
|
1166
|
-
import { dropThirdPartyOnlyError } from '@flopay/react';
|
|
504
|
+
### FloPayCardSetupProps
|
|
1167
505
|
|
|
1168
|
-
|
|
1169
|
-
|
|
506
|
+
| Prop | Type | Description |
|
|
507
|
+
|------|------|-------------|
|
|
508
|
+
| `sessionId` | `string` | Opaque id from the merchant-authenticated setup-session endpoint |
|
|
509
|
+
| `nonce` | `string` | Session-bound token returned with `sessionId` |
|
|
510
|
+
| `billingApiUrl` | `string?` | Billing API base URL; defaults to the configured environment |
|
|
511
|
+
| `telemetry` | `boolean?` | Set `false` to opt out of operational telemetry |
|
|
512
|
+
| `theme` | `ThemeId \| VaultCardThemeColors` | Preset (default `modern-light`) or raw hosted-form colors |
|
|
513
|
+
| `containerStyle` | `CSSProperties?` | Container styles merged over the preset surface |
|
|
514
|
+
| `loading` | `ReactNode?` | Content shown while the session and form load |
|
|
515
|
+
| `onReady` | `() => void` | The hosted form is mounted and accepting input |
|
|
516
|
+
| `onComplete` | `(event: FloPayCardSetupCompleteEvent) => void` | The card is verified and usable |
|
|
517
|
+
| `onDecline` | `(event: FloPayCardSetupDeclineEvent) => void` | Verification was declined |
|
|
518
|
+
| `onValidation` | `(message: string \| null) => void` | Live hosted-field validation, not an error |
|
|
519
|
+
| `onError` | `(error: FloPayCardSetupError) => void` | Validation or technical failure with `code` and `retryable` |
|
|
520
|
+
| `onCancel` | `(event: FloPayCardSetupCancelEvent) => void` | Unmounted before a terminal outcome |
|
|
521
|
+
|
|
522
|
+
### FloPayAutomaticPaymentButtonProps
|
|
523
|
+
|
|
524
|
+
Also accepts every standard `button` attribute, such as `onClick`, `disabled`, and `className`.
|
|
1170
525
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
`
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
`
|
|
1185
|
-
`
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
vault-capable session continues with the hosted card form, and Direct PayPal
|
|
1190
|
-
continues when the session advertises it; Stripe wallets and APMs are omitted.
|
|
1191
|
-
The degraded surface exposes one **Retry payment methods** action. It retries
|
|
1192
|
-
only provider initialization and never repeats session read/create/claim,
|
|
1193
|
-
automatic checkout, intent creation, or payment processing. A failed retry is
|
|
1194
|
-
not offered again during that mount, and cards or Direct PayPal remain usable.
|
|
1195
|
-
|
|
1196
|
-
When neither the hosted card form nor Direct PayPal can render, `onError` and
|
|
1197
|
-
the `error` render prop receive a `FloPayError` with code
|
|
1198
|
-
`CheckoutInitializationFailed`. The default error surface uses fixed buyer-safe
|
|
1199
|
-
copy. Raw provider messages are never placed in the DOM or merchant callback.
|
|
1200
|
-
Existing-session transport and malformed-response failures are separately
|
|
1201
|
-
reported at `session_read`; expected session outcomes remain non-alerting.
|
|
1202
|
-
|
|
1203
|
-
Direct PayPal script and unusable-namespace failures remain
|
|
1204
|
-
`PROVIDER_LOAD_FAILED` at `provider_load`. Once PayPal reports the button as
|
|
1205
|
-
eligible, a pre-ready `onError` or rejected `render()` is instead
|
|
1206
|
-
`PROVIDER_RUNTIME_FAILED` at `provider_ready`. These bounded groups contain no
|
|
1207
|
-
provider message or stack. Ineligibility, superseded renders, and recognized
|
|
1208
|
-
PayPal teardown remain non-alerting; only true load failures project to the
|
|
1209
|
-
merchant instrument feed as `checkout_error` / `sdk_load`.
|
|
1210
|
-
|
|
1211
|
-
The same classification applies to React-owned saved-payment processing in
|
|
1212
|
-
`auto` and `confirm` modes. An ordinary saved-payment `400` decline, a
|
|
1213
|
-
structured-field `400`, or a `422` still falls back to the usable full checkout
|
|
1214
|
-
and emits `operation.fallback`, but reports only the matching
|
|
1215
|
-
`payment_declined` or `validation_rejected` outcome. Genuine server,
|
|
1216
|
-
transport, malformed-response, and provider-runtime failures remain
|
|
1217
|
-
categorized technical errors. This does not change fallback rendering or
|
|
1218
|
-
merchant callback behavior, and no response body, provider payload, session
|
|
1219
|
-
value, payment data, or raw error message is added to telemetry.
|
|
526
|
+
| Prop | Type | Description |
|
|
527
|
+
|------|------|-------------|
|
|
528
|
+
| `sessionId` / `nonce` | `string?` | Reuse an existing auto-mode session |
|
|
529
|
+
| `createSession` | `InlineSessionDraft?` | Inline draft for the session to create |
|
|
530
|
+
| `clientId` | `string?` | Merchant client id for inline creation |
|
|
531
|
+
| `items` / `subscriptions` / `products` | arrays | Cart for inline creation |
|
|
532
|
+
| `account` | `object?` | Buyer account; `userId` required, `email` optional |
|
|
533
|
+
| `successUrl` / `cancelUrl` | `string?` | Redirect targets |
|
|
534
|
+
| `couponCodes` | `string[]?` | Coupons to apply |
|
|
535
|
+
| `tagsData` / `utmMetadata` | objects | Analytics context |
|
|
536
|
+
| `billingApiUrl` | `string?` | Billing API base URL |
|
|
537
|
+
| `locale` | `string?` | Locale for the fallback checkout |
|
|
538
|
+
| `theme` | `ThemeId?` | Theme for the button and its fallback checkout |
|
|
539
|
+
| `appearance` / `buttonsStyles` | objects | Appearance overrides |
|
|
540
|
+
| `onSuccess` | `(event: FloPayAutomaticPaymentSuccessEvent) => void` | Payment completed |
|
|
541
|
+
| `onDecline` | `(decline: DeclineEvent) => void` | Payment declined |
|
|
542
|
+
| `onError` | `(error: FloPayError) => void` | Payment or setup error |
|
|
543
|
+
| `children` | `ReactNode?` | Button content |
|
|
1220
544
|
|
|
1221
545
|
### SplitCardFormProps
|
|
1222
546
|
|
|
1223
|
-
|
|
547
|
+
| Prop | Type | Description |
|
|
548
|
+
|------|------|-------------|
|
|
549
|
+
| `sessionId` | `string` | Checkout session id |
|
|
550
|
+
| `nonce` | `string?` | Session-bound token forwarded on intent, decline, account, and process calls |
|
|
551
|
+
| `session` | `CheckoutSession \| null` | Session capability data, including the optional `vault` block and gateways |
|
|
552
|
+
| `billingApiUrl` | `string?` | Billing API base URL |
|
|
553
|
+
| `email` / `userId` / `firstName` / `lastName` | `string?` | Buyer details |
|
|
554
|
+
| `totalAmount` | `number?` | Amount in the smallest currency unit, for wallet and PayPal configuration |
|
|
555
|
+
| `currency` | `string?` | Currency code for wallet and PayPal configuration |
|
|
556
|
+
| `layout` | `'default' \| 'buttons'` | Payment form layout |
|
|
557
|
+
| `theme` | `ThemeId?` | Theme bundle |
|
|
558
|
+
| `submitLabel` | `(string \| null)?` | Live wording for the hosted card submit button; omitted, `null`, or blank shows the hosted default `CONFIRM PAYMENT` |
|
|
559
|
+
| `showStripe` / `showPayPal` | `boolean?` | Toggle Stripe methods and PayPal |
|
|
560
|
+
| `enabledPaymentMethods` | `string[]?` | Stripe method identifiers; `card` advertises the hosted card form |
|
|
561
|
+
| `enableAVS` | `boolean \| AVSFieldConfig` | Collect and validate the billing address |
|
|
562
|
+
| `cardFieldOrder` | `VaultCardFieldKey[]?` | Order of the hosted card rows |
|
|
563
|
+
| `onBuyerIdentityReady` | `(identity) => MaybePromise<void>` | Attach a buyer identity collected by the form; `FloPayCheckout` wires its late-buyer claim here |
|
|
564
|
+
| `onTokenizedBody` | `(body: TokenizedBody) => void` | Take over non-card backend submission yourself |
|
|
565
|
+
| `onComplete` / `onDecline` / `onError` | callbacks | Checkout outcomes |
|
|
566
|
+
| `directPaypal` | `object?` | Direct PayPal client configuration when wiring PayPal manually; `FloPayCheckout` resolves it from `gateways.paypal` |
|
|
567
|
+
| `showApplePay` / `showGooglePay` | deprecated | Superseded by the session's `enabledPaymentMethods` |
|
|
568
|
+
|
|
569
|
+
### DirectPayPalButtonProps
|
|
1224
570
|
|
|
1225
571
|
| Prop | Type | Description |
|
|
1226
572
|
|------|------|-------------|
|
|
1227
|
-
| `sessionId` | `string` | Checkout session
|
|
1228
|
-
| `
|
|
1229
|
-
| `
|
|
1230
|
-
| `
|
|
1231
|
-
| `
|
|
1232
|
-
| `
|
|
1233
|
-
| `
|
|
1234
|
-
| `
|
|
1235
|
-
| `
|
|
1236
|
-
| `
|
|
1237
|
-
| `totalAmount` | `number` | Amount in cents for PayPal / wallet config |
|
|
1238
|
-
| `currency` | `string` | Currency code for PayPal / wallet config (default: `'usd'`) |
|
|
1239
|
-
| `submitLabel` | `(string \| null)?` | Final, already-localized wording for the hosted card button (e.g. `'Pay $10'`). Updates live without remounting the card form; omitted, cleared, `null`, or blank shows the hosted default `CONFIRM PAYMENT`. See [Hosted card submit label](#hosted-card-submit-label). |
|
|
1240
|
-
| `onFirstNameChange` | `(value: string) => void` | First name change callback |
|
|
1241
|
-
| `onLastNameChange` | `(value: string) => void` | Last name change callback |
|
|
1242
|
-
| `onBuyerIdentityReady` | `(identity) => MaybePromise<void>?` | Called once checkout has collected a missing buyer email (or a wallet / PayPal approval supplied one) on a still-anonymous session. `FloPayCheckout` wires its late-buyer `PATCH /claim` here — including for custom-layout children, via `CheckoutContext`. A standalone form around a `session` with `buyerIdentified: false` that is given neither a prop nor a context claim builds its own from `sessionId` + `nonce`, so the hosted vault charge is never submitted against an unbound buyer. Supply the prop to take the claim over yourself. |
|
|
1243
|
-
| `onComplete` / `onDecline` / `onError` | callbacks | Observable checkout outcomes. |
|
|
1244
|
-
|
|
1245
|
-
#### Deferred-vault rollout boundary
|
|
1246
|
-
|
|
1247
|
-
SDK `1.4.9` is the compatibility boundary for omitting embedded capture
|
|
1248
|
-
credentials. Keep the backend's current
|
|
1249
|
-
`link session → capture → complete idempotency → 201` ordering for requests
|
|
1250
|
-
from older or unknown SDK versions, and until the deployed `@flopay/react`
|
|
1251
|
-
population has crossed the adoption threshold chosen by the backend rollout.
|
|
1252
|
-
The SDK advertises its version in `x-flo-sdk-version` on session create/read.
|
|
1253
|
-
|
|
1254
|
-
After that threshold, the backend may omit capture credentials from
|
|
1255
|
-
create/read/idempotent-replay responses for requests advertising SDK `>=1.4.9`,
|
|
1256
|
-
provided the session explicitly includes `card` in
|
|
1257
|
-
`gateways.stripe.enabledPaymentMethods` and the nonce-protected
|
|
1258
|
-
`POST /v1/checkouts/sessions/{id}/vault/capture` route remains available.
|
|
1259
|
-
Backend rollout verification should confirm that create/read/replay no longer
|
|
1260
|
-
invoke PCIVault and that capture issuance occurs only through the deferred
|
|
1261
|
-
endpoint when a buyer enters the card path.
|
|
573
|
+
| `sessionId` / `nonce` | `string` | Checkout session and its token |
|
|
574
|
+
| `billingApiUrl` | `string` | Billing API base URL |
|
|
575
|
+
| `clientId` | `string` | PayPal client id (`gateways.paypal.publishableKey`) |
|
|
576
|
+
| `environment` | `GatewayEnvironment?` | `sandbox` or `live` |
|
|
577
|
+
| `currency` | `string` | ISO 4217 currency code |
|
|
578
|
+
| `isSubscription` | `boolean` | Fallback flow selection when the session advertises no `providerObjectType` |
|
|
579
|
+
| `approvalPresentation` | `PayPalApprovalPresentation?` | Billing-API-advertised approval mode |
|
|
580
|
+
| `session` | `CheckoutSession \| null` | Backing session; its advertised values are authoritative |
|
|
581
|
+
| `onComplete` / `onDecline` | callbacks | Payment outcomes |
|
|
582
|
+
| `onTechnicalFailure` | callback | Post-click technical failures, including `popupBlocked` |
|
|
1262
583
|
|
|
1263
584
|
### Types
|
|
1264
585
|
|
|
1265
586
|
| Type | Description |
|
|
1266
587
|
|------|-------------|
|
|
588
|
+
| `FloPayCheckoutProps` | Props for `FloPayCheckout` |
|
|
1267
589
|
| `FloPayProviderProps` | Props for `FloPayProvider` |
|
|
1268
590
|
| `FloPayCardSetupProps` | Props for `FloPayCardSetup` |
|
|
1269
|
-
| `FloPayCardSetupCompleteEvent` | Verified setup
|
|
1270
|
-
| `FloPayCardSetupDeclineEvent` |
|
|
1271
|
-
| `FloPayCardSetupCancelEvent` |
|
|
1272
|
-
| `
|
|
591
|
+
| `FloPayCardSetupCompleteEvent` | Verified setup: `status`, `sessionId`, and optional display-only `paymentMethod` |
|
|
592
|
+
| `FloPayCardSetupDeclineEvent` | Declined setup: `status`, `sessionId`, `reason?`, `message?` |
|
|
593
|
+
| `FloPayCardSetupCancelEvent` | Unmounted before a terminal outcome: `status`, `sessionId` |
|
|
594
|
+
| `FloPayAutomaticPaymentButtonProps` | Props for `FloPayAutomaticPaymentButton` |
|
|
595
|
+
| `FloPayAutomaticPaymentSuccessEvent` | `result`, `session`, `sessionId`, and `autoCompleted` |
|
|
1273
596
|
| `SplitCardFormProps` | Props for `SplitCardForm` |
|
|
1274
|
-
| `
|
|
1275
|
-
| `
|
|
597
|
+
| `DirectPayPalButtonProps` | Props for `DirectPayPalButton` |
|
|
598
|
+
| `VaultCardFieldsProps` | Props for `VaultCardFields` |
|
|
599
|
+
| `CheckoutState` | `session`, `loading`, `error`, `claimPending?` |
|
|
600
|
+
| `DeclineEvent` | `method`, `message`, `code?`, `declineCode?` |
|
|
601
|
+
| `BeforeButtonClickEvent` | `method`, `sessionId?`, `createSession?` |
|
|
602
|
+
| `CheckoutButtonMethod` | The payment method a button represents |
|
|
603
|
+
| `InlineSessionDraft` | Draft accepted by `FloPayCheckoutProps.createSession` |
|
|
604
|
+
| `InlineSessionPatch` | Partial draft returned from `FloPayCheckoutProps.onBeforeButtonClick` |
|
|
605
|
+
| `FloInstrumentEvent` | Versioned merchant instrument union |
|
|
606
|
+
|
|
607
|
+
## Errors
|
|
608
|
+
|
|
609
|
+
Checkout errors reach `onError` (and the `error` render prop) as `FloPayError` values from `@flopay/shared`. Branch on `type` and `code`; raw provider messages are never shown to buyers or passed to callbacks. Declines are not errors: they reach `onDecline` with a `DeclineEvent`.
|
|
610
|
+
|
|
611
|
+
| Code | Type | When |
|
|
612
|
+
|------|------|------|
|
|
613
|
+
| `checkout_session_expired` | `api_error` | `FloPayCheckout` or `FloPayAutomaticPaymentButton` loaded an expired session. Create a fresh session and send the buyer back through checkout. |
|
|
614
|
+
| `CurrencyRequired` | `validation_error` | `createSession` has no resolvable currency. Thrown before any request. |
|
|
615
|
+
| `InvalidIdempotencyKey` | `validation_error` | `createSession.idempotencyKey` is blank or longer than 255 characters. |
|
|
616
|
+
| `CaptureMethodUnsupportedForSubscription` | `validation_error` | Manual capture was requested for a cart with a subscription. |
|
|
617
|
+
| `BuyerEmailCollectionRequired` | `validation_error` | A buyer email supplied to the checkout is not a valid email address. |
|
|
618
|
+
| `UnsupportedBackendVaultCapability` | `api_error` | The session offers no hosted card form or card capability and no other usable payment method. |
|
|
619
|
+
| `CheckoutInitializationFailed` | `api_error` | Neither the hosted card form nor Direct PayPal could render after Stripe.js failed to initialize. |
|
|
620
|
+
| `paypal_init_timeout` | `api_error` | A PayPal-only checkout could not start PayPal after automatic recovery. The **Retry PayPal** button tries again. |
|
|
621
|
+
| `external_payment_method_failed` | `api_error` | A wallet, PayPal, or alternative method failed technically after the buyer clicked it. The chooser offers a retry. |
|
|
622
|
+
| `InvalidCardSetupSurface` | `validation_error` | `FloPayCheckout` was given a card-setup session; use `FloPayCardSetup`. |
|
|
623
|
+
| `three_ds_aborted` / `three_ds_timeout` | `api_error` | The buyer cancelled, or did not finish, the authentication challenge for a saved card in confirm or auto mode. |
|
|
624
|
+
| `InvalidPaymentProcessResponse` | `api_error` | Saved-payment processing returned a response the SDK could not use. |
|
|
625
|
+
|
|
626
|
+
A `validation_error` is also reported when both `showStripe` and `showPayPal` are false with no PayPal gateway, or when a session advertises no supported gateway. When saving the AVS billing address is rejected with a `4xx`, `onError` receives the parsed billing API error while the message shows inline; the buyer corrects it for the next attempt.
|
|
627
|
+
|
|
628
|
+
`FloPayCardSetup` reports failures to `onError` as `FloPayCardSetupError`, which adds `retryable`. Network, rate-limit, `5xx`, card-form timeout, and unexpected failures are retryable; validation failures are not.
|
|
629
|
+
|
|
630
|
+
| Code | When |
|
|
631
|
+
|------|------|
|
|
632
|
+
| `MissingCheckoutSessionId` | Rendered without `sessionId` |
|
|
633
|
+
| `MissingCheckoutSessionToken` | Rendered without `nonce` |
|
|
634
|
+
| `InvalidCardSetupSession` | The session is not a zero-amount, product-free setup session |
|
|
635
|
+
| `UnsupportedBackendVaultCapability` | The setup session has no hosted card capability |
|
|
636
|
+
| `card_setup_widget_failed` | The hosted card form failed at runtime (retryable) |
|
|
637
|
+
| `card_setup_load_failed` | Loading the session or card form failed for another reason |
|