@flopay/react 1.3.0 → 1.3.1
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 +23 -0
- package/dist/index.cjs +112 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +115 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -464,6 +464,29 @@ The `SplitCardForm` layout:
|
|
|
464
464
|
6. Full Name input
|
|
465
465
|
7. Submit button
|
|
466
466
|
|
|
467
|
+
#### AVS postcode validation
|
|
468
|
+
|
|
469
|
+
When AVS collection is enabled (`enableAVS`) and the postcode field is shown,
|
|
470
|
+
`SplitCardForm` validates the postcode **format against the live selected
|
|
471
|
+
country before the card is captured**, on both the Stripe and vault card paths.
|
|
472
|
+
It reuses `@flopay/shared`'s country-aware
|
|
473
|
+
[postcode helpers](../shared/README.md#postal-code-helpers) (the same
|
|
474
|
+
`validator` rules the billing API applies), so client and server agree.
|
|
475
|
+
|
|
476
|
+
- **Supported country + malformed postcode** → the submit is blocked (the vault
|
|
477
|
+
widget's submit button is gated; the Stripe path returns before tokenizing)
|
|
478
|
+
and an inline, country-specific message shows the expected format
|
|
479
|
+
(e.g. *"Enter a valid ZIP Code (e.g. 12345 or 12345-6789)"*). On the vault
|
|
480
|
+
path the inline message appears once the field is blurred, since the disabled
|
|
481
|
+
submit means it can't be reached by a submit attempt.
|
|
482
|
+
- **Supported country + empty postcode** → the same submit gate applies, and
|
|
483
|
+
once the field is blurred (or a submit is attempted) an inline *required*
|
|
484
|
+
message shows (e.g. *"ZIP Code is required"*) rather than the expected-format
|
|
485
|
+
copy.
|
|
486
|
+
- **Country with no postal system** (or a locale `validator` doesn't recognise)
|
|
487
|
+
→ the postcode field is shown but **optional**: empty and format checks are
|
|
488
|
+
both skipped, so these buyers are never blocked.
|
|
489
|
+
|
|
467
490
|
### Vault PCI card form
|
|
468
491
|
|
|
469
492
|
When the billing API returns a hosted vault card form on the session
|
package/dist/index.cjs
CHANGED
|
@@ -1447,6 +1447,18 @@ function getButtonMethodLabel(method) {
|
|
|
1447
1447
|
return "Card";
|
|
1448
1448
|
}
|
|
1449
1449
|
}
|
|
1450
|
+
function malformedPostcodeMessage(country) {
|
|
1451
|
+
const example = (0, import_shared5.getPostalCodeExample)(country);
|
|
1452
|
+
return `Enter a valid ${(0, import_shared5.getPostalCodeLabel)(country)}${example ? ` (e.g. ${example})` : ""}`;
|
|
1453
|
+
}
|
|
1454
|
+
function computePostalCodeState(country, zip, visible) {
|
|
1455
|
+
const supported = (0, import_shared5.isPostalCodeSupported)(country);
|
|
1456
|
+
const trimmed = zip.trim();
|
|
1457
|
+
const required = visible && supported;
|
|
1458
|
+
const empty = required && !trimmed;
|
|
1459
|
+
const malformed = required && !!trimmed && !(0, import_shared5.isValidPostalCode)(country, trimmed);
|
|
1460
|
+
return { visible, supported, required, empty, malformed };
|
|
1461
|
+
}
|
|
1450
1462
|
function normalizeBeforeButtonClickError(method, err) {
|
|
1451
1463
|
return err instanceof import_shared6.FloPayError ? err : new import_shared6.FloPayError(
|
|
1452
1464
|
err instanceof Error ? err.message : `${getButtonMethodLabel(method)} before-click hook failed.`,
|
|
@@ -2679,6 +2691,11 @@ function SplitCardFormInner({
|
|
|
2679
2691
|
if (!flopay || !vaultActive || !sessionId) return null;
|
|
2680
2692
|
return flopay.cardCapture({ sessionId });
|
|
2681
2693
|
}, [flopay, vaultActive, sessionId]);
|
|
2694
|
+
const postalCodeState = (0, import_react9.useMemo)(() => {
|
|
2695
|
+
const cc = selectedCountry;
|
|
2696
|
+
const visible = avsConfig ? (0, import_shared5.isAVSFieldVisible)(avsConfig.postal_code, cc) : false;
|
|
2697
|
+
return computePostalCodeState(cc, zipCode, visible);
|
|
2698
|
+
}, [avsConfig, selectedCountry, zipCode]);
|
|
2682
2699
|
const { avsInvalid, invalidAvsFields } = (0, import_react9.useMemo)(() => {
|
|
2683
2700
|
const none = { line1: false, city: false, state: false, zip: false };
|
|
2684
2701
|
if (!vaultActive || !avsConfig) return { avsInvalid: false, invalidAvsFields: none };
|
|
@@ -2688,12 +2705,15 @@ function SplitCardFormInner({
|
|
|
2688
2705
|
line1: isEmpty(avsConfig.address_line_1, addressLine1),
|
|
2689
2706
|
city: isEmpty(avsConfig.city, city),
|
|
2690
2707
|
state: isEmpty(avsConfig.state, stateValue),
|
|
2691
|
-
|
|
2708
|
+
// Empty (required) or malformed both block; unsupported/no-postcode
|
|
2709
|
+
// locales never block (`postalCodeState` fails open above).
|
|
2710
|
+
zip: postalCodeState.empty || postalCodeState.malformed
|
|
2692
2711
|
};
|
|
2693
2712
|
const avsInvalid2 = invalidAvsFields2.line1 || invalidAvsFields2.city || invalidAvsFields2.state || invalidAvsFields2.zip;
|
|
2694
2713
|
return { avsInvalid: avsInvalid2, invalidAvsFields: invalidAvsFields2 };
|
|
2695
|
-
}, [vaultActive, avsConfig, selectedCountry, addressLine1, city, stateValue,
|
|
2714
|
+
}, [vaultActive, avsConfig, selectedCountry, addressLine1, city, stateValue, postalCodeState]);
|
|
2696
2715
|
const [hasAttemptedSubmit, setHasAttemptedSubmit] = (0, import_react9.useState)(false);
|
|
2716
|
+
const [zipTouched, setZipTouched] = (0, import_react9.useState)(false);
|
|
2697
2717
|
const [vaultMount, setVaultMount] = (0, import_react9.useState)(
|
|
2698
2718
|
() => toVaultMount(session?.vault)
|
|
2699
2719
|
);
|
|
@@ -2903,27 +2923,56 @@ function SplitCardFormInner({
|
|
|
2903
2923
|
nonce,
|
|
2904
2924
|
updateError
|
|
2905
2925
|
]);
|
|
2926
|
+
const vaultOutcomeRef = (0, import_react9.useRef)({
|
|
2927
|
+
onComplete,
|
|
2928
|
+
onError,
|
|
2929
|
+
updateError,
|
|
2930
|
+
emitDecline,
|
|
2931
|
+
resolvedAccount,
|
|
2932
|
+
fullName,
|
|
2933
|
+
avsConfig,
|
|
2934
|
+
avsCheckProp,
|
|
2935
|
+
sessionId,
|
|
2936
|
+
nonce,
|
|
2937
|
+
baseUrl
|
|
2938
|
+
});
|
|
2939
|
+
vaultOutcomeRef.current = {
|
|
2940
|
+
onComplete,
|
|
2941
|
+
onError,
|
|
2942
|
+
updateError,
|
|
2943
|
+
emitDecline,
|
|
2944
|
+
resolvedAccount,
|
|
2945
|
+
fullName,
|
|
2946
|
+
avsConfig,
|
|
2947
|
+
avsCheckProp,
|
|
2948
|
+
sessionId,
|
|
2949
|
+
nonce,
|
|
2950
|
+
baseUrl
|
|
2951
|
+
};
|
|
2952
|
+
const vaultCompletedRef = (0, import_react9.useRef)(false);
|
|
2906
2953
|
(0, import_react9.useEffect)(() => {
|
|
2907
2954
|
if (!vaultActive || !cardCapture) return;
|
|
2955
|
+
vaultCompletedRef.current = false;
|
|
2908
2956
|
let cancelled = false;
|
|
2909
2957
|
const offSubmitting = cardCapture.on("submitting", () => {
|
|
2910
2958
|
setHasAttemptedSubmit(true);
|
|
2911
2959
|
setOverlayStatus("processing");
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
const
|
|
2915
|
-
const
|
|
2916
|
-
const
|
|
2917
|
-
const
|
|
2918
|
-
const
|
|
2960
|
+
const { resolvedAccount: resolvedAccount2, avsConfig: avsConfig2, fullName: fullName2, avsCheckProp: avsCheckProp2, baseUrl: baseUrl2, sessionId: sessionId2, nonce: nonce2 } = vaultOutcomeRef.current;
|
|
2961
|
+
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;
|
|
2919
2968
|
const derivedState = line1Visible && !stateVisible && zipVisible ? (0, import_shared5.getStateFromPostalCode)(cc, zipCodeRef.current ?? "") : null;
|
|
2920
2969
|
const stateValue2 = stateVisible ? stateRef.current : derivedState;
|
|
2921
|
-
void new import_js2.PaymentAPI(
|
|
2970
|
+
void new import_js2.PaymentAPI(baseUrl2).patchAccountSnapshot(sessionId2, nonce2, {
|
|
2922
2971
|
accountData: {
|
|
2923
|
-
userId:
|
|
2924
|
-
email:
|
|
2925
|
-
firstName:
|
|
2926
|
-
lastName:
|
|
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(" ") ?? "",
|
|
2927
2976
|
...zipVisible && zipCodeRef.current ? { zip: zipCodeRef.current } : {},
|
|
2928
2977
|
...cityVisible && cityRef.current ? { city: cityRef.current } : {},
|
|
2929
2978
|
...stateValue2 ? { state: stateValue2 } : {},
|
|
@@ -2931,10 +2980,10 @@ function SplitCardFormInner({
|
|
|
2931
2980
|
...line2Visible && addressLine2Ref.current ? { addressLine2: addressLine2Ref.current } : {},
|
|
2932
2981
|
country: cc
|
|
2933
2982
|
},
|
|
2934
|
-
...
|
|
2935
|
-
...
|
|
2983
|
+
...avsCheckProp2 !== void 0 ? { avsCheck: avsCheckProp2 } : {},
|
|
2984
|
+
...avsConfig2 ? {
|
|
2936
2985
|
avsConfig: {
|
|
2937
|
-
country: (0, import_shared5.isAVSFieldVisible)(
|
|
2986
|
+
country: (0, import_shared5.isAVSFieldVisible)(avsConfig2.country, cc),
|
|
2938
2987
|
postal_code: zipVisible,
|
|
2939
2988
|
address_line_1: line1Visible,
|
|
2940
2989
|
address_line_2: line2Visible,
|
|
@@ -2946,30 +2995,33 @@ function SplitCardFormInner({
|
|
|
2946
2995
|
});
|
|
2947
2996
|
});
|
|
2948
2997
|
const offComplete = cardCapture.on("complete", async (event) => {
|
|
2949
|
-
markSessionRecentlyCompleted(event.sessionId ?? sessionId);
|
|
2998
|
+
markSessionRecentlyCompleted(event.sessionId ?? vaultOutcomeRef.current.sessionId);
|
|
2950
2999
|
setOverlayStatus("success");
|
|
2951
3000
|
await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_SUCCESS_DELAY_MS));
|
|
2952
|
-
if (
|
|
2953
|
-
|
|
3001
|
+
if (vaultCompletedRef.current) return;
|
|
3002
|
+
vaultCompletedRef.current = true;
|
|
3003
|
+
vaultOutcomeRef.current.onComplete?.({
|
|
2954
3004
|
status: "succeeded",
|
|
2955
3005
|
paymentIntentId: event.intentId,
|
|
2956
3006
|
checkoutMethod: "card"
|
|
2957
3007
|
});
|
|
2958
3008
|
});
|
|
2959
3009
|
const offDecline = cardCapture.on("decline", async (event) => {
|
|
3010
|
+
const { updateError: updateError2, emitDecline: emitDecline2 } = vaultOutcomeRef.current;
|
|
2960
3011
|
const message = event.message ?? "Your payment was declined. Please try another card or contact your bank.";
|
|
2961
|
-
|
|
3012
|
+
updateError2(message);
|
|
2962
3013
|
setOverlayStatus("error");
|
|
2963
|
-
|
|
3014
|
+
emitDecline2("card", message, event.declineReason ? { declineCode: event.declineReason } : void 0);
|
|
2964
3015
|
await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
|
|
2965
3016
|
if (cancelled) return;
|
|
2966
3017
|
setOverlayStatus(null);
|
|
2967
3018
|
});
|
|
2968
3019
|
const offError = cardCapture.on("error", async (event) => {
|
|
3020
|
+
const { updateError: updateError2, onError: onError2 } = vaultOutcomeRef.current;
|
|
2969
3021
|
const message = event.message ?? "There was a problem processing your payment. Please try again.";
|
|
2970
|
-
|
|
3022
|
+
updateError2(message);
|
|
2971
3023
|
setOverlayStatus("error");
|
|
2972
|
-
|
|
3024
|
+
onError2?.(new import_shared6.FloPayError(message, "api_error"));
|
|
2973
3025
|
await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
|
|
2974
3026
|
if (cancelled) return;
|
|
2975
3027
|
setOverlayStatus(null);
|
|
@@ -2981,25 +3033,7 @@ function SplitCardFormInner({
|
|
|
2981
3033
|
offDecline();
|
|
2982
3034
|
offError();
|
|
2983
3035
|
};
|
|
2984
|
-
}, [
|
|
2985
|
-
vaultActive,
|
|
2986
|
-
cardCapture,
|
|
2987
|
-
sessionId,
|
|
2988
|
-
nonce,
|
|
2989
|
-
baseUrl,
|
|
2990
|
-
avsConfig,
|
|
2991
|
-
avsCheckProp,
|
|
2992
|
-
resolvedAccount.userId,
|
|
2993
|
-
resolvedAccount.email,
|
|
2994
|
-
resolvedAccount.firstName,
|
|
2995
|
-
resolvedAccount.lastName,
|
|
2996
|
-
resolvedAccount.country,
|
|
2997
|
-
fullName,
|
|
2998
|
-
onComplete,
|
|
2999
|
-
onError,
|
|
3000
|
-
updateError,
|
|
3001
|
-
emitDecline
|
|
3002
|
-
]);
|
|
3036
|
+
}, [vaultActive, cardCapture]);
|
|
3003
3037
|
const directPaypalConfigured = !!directPaypal?.clientId;
|
|
3004
3038
|
const hasEnabledMethodsPayload = Array.isArray(enabledPaymentMethods);
|
|
3005
3039
|
const hasEnabledMethods = hasEnabledMethodsPayload && enabledPaymentMethods.length > 0;
|
|
@@ -3501,10 +3535,21 @@ function SplitCardFormInner({
|
|
|
3501
3535
|
try {
|
|
3502
3536
|
if (avsConfig) {
|
|
3503
3537
|
const country = selectedCountryRef.current;
|
|
3504
|
-
|
|
3538
|
+
const postal = computePostalCodeState(
|
|
3539
|
+
country,
|
|
3540
|
+
zipCodeRef.current,
|
|
3541
|
+
(0, import_shared5.isAVSFieldVisible)(avsConfig.postal_code, country)
|
|
3542
|
+
);
|
|
3543
|
+
if (postal.empty) {
|
|
3544
|
+
setZipTouched(true);
|
|
3505
3545
|
updateError((0, import_shared5.getPostalCodeLabel)(country) + " is required");
|
|
3506
3546
|
return;
|
|
3507
3547
|
}
|
|
3548
|
+
if (postal.malformed) {
|
|
3549
|
+
setZipTouched(true);
|
|
3550
|
+
updateError(malformedPostcodeMessage(country));
|
|
3551
|
+
return;
|
|
3552
|
+
}
|
|
3508
3553
|
if ((0, import_shared5.isAVSFieldVisible)(avsConfig.address_line_1, country) && !addressLine1Ref.current.trim()) {
|
|
3509
3554
|
updateError("Street address is required");
|
|
3510
3555
|
return;
|
|
@@ -3645,6 +3690,8 @@ function SplitCardFormInner({
|
|
|
3645
3690
|
const resolvedBorder = bStyles.cardInputBorder ?? (isButtons ? "#e5e7eb" : "#A4A4FF");
|
|
3646
3691
|
const resolvedDangerColor = appearanceVars?.colorDanger ?? "#dc2626";
|
|
3647
3692
|
const avsBorderColor = (fieldInvalid) => hasAttemptedSubmit && fieldInvalid ? resolvedDangerColor : resolvedBorder;
|
|
3693
|
+
const showPostcodeError = (postalCodeState.malformed || postalCodeState.empty) && (zipTouched || hasAttemptedSubmit);
|
|
3694
|
+
const zipBorderColor = showPostcodeError ? resolvedDangerColor : avsBorderColor(invalidAvsFields.zip);
|
|
3648
3695
|
const cardBg = bStyles.cardFormContainer?.backgroundColor ?? themedWrapperBg ?? (isButtons ? "white" : "#EDEDFF");
|
|
3649
3696
|
const cardInputBg = bStyles.cardInputBackground ?? appearanceVars?.colorBackground ?? "white";
|
|
3650
3697
|
const hideBackButtonLabel = isEmptySlotContent(cardBackButtonContent);
|
|
@@ -4074,7 +4121,7 @@ function SplitCardFormInner({
|
|
|
4074
4121
|
(0, import_shared5.isAVSFieldVisible)(avsConfig.postal_code, cc) && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
|
|
4075
4122
|
flex: avsLayoutProp === "row" ? 1 : void 0,
|
|
4076
4123
|
backgroundColor: cardInputBg,
|
|
4077
|
-
border: `1px solid ${
|
|
4124
|
+
border: `1px solid ${zipBorderColor}`,
|
|
4078
4125
|
padding: "10px",
|
|
4079
4126
|
...avsLayoutProp === "row" && (0, import_shared5.isAVSFieldVisible)(avsConfig.country, cc) ? { borderRadius: "0", borderTopRightRadius: resolvedBorderRadius, borderBottomRightRadius: resolvedBorderRadius } : { borderRadius: resolvedBorderRadius }
|
|
4080
4127
|
}, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
@@ -4091,13 +4138,31 @@ function SplitCardFormInner({
|
|
|
4091
4138
|
setZipCode(e.target.value);
|
|
4092
4139
|
onZipChange?.(e.target.value);
|
|
4093
4140
|
},
|
|
4141
|
+
onBlur: () => setZipTouched(true),
|
|
4094
4142
|
disabled: isSubmitting,
|
|
4095
|
-
required:
|
|
4143
|
+
required: postalCodeState.required,
|
|
4144
|
+
"aria-invalid": showPostcodeError || void 0,
|
|
4145
|
+
"aria-describedby": showPostcodeError ? "flopay-billing-postal-code-error" : void 0,
|
|
4096
4146
|
"data-testid": "flopay-zip",
|
|
4097
4147
|
style: inputFieldStyle()
|
|
4098
4148
|
}
|
|
4099
4149
|
) })
|
|
4100
|
-
] })
|
|
4150
|
+
] }),
|
|
4151
|
+
showPostcodeError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
4152
|
+
"div",
|
|
4153
|
+
{
|
|
4154
|
+
id: "flopay-billing-postal-code-error",
|
|
4155
|
+
role: "alert",
|
|
4156
|
+
"data-testid": "flopay-zip-error",
|
|
4157
|
+
style: {
|
|
4158
|
+
marginTop: "0.375rem",
|
|
4159
|
+
color: resolvedDangerColor,
|
|
4160
|
+
...sharedInputTypography,
|
|
4161
|
+
fontSize: "0.75rem"
|
|
4162
|
+
},
|
|
4163
|
+
children: postalCodeState.empty ? `${(0, import_shared5.getPostalCodeLabel)(cc)} is required` : malformedPostcodeMessage(cc)
|
|
4164
|
+
}
|
|
4165
|
+
)
|
|
4101
4166
|
] });
|
|
4102
4167
|
})() }),
|
|
4103
4168
|
vaultActive && cardPreFormSlot && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { order: -2, width: "100%" }, children: cardPreFormSlot }),
|