@flopay/react 1.3.1 → 1.3.3
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 +51 -0
- package/dist/index.cjs +51 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +51 -39
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -329,6 +329,33 @@ Skip the backend API route — create the session directly in the component:
|
|
|
329
329
|
|
|
330
330
|
The component POSTs to the billing API, gets the full session back, and renders the form — zero backend code needed.
|
|
331
331
|
|
|
332
|
+
That create POST sends a stable `Idempotency-Key` header automatically whenever a
|
|
333
|
+
secure RNG is available, so a timeout or lost response never mints a second
|
|
334
|
+
session. `FloPayCheckout` already coalesces concurrent mounts (StrictMode
|
|
335
|
+
double-mount, Suspense remount, navigation flicker) into a single create, and
|
|
336
|
+
rerenders during an in-flight create do **not** replace the key. You normally
|
|
337
|
+
don't need to think about it.
|
|
338
|
+
|
|
339
|
+
To control the key yourself — for example to keep it stable across your own
|
|
340
|
+
server retries — pass `idempotencyKey` on `createSession`:
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
<FloPayCheckout
|
|
344
|
+
createSession={{
|
|
345
|
+
/* … */
|
|
346
|
+
idempotencyKey: `checkout:${orderId}`, // one logical checkout; never reuse for a new purchase
|
|
347
|
+
}}
|
|
348
|
+
/>
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The key must be non-empty and at most 255 characters; an invalid value throws a
|
|
352
|
+
`FloPayError('validation_error')` before any request. Omit it and the SDK
|
|
353
|
+
generates a fresh cryptographically random key per logical create when a secure
|
|
354
|
+
RNG is available; without one it omits the header, and a merchant-supplied
|
|
355
|
+
`idempotencyKey` stays the way to remain idempotent. See the
|
|
356
|
+
[`@flopay/js` idempotency docs](../js/README.md#idempotent-checkout-creation)
|
|
357
|
+
for the full contract.
|
|
358
|
+
|
|
332
359
|
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`.
|
|
333
360
|
|
|
334
361
|
When you use `onBeforeButtonClick` with `createSession`, any returned `InlineSessionPatch` is merged into the draft session params before the selected buttons-layout flow continues. That lets you add tracking data or update account fields just in time without pre-creating a separate backend session.
|
|
@@ -487,6 +514,30 @@ It reuses `@flopay/shared`'s country-aware
|
|
|
487
514
|
→ the postcode field is shown but **optional**: empty and format checks are
|
|
488
515
|
both skipped, so these buyers are never blocked.
|
|
489
516
|
|
|
517
|
+
On the **vault card path** the widget's submit button is gated **synchronously**
|
|
518
|
+
on the client-side AVS check above. Because the client postcode rules mirror the
|
|
519
|
+
backend validator (`validator@13.15.35`) verbatim, a postcode the server's
|
|
520
|
+
`PATCH /v1/checkouts/sessions/{id}/account` would reject is already blocked here —
|
|
521
|
+
before the button ever enables — with no network round-trip in the critical path.
|
|
522
|
+
The gate is **never** held pending an async request: the hosted widget silently
|
|
523
|
+
swallows a click that lands while the button is disabled and offers no way to
|
|
524
|
+
auto-resubmit on release, so an async gate would strand any buyer (or single-click
|
|
525
|
+
preview) who clicked before it resolved — the checkout would write no attempt and
|
|
526
|
+
time out (sdk#124).
|
|
527
|
+
|
|
528
|
+
When the buyer submits, `SplitCardForm` persists the billing snapshot
|
|
529
|
+
best-effort **in parallel** with the widget's charge:
|
|
530
|
+
|
|
531
|
+
- **Backend rejects the address (a `4xx`)** — a residual, non-postcode drift, since
|
|
532
|
+
the postcode is already client-validated — the specific message (e.g. *"Postal/ZIP
|
|
533
|
+
code is not valid for the selected country…"*) shows inline **and** fires `onError`
|
|
534
|
+
with the parsed `FloPayError`, and the processing overlay clears so the buyer can
|
|
535
|
+
correct and retry. The charge cannot be cancelled once submitted, so this surfaces
|
|
536
|
+
for the buyer's *next* attempt rather than stopping the in-flight one.
|
|
537
|
+
- **Transient failure (`5xx` / network)** → best-effort and silent: the backend
|
|
538
|
+
listener falls back to the session's baseline address while the widget completes
|
|
539
|
+
the charge.
|
|
540
|
+
|
|
490
541
|
### Vault PCI card form
|
|
491
542
|
|
|
492
543
|
When the billing API returns a hosted vault card form on the session
|
package/dist/index.cjs
CHANGED
|
@@ -1365,6 +1365,9 @@ var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
|
1365
1365
|
var STRIPE_RESUME_KEY = "flopay_stripe_resume";
|
|
1366
1366
|
var LEGACY_WALLET_RESUME_KEY = "flopay_wallet_resume";
|
|
1367
1367
|
var PAYPAL_RESUME_KEY = "flopay_paypal_resume";
|
|
1368
|
+
function isAccountValidationError(err) {
|
|
1369
|
+
return err instanceof import_shared6.FloPayError && typeof err.statusCode === "number" && err.statusCode >= 400 && err.statusCode < 500;
|
|
1370
|
+
}
|
|
1368
1371
|
function toVaultMount(block) {
|
|
1369
1372
|
if (!block?.html) return null;
|
|
1370
1373
|
return {
|
|
@@ -2866,10 +2869,6 @@ function SplitCardFormInner({
|
|
|
2866
2869
|
},
|
|
2867
2870
|
[onErrorChange]
|
|
2868
2871
|
);
|
|
2869
|
-
(0, import_react9.useEffect)(() => {
|
|
2870
|
-
if (!vaultActive || !cardCapture) return;
|
|
2871
|
-
cardCapture.setSubmitGate?.(avsInvalid);
|
|
2872
|
-
}, [vaultActive, cardCapture, avsInvalid]);
|
|
2873
2872
|
(0, import_react9.useEffect)(() => {
|
|
2874
2873
|
if (!vaultActive || !cardCapture) return;
|
|
2875
2874
|
cardCapture.setCardFieldOrder?.(cardFieldOrder ?? null, avsConfig == null);
|
|
@@ -2950,6 +2949,46 @@ function SplitCardFormInner({
|
|
|
2950
2949
|
baseUrl
|
|
2951
2950
|
};
|
|
2952
2951
|
const vaultCompletedRef = (0, import_react9.useRef)(false);
|
|
2952
|
+
const buildVaultAccountSnapshot = (0, import_react9.useCallback)(() => {
|
|
2953
|
+
const { resolvedAccount: resolvedAccount2, avsConfig: avsConfig2, fullName: fullName2, avsCheckProp: avsCheckProp2 } = vaultOutcomeRef.current;
|
|
2954
|
+
const cc = selectedCountryRef.current || resolvedAccount2.country || "US";
|
|
2955
|
+
const stateVisible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.state, cc) : false;
|
|
2956
|
+
const line1Visible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.address_line_1, cc) : false;
|
|
2957
|
+
const zipVisible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.postal_code, cc) : false;
|
|
2958
|
+
const cityVisible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.city, cc) : false;
|
|
2959
|
+
const line2Visible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.address_line_2, cc) : false;
|
|
2960
|
+
const derivedState = line1Visible && !stateVisible && zipVisible ? (0, import_shared5.getStateFromPostalCode)(cc, (zipCodeRef.current ?? "").trim()) : null;
|
|
2961
|
+
const stateValue2 = stateVisible ? stateRef.current : derivedState;
|
|
2962
|
+
return {
|
|
2963
|
+
accountData: {
|
|
2964
|
+
userId: resolvedAccount2.userId ?? "",
|
|
2965
|
+
email: resolvedAccount2.email ?? "",
|
|
2966
|
+
firstName: resolvedAccount2.firstName ?? fullName2.trim().split(/\s+/)[0] ?? "",
|
|
2967
|
+
lastName: resolvedAccount2.lastName ?? fullName2.trim().split(/\s+/).slice(1).join(" ") ?? "",
|
|
2968
|
+
...zipVisible && zipCodeRef.current ? { zip: zipCodeRef.current } : {},
|
|
2969
|
+
...cityVisible && cityRef.current ? { city: cityRef.current } : {},
|
|
2970
|
+
...stateValue2 ? { state: stateValue2 } : {},
|
|
2971
|
+
...line1Visible && addressLine1Ref.current ? { addressLine1: addressLine1Ref.current } : {},
|
|
2972
|
+
...line2Visible && addressLine2Ref.current ? { addressLine2: addressLine2Ref.current } : {},
|
|
2973
|
+
country: cc
|
|
2974
|
+
},
|
|
2975
|
+
...avsCheckProp2 !== void 0 ? { avsCheck: avsCheckProp2 } : {},
|
|
2976
|
+
...avsConfig2 ? {
|
|
2977
|
+
avsConfig: {
|
|
2978
|
+
country: (0, import_shared5.isAVSFieldVisible)(avsConfig2.country, cc),
|
|
2979
|
+
postal_code: zipVisible,
|
|
2980
|
+
address_line_1: line1Visible,
|
|
2981
|
+
address_line_2: line2Visible,
|
|
2982
|
+
city: cityVisible,
|
|
2983
|
+
state: stateVisible
|
|
2984
|
+
}
|
|
2985
|
+
} : {}
|
|
2986
|
+
};
|
|
2987
|
+
}, []);
|
|
2988
|
+
(0, import_react9.useEffect)(() => {
|
|
2989
|
+
if (!vaultActive || !cardCapture) return;
|
|
2990
|
+
cardCapture.setSubmitGate?.(avsInvalid);
|
|
2991
|
+
}, [vaultActive, cardCapture, avsInvalid]);
|
|
2953
2992
|
(0, import_react9.useEffect)(() => {
|
|
2954
2993
|
if (!vaultActive || !cardCapture) return;
|
|
2955
2994
|
vaultCompletedRef.current = false;
|
|
@@ -2957,41 +2996,14 @@ function SplitCardFormInner({
|
|
|
2957
2996
|
const offSubmitting = cardCapture.on("submitting", () => {
|
|
2958
2997
|
setHasAttemptedSubmit(true);
|
|
2959
2998
|
setOverlayStatus("processing");
|
|
2960
|
-
const {
|
|
2999
|
+
const { baseUrl: baseUrl2, sessionId: sessionId2, nonce: nonce2, updateError: updateError2, onError: onError2 } = vaultOutcomeRef.current;
|
|
2961
3000
|
if (!sessionId2 || !nonce2) return;
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
const derivedState = line1Visible && !stateVisible && zipVisible ? (0, import_shared5.getStateFromPostalCode)(cc, zipCodeRef.current ?? "") : null;
|
|
2969
|
-
const stateValue2 = stateVisible ? stateRef.current : derivedState;
|
|
2970
|
-
void new import_js2.PaymentAPI(baseUrl2).patchAccountSnapshot(sessionId2, nonce2, {
|
|
2971
|
-
accountData: {
|
|
2972
|
-
userId: resolvedAccount2.userId ?? "",
|
|
2973
|
-
email: resolvedAccount2.email ?? "",
|
|
2974
|
-
firstName: resolvedAccount2.firstName ?? fullName2.trim().split(/\s+/)[0] ?? "",
|
|
2975
|
-
lastName: resolvedAccount2.lastName ?? fullName2.trim().split(/\s+/).slice(1).join(" ") ?? "",
|
|
2976
|
-
...zipVisible && zipCodeRef.current ? { zip: zipCodeRef.current } : {},
|
|
2977
|
-
...cityVisible && cityRef.current ? { city: cityRef.current } : {},
|
|
2978
|
-
...stateValue2 ? { state: stateValue2 } : {},
|
|
2979
|
-
...line1Visible && addressLine1Ref.current ? { addressLine1: addressLine1Ref.current } : {},
|
|
2980
|
-
...line2Visible && addressLine2Ref.current ? { addressLine2: addressLine2Ref.current } : {},
|
|
2981
|
-
country: cc
|
|
2982
|
-
},
|
|
2983
|
-
...avsCheckProp2 !== void 0 ? { avsCheck: avsCheckProp2 } : {},
|
|
2984
|
-
...avsConfig2 ? {
|
|
2985
|
-
avsConfig: {
|
|
2986
|
-
country: (0, import_shared5.isAVSFieldVisible)(avsConfig2.country, cc),
|
|
2987
|
-
postal_code: zipVisible,
|
|
2988
|
-
address_line_1: line1Visible,
|
|
2989
|
-
address_line_2: line2Visible,
|
|
2990
|
-
city: cityVisible,
|
|
2991
|
-
state: stateVisible
|
|
2992
|
-
}
|
|
2993
|
-
} : {}
|
|
2994
|
-
}).catch(() => {
|
|
3001
|
+
void new import_js2.PaymentAPI(baseUrl2).patchAccountSnapshot(sessionId2, nonce2, buildVaultAccountSnapshot()).catch((err) => {
|
|
3002
|
+
if (!isAccountValidationError(err)) return;
|
|
3003
|
+
const message = err instanceof Error ? err.message : "Please check your billing address and try again.";
|
|
3004
|
+
updateError2(message);
|
|
3005
|
+
setOverlayStatus(null);
|
|
3006
|
+
onError2?.(err instanceof import_shared6.FloPayError ? err : new import_shared6.FloPayError(message, "api_error"));
|
|
2995
3007
|
});
|
|
2996
3008
|
});
|
|
2997
3009
|
const offComplete = cardCapture.on("complete", async (event) => {
|
|
@@ -3221,7 +3233,7 @@ function SplitCardFormInner({
|
|
|
3221
3233
|
const stateVisible = (0, import_shared5.isAVSFieldVisible)(avsConfig.state, c);
|
|
3222
3234
|
const line1Visible = (0, import_shared5.isAVSFieldVisible)(avsConfig.address_line_1, c);
|
|
3223
3235
|
const zipVisible = (0, import_shared5.isAVSFieldVisible)(avsConfig.postal_code, c);
|
|
3224
|
-
const derivedState = line1Visible && !stateVisible && zipVisible ? (0, import_shared5.getStateFromPostalCode)(c, zipCodeRef.current ?? "") : null;
|
|
3236
|
+
const derivedState = line1Visible && !stateVisible && zipVisible ? (0, import_shared5.getStateFromPostalCode)(c, (zipCodeRef.current ?? "").trim()) : null;
|
|
3225
3237
|
const stateValue2 = stateVisible ? stateRef.current : derivedState;
|
|
3226
3238
|
return {
|
|
3227
3239
|
country: c,
|