@flopay/react 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -487,6 +487,30 @@ It reuses `@flopay/shared`'s country-aware
487
487
  → the postcode field is shown but **optional**: empty and format checks are
488
488
  both skipped, so these buyers are never blocked.
489
489
 
490
+ On the **vault card path** the widget's submit button is gated **synchronously**
491
+ on the client-side AVS check above. Because the client postcode rules mirror the
492
+ backend validator (`validator@13.15.35`) verbatim, a postcode the server's
493
+ `PATCH /v1/checkouts/sessions/{id}/account` would reject is already blocked here —
494
+ before the button ever enables — with no network round-trip in the critical path.
495
+ The gate is **never** held pending an async request: the hosted widget silently
496
+ swallows a click that lands while the button is disabled and offers no way to
497
+ auto-resubmit on release, so an async gate would strand any buyer (or single-click
498
+ preview) who clicked before it resolved — the checkout would write no attempt and
499
+ time out (sdk#124).
500
+
501
+ When the buyer submits, `SplitCardForm` persists the billing snapshot
502
+ best-effort **in parallel** with the widget's charge:
503
+
504
+ - **Backend rejects the address (a `4xx`)** — a residual, non-postcode drift, since
505
+ the postcode is already client-validated — the specific message (e.g. *"Postal/ZIP
506
+ code is not valid for the selected country…"*) shows inline **and** fires `onError`
507
+ with the parsed `FloPayError`, and the processing overlay clears so the buyer can
508
+ correct and retry. The charge cannot be cancelled once submitted, so this surfaces
509
+ for the buyer's *next* attempt rather than stopping the in-flight one.
510
+ - **Transient failure (`5xx` / network)** → best-effort and silent: the backend
511
+ listener falls back to the session's baseline address while the widget completes
512
+ the charge.
513
+
490
514
  ### Vault PCI card form
491
515
 
492
516
  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 { resolvedAccount: resolvedAccount2, avsConfig: avsConfig2, fullName: fullName2, avsCheckProp: avsCheckProp2, baseUrl: baseUrl2, sessionId: sessionId2, nonce: nonce2 } = vaultOutcomeRef.current;
2999
+ const { baseUrl: baseUrl2, sessionId: sessionId2, nonce: nonce2, updateError: updateError2, onError: onError2 } = vaultOutcomeRef.current;
2961
3000
  if (!sessionId2 || !nonce2) return;
2962
- const cc = selectedCountryRef.current || resolvedAccount2.country || "US";
2963
- const stateVisible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.state, cc) : false;
2964
- const line1Visible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.address_line_1, cc) : false;
2965
- const zipVisible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.postal_code, cc) : false;
2966
- const cityVisible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.city, cc) : false;
2967
- const line2Visible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.address_line_2, cc) : false;
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,