@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/dist/index.mjs CHANGED
@@ -248,6 +248,9 @@ import {
248
248
  resolveAVSConfig,
249
249
  isAVSFieldVisible,
250
250
  getStateFromPostalCode,
251
+ isPostalCodeSupported,
252
+ isValidPostalCode,
253
+ getPostalCodeExample,
251
254
  filterStripeMethodsByCountry,
252
255
  getStripeMethodDisplayName,
253
256
  hasVendoredStripeMethodLogo,
@@ -1416,6 +1419,18 @@ function getButtonMethodLabel(method) {
1416
1419
  return "Card";
1417
1420
  }
1418
1421
  }
1422
+ function malformedPostcodeMessage(country) {
1423
+ const example = getPostalCodeExample(country);
1424
+ return `Enter a valid ${getPostalCodeLabel(country)}${example ? ` (e.g. ${example})` : ""}`;
1425
+ }
1426
+ function computePostalCodeState(country, zip, visible) {
1427
+ const supported = isPostalCodeSupported(country);
1428
+ const trimmed = zip.trim();
1429
+ const required = visible && supported;
1430
+ const empty = required && !trimmed;
1431
+ const malformed = required && !!trimmed && !isValidPostalCode(country, trimmed);
1432
+ return { visible, supported, required, empty, malformed };
1433
+ }
1419
1434
  function normalizeBeforeButtonClickError(method, err) {
1420
1435
  return err instanceof FloPayError3 ? err : new FloPayError3(
1421
1436
  err instanceof Error ? err.message : `${getButtonMethodLabel(method)} before-click hook failed.`,
@@ -2648,6 +2663,11 @@ function SplitCardFormInner({
2648
2663
  if (!flopay || !vaultActive || !sessionId) return null;
2649
2664
  return flopay.cardCapture({ sessionId });
2650
2665
  }, [flopay, vaultActive, sessionId]);
2666
+ const postalCodeState = useMemo3(() => {
2667
+ const cc = selectedCountry;
2668
+ const visible = avsConfig ? isAVSFieldVisible(avsConfig.postal_code, cc) : false;
2669
+ return computePostalCodeState(cc, zipCode, visible);
2670
+ }, [avsConfig, selectedCountry, zipCode]);
2651
2671
  const { avsInvalid, invalidAvsFields } = useMemo3(() => {
2652
2672
  const none = { line1: false, city: false, state: false, zip: false };
2653
2673
  if (!vaultActive || !avsConfig) return { avsInvalid: false, invalidAvsFields: none };
@@ -2657,12 +2677,15 @@ function SplitCardFormInner({
2657
2677
  line1: isEmpty(avsConfig.address_line_1, addressLine1),
2658
2678
  city: isEmpty(avsConfig.city, city),
2659
2679
  state: isEmpty(avsConfig.state, stateValue),
2660
- zip: isEmpty(avsConfig.postal_code, zipCode)
2680
+ // Empty (required) or malformed both block; unsupported/no-postcode
2681
+ // locales never block (`postalCodeState` fails open above).
2682
+ zip: postalCodeState.empty || postalCodeState.malformed
2661
2683
  };
2662
2684
  const avsInvalid2 = invalidAvsFields2.line1 || invalidAvsFields2.city || invalidAvsFields2.state || invalidAvsFields2.zip;
2663
2685
  return { avsInvalid: avsInvalid2, invalidAvsFields: invalidAvsFields2 };
2664
- }, [vaultActive, avsConfig, selectedCountry, addressLine1, city, stateValue, zipCode]);
2686
+ }, [vaultActive, avsConfig, selectedCountry, addressLine1, city, stateValue, postalCodeState]);
2665
2687
  const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState3(false);
2688
+ const [zipTouched, setZipTouched] = useState3(false);
2666
2689
  const [vaultMount, setVaultMount] = useState3(
2667
2690
  () => toVaultMount(session?.vault)
2668
2691
  );
@@ -2872,27 +2895,56 @@ function SplitCardFormInner({
2872
2895
  nonce,
2873
2896
  updateError
2874
2897
  ]);
2898
+ const vaultOutcomeRef = useRef4({
2899
+ onComplete,
2900
+ onError,
2901
+ updateError,
2902
+ emitDecline,
2903
+ resolvedAccount,
2904
+ fullName,
2905
+ avsConfig,
2906
+ avsCheckProp,
2907
+ sessionId,
2908
+ nonce,
2909
+ baseUrl
2910
+ });
2911
+ vaultOutcomeRef.current = {
2912
+ onComplete,
2913
+ onError,
2914
+ updateError,
2915
+ emitDecline,
2916
+ resolvedAccount,
2917
+ fullName,
2918
+ avsConfig,
2919
+ avsCheckProp,
2920
+ sessionId,
2921
+ nonce,
2922
+ baseUrl
2923
+ };
2924
+ const vaultCompletedRef = useRef4(false);
2875
2925
  useEffect5(() => {
2876
2926
  if (!vaultActive || !cardCapture) return;
2927
+ vaultCompletedRef.current = false;
2877
2928
  let cancelled = false;
2878
2929
  const offSubmitting = cardCapture.on("submitting", () => {
2879
2930
  setHasAttemptedSubmit(true);
2880
2931
  setOverlayStatus("processing");
2881
- if (!sessionId || !nonce) return;
2882
- const cc = selectedCountryRef.current || resolvedAccount.country || "US";
2883
- const stateVisible = avsConfig ? isAVSFieldVisible(avsConfig.state, cc) : false;
2884
- const line1Visible = avsConfig ? isAVSFieldVisible(avsConfig.address_line_1, cc) : false;
2885
- const zipVisible = avsConfig ? isAVSFieldVisible(avsConfig.postal_code, cc) : false;
2886
- const cityVisible = avsConfig ? isAVSFieldVisible(avsConfig.city, cc) : false;
2887
- const line2Visible = avsConfig ? isAVSFieldVisible(avsConfig.address_line_2, cc) : false;
2932
+ const { resolvedAccount: resolvedAccount2, avsConfig: avsConfig2, fullName: fullName2, avsCheckProp: avsCheckProp2, baseUrl: baseUrl2, sessionId: sessionId2, nonce: nonce2 } = vaultOutcomeRef.current;
2933
+ if (!sessionId2 || !nonce2) return;
2934
+ const cc = selectedCountryRef.current || resolvedAccount2.country || "US";
2935
+ const stateVisible = avsConfig2 ? isAVSFieldVisible(avsConfig2.state, cc) : false;
2936
+ const line1Visible = avsConfig2 ? isAVSFieldVisible(avsConfig2.address_line_1, cc) : false;
2937
+ const zipVisible = avsConfig2 ? isAVSFieldVisible(avsConfig2.postal_code, cc) : false;
2938
+ const cityVisible = avsConfig2 ? isAVSFieldVisible(avsConfig2.city, cc) : false;
2939
+ const line2Visible = avsConfig2 ? isAVSFieldVisible(avsConfig2.address_line_2, cc) : false;
2888
2940
  const derivedState = line1Visible && !stateVisible && zipVisible ? getStateFromPostalCode(cc, zipCodeRef.current ?? "") : null;
2889
2941
  const stateValue2 = stateVisible ? stateRef.current : derivedState;
2890
- void new PaymentAPI2(baseUrl).patchAccountSnapshot(sessionId, nonce, {
2942
+ void new PaymentAPI2(baseUrl2).patchAccountSnapshot(sessionId2, nonce2, {
2891
2943
  accountData: {
2892
- userId: resolvedAccount.userId ?? "",
2893
- email: resolvedAccount.email ?? "",
2894
- firstName: resolvedAccount.firstName ?? fullName.trim().split(/\s+/)[0] ?? "",
2895
- lastName: resolvedAccount.lastName ?? fullName.trim().split(/\s+/).slice(1).join(" ") ?? "",
2944
+ userId: resolvedAccount2.userId ?? "",
2945
+ email: resolvedAccount2.email ?? "",
2946
+ firstName: resolvedAccount2.firstName ?? fullName2.trim().split(/\s+/)[0] ?? "",
2947
+ lastName: resolvedAccount2.lastName ?? fullName2.trim().split(/\s+/).slice(1).join(" ") ?? "",
2896
2948
  ...zipVisible && zipCodeRef.current ? { zip: zipCodeRef.current } : {},
2897
2949
  ...cityVisible && cityRef.current ? { city: cityRef.current } : {},
2898
2950
  ...stateValue2 ? { state: stateValue2 } : {},
@@ -2900,10 +2952,10 @@ function SplitCardFormInner({
2900
2952
  ...line2Visible && addressLine2Ref.current ? { addressLine2: addressLine2Ref.current } : {},
2901
2953
  country: cc
2902
2954
  },
2903
- ...avsCheckProp !== void 0 ? { avsCheck: avsCheckProp } : {},
2904
- ...avsConfig ? {
2955
+ ...avsCheckProp2 !== void 0 ? { avsCheck: avsCheckProp2 } : {},
2956
+ ...avsConfig2 ? {
2905
2957
  avsConfig: {
2906
- country: isAVSFieldVisible(avsConfig.country, cc),
2958
+ country: isAVSFieldVisible(avsConfig2.country, cc),
2907
2959
  postal_code: zipVisible,
2908
2960
  address_line_1: line1Visible,
2909
2961
  address_line_2: line2Visible,
@@ -2915,30 +2967,33 @@ function SplitCardFormInner({
2915
2967
  });
2916
2968
  });
2917
2969
  const offComplete = cardCapture.on("complete", async (event) => {
2918
- markSessionRecentlyCompleted(event.sessionId ?? sessionId);
2970
+ markSessionRecentlyCompleted(event.sessionId ?? vaultOutcomeRef.current.sessionId);
2919
2971
  setOverlayStatus("success");
2920
2972
  await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_SUCCESS_DELAY_MS));
2921
- if (cancelled) return;
2922
- onComplete?.({
2973
+ if (vaultCompletedRef.current) return;
2974
+ vaultCompletedRef.current = true;
2975
+ vaultOutcomeRef.current.onComplete?.({
2923
2976
  status: "succeeded",
2924
2977
  paymentIntentId: event.intentId,
2925
2978
  checkoutMethod: "card"
2926
2979
  });
2927
2980
  });
2928
2981
  const offDecline = cardCapture.on("decline", async (event) => {
2982
+ const { updateError: updateError2, emitDecline: emitDecline2 } = vaultOutcomeRef.current;
2929
2983
  const message = event.message ?? "Your payment was declined. Please try another card or contact your bank.";
2930
- updateError(message);
2984
+ updateError2(message);
2931
2985
  setOverlayStatus("error");
2932
- emitDecline("card", message, event.declineReason ? { declineCode: event.declineReason } : void 0);
2986
+ emitDecline2("card", message, event.declineReason ? { declineCode: event.declineReason } : void 0);
2933
2987
  await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
2934
2988
  if (cancelled) return;
2935
2989
  setOverlayStatus(null);
2936
2990
  });
2937
2991
  const offError = cardCapture.on("error", async (event) => {
2992
+ const { updateError: updateError2, onError: onError2 } = vaultOutcomeRef.current;
2938
2993
  const message = event.message ?? "There was a problem processing your payment. Please try again.";
2939
- updateError(message);
2994
+ updateError2(message);
2940
2995
  setOverlayStatus("error");
2941
- onError?.(new FloPayError3(message, "api_error"));
2996
+ onError2?.(new FloPayError3(message, "api_error"));
2942
2997
  await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
2943
2998
  if (cancelled) return;
2944
2999
  setOverlayStatus(null);
@@ -2950,25 +3005,7 @@ function SplitCardFormInner({
2950
3005
  offDecline();
2951
3006
  offError();
2952
3007
  };
2953
- }, [
2954
- vaultActive,
2955
- cardCapture,
2956
- sessionId,
2957
- nonce,
2958
- baseUrl,
2959
- avsConfig,
2960
- avsCheckProp,
2961
- resolvedAccount.userId,
2962
- resolvedAccount.email,
2963
- resolvedAccount.firstName,
2964
- resolvedAccount.lastName,
2965
- resolvedAccount.country,
2966
- fullName,
2967
- onComplete,
2968
- onError,
2969
- updateError,
2970
- emitDecline
2971
- ]);
3008
+ }, [vaultActive, cardCapture]);
2972
3009
  const directPaypalConfigured = !!directPaypal?.clientId;
2973
3010
  const hasEnabledMethodsPayload = Array.isArray(enabledPaymentMethods);
2974
3011
  const hasEnabledMethods = hasEnabledMethodsPayload && enabledPaymentMethods.length > 0;
@@ -3470,10 +3507,21 @@ function SplitCardFormInner({
3470
3507
  try {
3471
3508
  if (avsConfig) {
3472
3509
  const country = selectedCountryRef.current;
3473
- if (isAVSFieldVisible(avsConfig.postal_code, country) && !zipCodeRef.current.trim()) {
3510
+ const postal = computePostalCodeState(
3511
+ country,
3512
+ zipCodeRef.current,
3513
+ isAVSFieldVisible(avsConfig.postal_code, country)
3514
+ );
3515
+ if (postal.empty) {
3516
+ setZipTouched(true);
3474
3517
  updateError(getPostalCodeLabel(country) + " is required");
3475
3518
  return;
3476
3519
  }
3520
+ if (postal.malformed) {
3521
+ setZipTouched(true);
3522
+ updateError(malformedPostcodeMessage(country));
3523
+ return;
3524
+ }
3477
3525
  if (isAVSFieldVisible(avsConfig.address_line_1, country) && !addressLine1Ref.current.trim()) {
3478
3526
  updateError("Street address is required");
3479
3527
  return;
@@ -3614,6 +3662,8 @@ function SplitCardFormInner({
3614
3662
  const resolvedBorder = bStyles.cardInputBorder ?? (isButtons ? "#e5e7eb" : "#A4A4FF");
3615
3663
  const resolvedDangerColor = appearanceVars?.colorDanger ?? "#dc2626";
3616
3664
  const avsBorderColor = (fieldInvalid) => hasAttemptedSubmit && fieldInvalid ? resolvedDangerColor : resolvedBorder;
3665
+ const showPostcodeError = (postalCodeState.malformed || postalCodeState.empty) && (zipTouched || hasAttemptedSubmit);
3666
+ const zipBorderColor = showPostcodeError ? resolvedDangerColor : avsBorderColor(invalidAvsFields.zip);
3617
3667
  const cardBg = bStyles.cardFormContainer?.backgroundColor ?? themedWrapperBg ?? (isButtons ? "white" : "#EDEDFF");
3618
3668
  const cardInputBg = bStyles.cardInputBackground ?? appearanceVars?.colorBackground ?? "white";
3619
3669
  const hideBackButtonLabel = isEmptySlotContent(cardBackButtonContent);
@@ -4043,7 +4093,7 @@ function SplitCardFormInner({
4043
4093
  isAVSFieldVisible(avsConfig.postal_code, cc) && /* @__PURE__ */ jsx7("div", { style: {
4044
4094
  flex: avsLayoutProp === "row" ? 1 : void 0,
4045
4095
  backgroundColor: cardInputBg,
4046
- border: `1px solid ${avsBorderColor(invalidAvsFields.zip)}`,
4096
+ border: `1px solid ${zipBorderColor}`,
4047
4097
  padding: "10px",
4048
4098
  ...avsLayoutProp === "row" && isAVSFieldVisible(avsConfig.country, cc) ? { borderRadius: "0", borderTopRightRadius: resolvedBorderRadius, borderBottomRightRadius: resolvedBorderRadius } : { borderRadius: resolvedBorderRadius }
4049
4099
  }, children: /* @__PURE__ */ jsx7(
@@ -4060,13 +4110,31 @@ function SplitCardFormInner({
4060
4110
  setZipCode(e.target.value);
4061
4111
  onZipChange?.(e.target.value);
4062
4112
  },
4113
+ onBlur: () => setZipTouched(true),
4063
4114
  disabled: isSubmitting,
4064
- required: true,
4115
+ required: postalCodeState.required,
4116
+ "aria-invalid": showPostcodeError || void 0,
4117
+ "aria-describedby": showPostcodeError ? "flopay-billing-postal-code-error" : void 0,
4065
4118
  "data-testid": "flopay-zip",
4066
4119
  style: inputFieldStyle()
4067
4120
  }
4068
4121
  ) })
4069
- ] })
4122
+ ] }),
4123
+ showPostcodeError && /* @__PURE__ */ jsx7(
4124
+ "div",
4125
+ {
4126
+ id: "flopay-billing-postal-code-error",
4127
+ role: "alert",
4128
+ "data-testid": "flopay-zip-error",
4129
+ style: {
4130
+ marginTop: "0.375rem",
4131
+ color: resolvedDangerColor,
4132
+ ...sharedInputTypography,
4133
+ fontSize: "0.75rem"
4134
+ },
4135
+ children: postalCodeState.empty ? `${getPostalCodeLabel(cc)} is required` : malformedPostcodeMessage(cc)
4136
+ }
4137
+ )
4070
4138
  ] });
4071
4139
  })() }),
4072
4140
  vaultActive && cardPreFormSlot && /* @__PURE__ */ jsx7("div", { style: { order: -2, width: "100%" }, children: cardPreFormSlot }),