@flopay/react 1.3.2 → 1.3.4

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
@@ -100,7 +100,7 @@ function FloPayProvider({
100
100
  }
101
101
 
102
102
  // src/flopay-checkout.tsx
103
- import React8, { useCallback as useCallback2, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef5, useState as useState4 } from "react";
103
+ import React8, { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef5, useState as useState4 } from "react";
104
104
  import { PaymentAPI as PaymentAPI4 } from "@flopay/js";
105
105
  import { SDK_VERSION, FloPayError as FloPayError5, resolveBillingApiUrl as resolveBillingApiUrl3, buildCheckoutDisplayData, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme2, resolveTheme as resolveTheme2 } from "@flopay/shared";
106
106
 
@@ -333,7 +333,7 @@ function VaultCardFields({
333
333
  }
334
334
 
335
335
  // src/split-card-form.tsx
336
- import React7, { forwardRef, useCallback, useContext as useContext3, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
336
+ import React7, { forwardRef, useCallback as useCallback2, useContext as useContext3, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
337
337
 
338
338
  // src/hooks.ts
339
339
  import { useContext as useContext2 } from "react";
@@ -770,12 +770,67 @@ function isInAppBrowser(userAgent) {
770
770
  }
771
771
 
772
772
  // src/direct-paypal-button.tsx
773
- import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
773
+ import { useCallback, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
774
774
  import { loadScript } from "@paypal/paypal-js";
775
775
  import { PaymentAPI } from "@flopay/js";
776
776
  import { FloPayError as FloPayError2, normalizeGatewayEnvironment } from "@flopay/shared";
777
+
778
+ // src/external-method-recovery.ts
779
+ var EXTERNAL_METHOD_CALLBACK_GRACE_MS = 600;
780
+ function getProviderErrorMessage(err) {
781
+ if (err instanceof Error) return err.message;
782
+ if (typeof err === "object" && err !== null) {
783
+ const message = err.message;
784
+ return typeof message === "string" ? message : void 0;
785
+ }
786
+ return typeof err === "string" ? err : void 0;
787
+ }
788
+ function sanitizeExternalFailureCode(value) {
789
+ if (typeof value !== "string") return void 0;
790
+ const trimmed = value.trim();
791
+ return /^[a-z0-9_.-]{1,64}$/i.test(trimmed) ? trimmed : void 0;
792
+ }
793
+ function getProviderErrorCode(err) {
794
+ if (typeof err !== "object" || err === null) return void 0;
795
+ const record = err;
796
+ return sanitizeExternalFailureCode(record.code) ?? sanitizeExternalFailureCode(record.type) ?? sanitizeExternalFailureCode(record.name);
797
+ }
798
+ function getProviderDeclineCode(err) {
799
+ if (typeof err !== "object" || err === null) return void 0;
800
+ return sanitizeExternalFailureCode(err.decline_code);
801
+ }
802
+ function isProviderDecline(err) {
803
+ if (typeof err !== "object" || err === null) return false;
804
+ const type = sanitizeExternalFailureCode(err.type)?.toLowerCase();
805
+ return type === "card_error" || getProviderDeclineCode(err) !== void 0;
806
+ }
807
+ function isPopupBlockedError(err) {
808
+ const message = getProviderErrorMessage(err)?.toLowerCase() ?? "";
809
+ const code = getProviderErrorCode(err)?.toLowerCase() ?? "";
810
+ const signal = `${code} ${message}`;
811
+ return signal.includes("popup") && signal.includes("block");
812
+ }
813
+
814
+ // src/direct-paypal-button.tsx
777
815
  import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
778
816
  var DEFAULT_BUTTON_HEIGHT = 45;
817
+ var DIRECT_PAYPAL_RECOVERY_MESSAGE = "We couldn't open PayPal. Try again or choose another payment method.";
818
+ var DIRECT_PAYPAL_RECOVERY_ACTION_STYLE = {
819
+ width: "100%",
820
+ minHeight: DEFAULT_BUTTON_HEIGHT,
821
+ margin: "0 0 0.5rem",
822
+ padding: "0.75rem 0.875rem",
823
+ border: "1px solid #2563eb",
824
+ borderRadius: 8,
825
+ background: "#eff6ff",
826
+ color: "#1d4ed8",
827
+ fontSize: "0.95rem",
828
+ fontWeight: 700,
829
+ cursor: "pointer"
830
+ };
831
+ function buildDirectPayPalRecoveryMessage(popupBlocked) {
832
+ return popupBlocked ? `${DIRECT_PAYPAL_RECOVERY_MESSAGE} Allow pop-ups for this site, then try again.` : DIRECT_PAYPAL_RECOVERY_MESSAGE;
833
+ }
779
834
  function DirectPayPalButton({
780
835
  sessionId,
781
836
  nonce,
@@ -789,6 +844,7 @@ function DirectPayPalButton({
789
844
  onComplete,
790
845
  onErrorChange,
791
846
  onDecline,
847
+ onTechnicalFailure,
792
848
  isProcessing = false,
793
849
  onLoadStateChange,
794
850
  onButtonClick,
@@ -798,7 +854,12 @@ function DirectPayPalButton({
798
854
  debug = false
799
855
  }) {
800
856
  const containerRef = useRef3(null);
857
+ const focusTargetRef = useRef3(null);
858
+ const pendingProviderFocusRef = useRef3(false);
801
859
  const [ready, setReady] = useState2(false);
860
+ const [renderGeneration, setRenderGeneration] = useState2(0);
861
+ const activeRenderGenerationRef = useRef3(0);
862
+ const [showRetryFocusTarget, setShowRetryFocusTarget] = useState2(false);
802
863
  const [failed, setFailed] = useState2(false);
803
864
  const [submitting, setSubmitting] = useState2(false);
804
865
  const baseUrl = useMemo2(() => billingApiUrl.replace(/\/+$/, ""), [billingApiUrl]);
@@ -811,6 +872,7 @@ function DirectPayPalButton({
811
872
  const onCompleteRef = useRef3(onComplete);
812
873
  const onErrorChangeRef = useRef3(onErrorChange);
813
874
  const onDeclineRef = useRef3(onDecline);
875
+ const onTechnicalFailureRef = useRef3(onTechnicalFailure);
814
876
  const onButtonClickRef = useRef3(onButtonClick);
815
877
  const onLoadStateChangeRef = useRef3(onLoadStateChange);
816
878
  const runBeforeButtonClickRef = useRef3(runBeforeButtonClick);
@@ -818,6 +880,11 @@ function DirectPayPalButton({
818
880
  const emailRef = useRef3(email);
819
881
  const nonceRef = useRef3(nonce);
820
882
  const beforeClickRef = useRef3(null);
883
+ const attemptGenerationRef = useRef3(0);
884
+ const attemptRef = useRef3(null);
885
+ const invalidatedAttemptGenerationRef = useRef3(null);
886
+ const attemptContextBySurfaceRef = useRef3(/* @__PURE__ */ new Map());
887
+ const approvalContextByTokenRef = useRef3(/* @__PURE__ */ new Map());
821
888
  useEffect4(() => {
822
889
  onTokenizedBodyRef.current = onTokenizedBody;
823
890
  }, [onTokenizedBody]);
@@ -830,6 +897,9 @@ function DirectPayPalButton({
830
897
  useEffect4(() => {
831
898
  onDeclineRef.current = onDecline;
832
899
  }, [onDecline]);
900
+ useEffect4(() => {
901
+ onTechnicalFailureRef.current = onTechnicalFailure;
902
+ }, [onTechnicalFailure]);
833
903
  useEffect4(() => {
834
904
  onButtonClickRef.current = onButtonClick;
835
905
  }, [onButtonClick]);
@@ -848,6 +918,129 @@ function DirectPayPalButton({
848
918
  useEffect4(() => {
849
919
  nonceRef.current = nonce;
850
920
  }, [nonce]);
921
+ useEffect4(() => {
922
+ activeRenderGenerationRef.current = renderGeneration;
923
+ }, [renderGeneration]);
924
+ useEffect4(() => {
925
+ if (showRetryFocusTarget) focusTargetRef.current?.focus();
926
+ }, [showRetryFocusTarget, renderGeneration]);
927
+ const focusRetryTarget = useCallback(() => {
928
+ window.setTimeout(() => {
929
+ focusTargetRef.current?.focus();
930
+ }, 0);
931
+ }, []);
932
+ const remountPayPalButtons = useCallback(() => {
933
+ setReady(false);
934
+ setRenderGeneration((current) => current + 1);
935
+ }, []);
936
+ const focusPayPalSurface = useCallback(() => {
937
+ setShowRetryFocusTarget(false);
938
+ const target = containerRef.current?.querySelector(
939
+ 'iframe, button, [tabindex]:not([tabindex="-1"])'
940
+ );
941
+ (target ?? containerRef.current)?.focus?.();
942
+ }, []);
943
+ useEffect4(() => {
944
+ if (!ready || !pendingProviderFocusRef.current) return;
945
+ pendingProviderFocusRef.current = false;
946
+ focusPayPalSurface();
947
+ }, [focusPayPalSurface, ready, renderGeneration]);
948
+ const notifyTechnicalFailure = useCallback((err, options) => {
949
+ const handler = onTechnicalFailureRef.current;
950
+ if (handler) {
951
+ handler("paypal", err, options);
952
+ return;
953
+ }
954
+ onErrorChangeRef.current?.(
955
+ buildDirectPayPalRecoveryMessage(options?.popupBlocked ?? isPopupBlockedError(err))
956
+ );
957
+ }, []);
958
+ const clearAttemptTimer = () => {
959
+ if (attemptRef.current?.timer) {
960
+ clearTimeout(attemptRef.current.timer);
961
+ attemptRef.current.timer = null;
962
+ }
963
+ };
964
+ const startAttempt = () => {
965
+ clearAttemptTimer();
966
+ const generation = attemptGenerationRef.current + 1;
967
+ attemptGenerationRef.current = generation;
968
+ invalidatedAttemptGenerationRef.current = null;
969
+ attemptRef.current = { generation, timer: null, yieldedControl: false };
970
+ setShowRetryFocusTarget(false);
971
+ return generation;
972
+ };
973
+ const finishAttempt = (generation) => {
974
+ if (generation !== void 0 && attemptRef.current?.generation !== generation) return false;
975
+ clearAttemptTimer();
976
+ attemptRef.current = null;
977
+ invalidatedAttemptGenerationRef.current = null;
978
+ return true;
979
+ };
980
+ const invalidateAttempt = (options) => {
981
+ const generation = options?.generation ?? attemptRef.current?.generation ?? attemptGenerationRef.current;
982
+ if (!options?.generation || attemptRef.current?.generation === generation) {
983
+ clearAttemptTimer();
984
+ attemptRef.current = null;
985
+ }
986
+ invalidatedAttemptGenerationRef.current = generation;
987
+ if (options?.showRetry) setShowRetryFocusTarget(true);
988
+ if (options?.remount) remountPayPalButtons();
989
+ if (options?.focus !== false) focusRetryTarget();
990
+ };
991
+ const isAttemptActive = (generation) => attemptRef.current?.generation === generation && invalidatedAttemptGenerationRef.current !== generation;
992
+ const armMissingCallbackRecovery = (attempt) => {
993
+ if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
994
+ clearAttemptTimer();
995
+ attempt.timer = setTimeout(() => {
996
+ if (attemptRef.current?.generation !== attempt.generation) return;
997
+ attemptRef.current = null;
998
+ invalidatedAttemptGenerationRef.current = attempt.generation;
999
+ notifyTechnicalFailure(
1000
+ new Error("External payment method returned without a terminal callback."),
1001
+ { code: "external_method_missing_terminal_callback" }
1002
+ );
1003
+ setShowRetryFocusTarget(true);
1004
+ remountPayPalButtons();
1005
+ focusRetryTarget();
1006
+ }, EXTERNAL_METHOD_CALLBACK_GRACE_MS);
1007
+ };
1008
+ const scheduleMissingCallbackRecovery = () => {
1009
+ const attempt = attemptRef.current;
1010
+ if (!attempt?.yieldedControl) return;
1011
+ armMissingCallbackRecovery(attempt);
1012
+ };
1013
+ const markAttemptYieldedControl = () => {
1014
+ const attempt = attemptRef.current;
1015
+ if (!attempt) return;
1016
+ attempt.yieldedControl = true;
1017
+ clearAttemptTimer();
1018
+ };
1019
+ const registerApprovalContext = (token) => {
1020
+ const context = beforeClickRef.current;
1021
+ if (context) {
1022
+ approvalContextByTokenRef.current.set(token, context);
1023
+ }
1024
+ return token;
1025
+ };
1026
+ useEffect4(() => {
1027
+ const handleVisibilityChange = () => {
1028
+ if (document.visibilityState === "visible") {
1029
+ scheduleMissingCallbackRecovery();
1030
+ } else {
1031
+ markAttemptYieldedControl();
1032
+ }
1033
+ };
1034
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1035
+ window.addEventListener("focus", scheduleMissingCallbackRecovery);
1036
+ window.addEventListener("blur", markAttemptYieldedControl);
1037
+ return () => {
1038
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1039
+ window.removeEventListener("focus", scheduleMissingCallbackRecovery);
1040
+ window.removeEventListener("blur", markAttemptYieldedControl);
1041
+ clearAttemptTimer();
1042
+ };
1043
+ }, []);
851
1044
  useEffect4(() => {
852
1045
  onLoadStateChangeRef.current?.(ready && !failed);
853
1046
  }, [ready, failed]);
@@ -984,13 +1177,13 @@ function DirectPayPalButton({
984
1177
  console.error("[FloPay] DirectPayPal load/render failure:", message);
985
1178
  };
986
1179
  setFailed(false);
987
- const dispatchTokenizedBody = async (body) => {
988
- const prepared = beforeClickRef.current;
1180
+ const dispatchTokenizedBody = async (body, prepared = beforeClickRef.current) => {
989
1181
  const effectiveSessionId = prepared?.sessionId ?? sessionId;
990
1182
  if (onTokenizedBodyRef.current) {
991
1183
  onTokenizedBodyRef.current(body, {
992
1184
  sessionId: effectiveSessionId,
993
- accountPatch: prepared?.accountPatch
1185
+ accountPatch: prepared?.accountPatch,
1186
+ nonce: prepared?.nonce
994
1187
  });
995
1188
  return;
996
1189
  }
@@ -1003,7 +1196,7 @@ function DirectPayPalButton({
1003
1196
  effectiveUserId,
1004
1197
  {
1005
1198
  sessionId: effectiveSessionId,
1006
- nonce: nonceRef.current,
1199
+ nonce: prepared?.nonce ?? nonceRef.current,
1007
1200
  tokenizedData: body,
1008
1201
  accountData: {
1009
1202
  userId: effectiveUserId,
@@ -1039,7 +1232,7 @@ function DirectPayPalButton({
1039
1232
  const effectiveSessionId = prepared?.sessionId ?? sessionId;
1040
1233
  const effectiveEmail = prepared?.accountPatch?.email ?? emailRef.current;
1041
1234
  const intentHeaders = { "Content-Type": "application/json" };
1042
- const currentNonce = nonceRef.current;
1235
+ const currentNonce = prepared?.nonce ?? nonceRef.current;
1043
1236
  if (currentNonce) intentHeaders["x-checkout-session-token"] = currentNonce;
1044
1237
  const response = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
1045
1238
  method: "POST",
@@ -1061,6 +1254,7 @@ function DirectPayPalButton({
1061
1254
  }
1062
1255
  return id;
1063
1256
  };
1257
+ const effectRenderGeneration = renderGeneration;
1064
1258
  appendDebug("loadScript:scheduled (deferred 1 tick)");
1065
1259
  let loadPromise = null;
1066
1260
  const startTimer = setTimeout(() => {
@@ -1093,10 +1287,16 @@ function DirectPayPalButton({
1093
1287
  return;
1094
1288
  }
1095
1289
  const handleApprove = async (data) => {
1290
+ if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
1291
+ const token = data.subscriptionID ?? data.orderID ?? "";
1292
+ const context = token ? approvalContextByTokenRef.current.get(token) : attemptContextBySurfaceRef.current.get(effectRenderGeneration);
1293
+ if (!context || !isAttemptActive(context.generation)) return;
1294
+ if (!finishAttempt(context.generation)) return;
1295
+ approvalContextByTokenRef.current.delete(token);
1296
+ attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1096
1297
  try {
1097
1298
  setSubmitting(true);
1098
1299
  onErrorChangeRef.current?.(null);
1099
- const token = data.subscriptionID ?? data.orderID ?? "";
1100
1300
  if (!token) {
1101
1301
  throw new FloPayError2(
1102
1302
  "PayPal did not return an approval token.",
@@ -1107,7 +1307,7 @@ function DirectPayPalButton({
1107
1307
  await dispatchTokenizedBody({
1108
1308
  id: token,
1109
1309
  isPaypal: true
1110
- });
1310
+ }, context);
1111
1311
  } catch (err) {
1112
1312
  forwardError(err instanceof Error ? err.message : "PayPal capture failed.");
1113
1313
  } finally {
@@ -1123,47 +1323,70 @@ function DirectPayPalButton({
1123
1323
  // matching the Stripe-rendered PayPal flow.
1124
1324
  onClick: async (_data, actions) => {
1125
1325
  const runner = runBeforeButtonClickRef.current;
1326
+ let prepared = {};
1126
1327
  if (runner) {
1127
1328
  try {
1128
1329
  const beforeClick = await runner("paypal");
1129
1330
  if (!beforeClick.proceed) {
1130
1331
  beforeClickRef.current = null;
1332
+ attemptContextBySurfaceRef.current.delete(effectRenderGeneration);
1131
1333
  await actions.reject();
1132
1334
  return;
1133
1335
  }
1134
- beforeClickRef.current = {
1336
+ prepared = {
1135
1337
  sessionId: beforeClick.sessionId,
1136
- accountPatch: beforeClick.accountPatch
1338
+ accountPatch: beforeClick.accountPatch,
1339
+ nonce: beforeClick.nonce
1137
1340
  };
1138
1341
  } catch (err) {
1139
1342
  appendDebug(`onClick:runBeforeButtonClick rejected msg=${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`);
1140
1343
  beforeClickRef.current = null;
1344
+ attemptContextBySurfaceRef.current.delete(effectRenderGeneration);
1141
1345
  await actions.reject();
1142
1346
  return;
1143
1347
  }
1144
- } else {
1145
- beforeClickRef.current = null;
1146
1348
  }
1147
1349
  onButtonClickRef.current?.("paypal");
1350
+ const generation = startAttempt();
1351
+ const context = {
1352
+ generation,
1353
+ surfaceKey: effectRenderGeneration,
1354
+ ...prepared
1355
+ };
1356
+ beforeClickRef.current = context;
1357
+ attemptContextBySurfaceRef.current.set(effectRenderGeneration, context);
1148
1358
  await actions.resolve();
1149
1359
  },
1150
1360
  // When the SDK is already holding an order/subscription id from a
1151
1361
  // prior backend round-trip (the `paypal_direct_required` retry
1152
1362
  // path), feed it straight to PayPal instead of creating a new one.
1153
1363
  // Otherwise fall back to the normal create-intent call.
1154
- createOrder: isSubscription ? void 0 : existingOrderId ? () => Promise.resolve(existingOrderId) : () => createPaypalIntent("Failed to create PayPal order."),
1155
- createSubscription: isSubscription ? existingOrderId ? () => Promise.resolve(existingOrderId) : () => createPaypalIntent("Failed to create PayPal subscription.") : void 0,
1364
+ createOrder: isSubscription ? void 0 : existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal order.").then(registerApprovalContext),
1365
+ createSubscription: isSubscription ? existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal subscription.").then(registerApprovalContext) : void 0,
1156
1366
  onApprove: handleApprove,
1157
1367
  onCancel: () => {
1368
+ const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
1369
+ if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1158
1370
  beforeClickRef.current = null;
1159
- onDeclineRef.current?.(buildDeclineEvent("paypal", "PayPal checkout was cancelled."));
1371
+ pendingProviderFocusRef.current = true;
1372
+ invalidateAttempt({ generation: context?.generation, remount: true, focus: false });
1160
1373
  },
1161
1374
  onError: (err) => {
1375
+ if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
1162
1376
  const message = err instanceof Error ? err.message : "PayPal failed to render.";
1163
1377
  appendDebug(`onError rendered=${rendered} msg=${message.slice(0, 120)}`);
1164
1378
  if (rendered) {
1165
- forwardError(message);
1166
- onDeclineRef.current?.(buildDeclineEvent("paypal", message));
1379
+ const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
1380
+ if (context && !isAttemptActive(context.generation)) return;
1381
+ if (!context && invalidatedAttemptGenerationRef.current === attemptGenerationRef.current) return;
1382
+ if (isZoidLifecycleMessage(message)) {
1383
+ finishAttempt(context?.generation);
1384
+ return;
1385
+ }
1386
+ if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1387
+ beforeClickRef.current = null;
1388
+ invalidateAttempt({ generation: context?.generation, remount: true, showRetry: true, focus: true });
1389
+ notifyTechnicalFailure(err, { code: "paypal_runtime_failed" });
1167
1390
  return;
1168
1391
  }
1169
1392
  markRenderFailed(message);
@@ -1263,7 +1486,7 @@ function DirectPayPalButton({
1263
1486
  });
1264
1487
  }
1265
1488
  };
1266
- }, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId]);
1489
+ }, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration]);
1267
1490
  const debugPanel = debug ? /* @__PURE__ */ jsxs3(
1268
1491
  "pre",
1269
1492
  {
@@ -1318,15 +1541,29 @@ ${debugLines.join("\n")}`
1318
1541
  {
1319
1542
  ref: containerRef,
1320
1543
  "data-testid": "flopay-direct-paypal-container",
1544
+ tabIndex: -1,
1545
+ "aria-label": "PayPal payment method",
1321
1546
  style: {
1322
1547
  minHeight: DEFAULT_BUTTON_HEIGHT,
1323
1548
  display: "flex",
1324
1549
  opacity: ready ? 1 : 0
1325
1550
  },
1326
1551
  "aria-busy": submitting || isProcessing
1327
- }
1552
+ },
1553
+ renderGeneration
1328
1554
  )
1329
- ] })
1555
+ ] }),
1556
+ showRetryFocusTarget && /* @__PURE__ */ jsx6(
1557
+ "button",
1558
+ {
1559
+ ref: focusTargetRef,
1560
+ type: "button",
1561
+ "data-testid": "flopay-direct-paypal-focus-target",
1562
+ onClick: focusPayPalSurface,
1563
+ style: DIRECT_PAYPAL_RECOVERY_ACTION_STYLE,
1564
+ children: "Try PayPal again"
1565
+ }
1566
+ )
1330
1567
  ] })
1331
1568
  );
1332
1569
  }
@@ -1410,6 +1647,19 @@ var BUTTONS_PANEL_HIDDEN_STYLE = {
1410
1647
  height: 0,
1411
1648
  overflow: "hidden"
1412
1649
  };
1650
+ var EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE = {
1651
+ width: "100%",
1652
+ minHeight: DEFAULT_BUTTONS_LAYOUT_BUTTON_HEIGHT,
1653
+ margin: "0 0 0.5rem",
1654
+ padding: "0.75rem 0.875rem",
1655
+ border: "1px solid #2563eb",
1656
+ borderRadius: 8,
1657
+ background: "#eff6ff",
1658
+ color: "#1d4ed8",
1659
+ fontSize: "0.95rem",
1660
+ fontWeight: 700,
1661
+ cursor: "pointer"
1662
+ };
1413
1663
  function getButtonMethodLabel(method) {
1414
1664
  switch (method) {
1415
1665
  case "paypal":
@@ -1422,6 +1672,118 @@ function getButtonMethodLabel(method) {
1422
1672
  return "Card";
1423
1673
  }
1424
1674
  }
1675
+ function checkoutButtonMethodFromProviderMethod(method) {
1676
+ if (method === "paypal") return "paypal";
1677
+ if (method === "apple_pay") return "apple_pay";
1678
+ return "google_pay";
1679
+ }
1680
+ function getExternalMethodDisplayName(method) {
1681
+ return method === "paypal" ? "PayPal" : getStripeMethodDisplayName(method);
1682
+ }
1683
+ function buildExternalMethodRecoveryMessage(method, popupBlocked) {
1684
+ const base = `We couldn't open ${getExternalMethodDisplayName(method)}. Try again or choose another payment method.`;
1685
+ return popupBlocked ? `${base} Allow pop-ups for this site, then try again.` : base;
1686
+ }
1687
+ function useExternalAttemptReconciliation(onMissingTerminal) {
1688
+ const generationRef = useRef4(0);
1689
+ const invalidatedGenerationRef = useRef4(null);
1690
+ const attemptRef = useRef4(null);
1691
+ const clearAttemptTimer = useCallback2((targetAttempt = attemptRef.current) => {
1692
+ const attempt = targetAttempt;
1693
+ if (attempt?.timer) {
1694
+ clearTimeout(attempt.timer);
1695
+ attempt.timer = null;
1696
+ }
1697
+ }, []);
1698
+ const armAttemptTimer = useCallback2((attempt, delayMs) => {
1699
+ if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
1700
+ clearAttemptTimer(attempt);
1701
+ attempt.timer = setTimeout(() => {
1702
+ attempt.timer = null;
1703
+ if (attemptRef.current?.generation !== attempt.generation) return;
1704
+ attemptRef.current = null;
1705
+ invalidatedGenerationRef.current = attempt.generation;
1706
+ onMissingTerminal(
1707
+ attempt.method,
1708
+ new Error("External payment method returned without a terminal callback."),
1709
+ { code: "external_method_missing_terminal_callback" }
1710
+ );
1711
+ }, delayMs);
1712
+ }, [clearAttemptTimer, onMissingTerminal]);
1713
+ const armRecoveryTimer = useCallback2((attempt) => {
1714
+ if (!attempt.yieldedControl) return;
1715
+ armAttemptTimer(attempt, EXTERNAL_METHOD_CALLBACK_GRACE_MS);
1716
+ }, [armAttemptTimer]);
1717
+ const startAttempt = useCallback2((method) => {
1718
+ clearAttemptTimer();
1719
+ const generation = generationRef.current + 1;
1720
+ generationRef.current = generation;
1721
+ invalidatedGenerationRef.current = null;
1722
+ const attempt = { generation, method, timer: null, yieldedControl: false };
1723
+ attemptRef.current = attempt;
1724
+ return generation;
1725
+ }, [clearAttemptTimer]);
1726
+ const finishAttempt = useCallback2((generation) => {
1727
+ const attempt = attemptRef.current;
1728
+ if (typeof generation === "number" && attempt?.generation !== generation) return false;
1729
+ clearAttemptTimer(attempt);
1730
+ attemptRef.current = null;
1731
+ if (typeof generation !== "number" || invalidatedGenerationRef.current === generation) {
1732
+ invalidatedGenerationRef.current = null;
1733
+ }
1734
+ return true;
1735
+ }, [clearAttemptTimer]);
1736
+ const invalidateAttempt = useCallback2((generation) => {
1737
+ const attempt = attemptRef.current;
1738
+ const targetGeneration = generation ?? attempt?.generation ?? generationRef.current;
1739
+ if (!generation || attempt?.generation === generation) {
1740
+ clearAttemptTimer(attempt);
1741
+ attemptRef.current = null;
1742
+ }
1743
+ invalidatedGenerationRef.current = targetGeneration;
1744
+ }, [clearAttemptTimer]);
1745
+ const isAttemptInvalidated = useCallback2(
1746
+ (generation) => invalidatedGenerationRef.current === (generation ?? generationRef.current),
1747
+ []
1748
+ );
1749
+ const isAttemptCurrent = useCallback2(
1750
+ (generation) => attemptRef.current?.generation === generation && invalidatedGenerationRef.current !== generation,
1751
+ []
1752
+ );
1753
+ const scheduleRecoveryIfReturned = useCallback2(() => {
1754
+ const attempt = attemptRef.current;
1755
+ if (!attempt) return;
1756
+ armRecoveryTimer(attempt);
1757
+ }, [armRecoveryTimer]);
1758
+ const markAttemptYieldedControl = useCallback2(() => {
1759
+ const attempt = attemptRef.current;
1760
+ if (!attempt) return;
1761
+ attempt.yieldedControl = true;
1762
+ clearAttemptTimer(attempt);
1763
+ }, [clearAttemptTimer]);
1764
+ useEffect5(() => {
1765
+ const handleVisibilityChange = () => {
1766
+ if (document.visibilityState === "visible") {
1767
+ scheduleRecoveryIfReturned();
1768
+ } else {
1769
+ markAttemptYieldedControl();
1770
+ }
1771
+ };
1772
+ const handleBlur = () => {
1773
+ markAttemptYieldedControl();
1774
+ };
1775
+ document.addEventListener("visibilitychange", handleVisibilityChange);
1776
+ window.addEventListener("blur", handleBlur);
1777
+ window.addEventListener("focus", scheduleRecoveryIfReturned);
1778
+ return () => {
1779
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
1780
+ window.removeEventListener("blur", handleBlur);
1781
+ window.removeEventListener("focus", scheduleRecoveryIfReturned);
1782
+ clearAttemptTimer();
1783
+ };
1784
+ }, [clearAttemptTimer, markAttemptYieldedControl, scheduleRecoveryIfReturned]);
1785
+ return { startAttempt, finishAttempt, invalidateAttempt, isAttemptInvalidated, isAttemptCurrent };
1786
+ }
1425
1787
  function malformedPostcodeMessage(country) {
1426
1788
  const example = getPostalCodeExample(country);
1427
1789
  return `Enter a valid ${getPostalCodeLabel(country)}${example ? ` (e.g. ${example})` : ""}`;
@@ -1516,6 +1878,7 @@ function PayPalButtonInner({
1516
1878
  isProcessing = false,
1517
1879
  onButtonClick,
1518
1880
  onDecline,
1881
+ onTechnicalFailure,
1519
1882
  runBeforeButtonClick,
1520
1883
  onLoadStateChange,
1521
1884
  placeholderBorderRadius
@@ -1528,8 +1891,55 @@ function PayPalButtonInner({
1528
1891
  }, [loadState, onLoadStateChange]);
1529
1892
  const [submitting, setSubmitting] = useState3(false);
1530
1893
  const paypalResumeAttempted = useRef4(false);
1531
- const beforeClickRef = useRef4(null);
1894
+ const focusTargetRef = useRef4(null);
1895
+ const recoveryActionRef = useRef4(null);
1896
+ const [surfaceKey, setSurfaceKey] = useState3(0);
1897
+ const [showRecoveryAction, setShowRecoveryAction] = useState3(false);
1898
+ const pendingProviderFocusRef = useRef4(false);
1899
+ const attemptContextBySurfaceRef = useRef4(/* @__PURE__ */ new Map());
1532
1900
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
1901
+ const {
1902
+ startAttempt,
1903
+ finishAttempt,
1904
+ invalidateAttempt,
1905
+ isAttemptInvalidated,
1906
+ isAttemptCurrent
1907
+ } = useExternalAttemptReconciliation((method, err, options) => {
1908
+ onTechnicalFailure?.(method, err, {
1909
+ ...options,
1910
+ popupBlocked: options?.popupBlocked ?? isPopupBlockedError(err)
1911
+ });
1912
+ setShowRecoveryAction(true);
1913
+ setSurfaceKey((key) => key + 1);
1914
+ });
1915
+ useEffect5(() => {
1916
+ if (showRecoveryAction) recoveryActionRef.current?.focus();
1917
+ }, [showRecoveryAction, surfaceKey]);
1918
+ const resetSurface = useCallback2(() => {
1919
+ setSurfaceKey((key) => key + 1);
1920
+ }, []);
1921
+ const recoverTechnicalFailure = useCallback2((err, code, generation) => {
1922
+ invalidateAttempt(generation);
1923
+ onTechnicalFailure?.("paypal", err, { code, popupBlocked: isPopupBlockedError(err) });
1924
+ setShowRecoveryAction(true);
1925
+ resetSurface();
1926
+ }, [invalidateAttempt, onTechnicalFailure, resetSurface]);
1927
+ const focusProviderSurface = useCallback2(() => {
1928
+ window.setTimeout(() => {
1929
+ const target = focusTargetRef.current?.querySelector("iframe");
1930
+ (target ?? focusTargetRef.current)?.focus();
1931
+ }, 0);
1932
+ }, []);
1933
+ useEffect5(() => {
1934
+ if (!pendingProviderFocusRef.current) return;
1935
+ pendingProviderFocusRef.current = false;
1936
+ focusProviderSurface();
1937
+ }, [focusProviderSurface, surfaceKey]);
1938
+ const handleRecoveryActionClick = useCallback2(() => {
1939
+ setShowRecoveryAction(false);
1940
+ onErrorChange?.(null);
1941
+ focusProviderSurface();
1942
+ }, [focusProviderSurface, onErrorChange]);
1533
1943
  useEffect5(() => {
1534
1944
  if (!stripe || paypalResumeAttempted.current) return;
1535
1945
  const params = new URLSearchParams(window.location.search);
@@ -1596,29 +2006,40 @@ function PayPalButtonInner({
1596
2006
  }
1597
2007
  })();
1598
2008
  }, [stripe, onTokenizedBody, onErrorChange, onDecline]);
1599
- const handlePayPalClick = useCallback(async (event) => {
2009
+ const handlePayPalClick = useCallback2(async (event) => {
1600
2010
  if (isProcessing || submitting) {
1601
2011
  event.reject();
1602
2012
  return;
1603
2013
  }
1604
2014
  const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick("paypal") : { proceed: true };
1605
2015
  if (!beforeClick.proceed) {
1606
- beforeClickRef.current = null;
2016
+ attemptContextBySurfaceRef.current.delete(surfaceKey);
1607
2017
  event.reject();
1608
2018
  return;
1609
2019
  }
1610
- beforeClickRef.current = {
2020
+ const generation = startAttempt("paypal");
2021
+ attemptContextBySurfaceRef.current.set(surfaceKey, {
2022
+ generation,
1611
2023
  accountPatch: beforeClick.accountPatch,
1612
2024
  sessionId: beforeClick.sessionId,
1613
2025
  nonce: beforeClick.nonce
1614
- };
2026
+ });
2027
+ setShowRecoveryAction(false);
1615
2028
  onButtonClick?.("paypal");
1616
2029
  event.resolve();
1617
- }, [isProcessing, onButtonClick, runBeforeButtonClick, submitting]);
1618
- const handlePayPalConfirm = useCallback(async (event) => {
2030
+ }, [attemptContextBySurfaceRef, isProcessing, onButtonClick, runBeforeButtonClick, startAttempt, submitting, surfaceKey]);
2031
+ const handlePayPalConfirm = useCallback2(async (event) => {
1619
2032
  if (!stripe || !elements) return;
1620
- let prepared = beforeClickRef.current;
1621
- beforeClickRef.current = null;
2033
+ const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
2034
+ if (attemptContext && !isAttemptCurrent(attemptContext.generation)) {
2035
+ attemptContextBySurfaceRef.current.delete(surfaceKey);
2036
+ return;
2037
+ }
2038
+ if (!attemptContext && isAttemptInvalidated()) return;
2039
+ if (attemptContext && !finishAttempt(attemptContext.generation)) return;
2040
+ if (!attemptContext) finishAttempt();
2041
+ let prepared = attemptContext ?? null;
2042
+ attemptContextBySurfaceRef.current.delete(surfaceKey);
1622
2043
  if (!prepared && runBeforeButtonClick) {
1623
2044
  const beforeClick = await runBeforeButtonClick("paypal");
1624
2045
  if (!beforeClick.proceed) {
@@ -1643,9 +2064,8 @@ function PayPalButtonInner({
1643
2064
  const createPM = stripe.createPaymentMethod;
1644
2065
  const { error: pmError, paymentMethod } = await createPM({ type: "paypal" });
1645
2066
  if (pmError) {
1646
- const message = pmError.message ?? "PayPal payment failed.";
1647
- onErrorChange?.(message);
1648
- event.paymentFailed({ reason: "fail", message });
2067
+ recoverTechnicalFailure(pmError, "stripe_paypal_create_payment_method_failed", attemptContext?.generation);
2068
+ event.paymentFailed({ reason: "fail" });
1649
2069
  return;
1650
2070
  }
1651
2071
  const intentHeaders = { "Content-Type": "application/json" };
@@ -1704,11 +2124,16 @@ function PayPalButtonInner({
1704
2124
  } catch {
1705
2125
  }
1706
2126
  }
1707
- const message = confirmError.message ?? "PayPal payment failed.";
1708
- onErrorChange?.(message);
1709
- onDecline?.(buildDeclineEvent("paypal", message, {
1710
- code: confirmError.code
1711
- }));
2127
+ if (isProviderDecline(confirmError)) {
2128
+ const message = confirmError.message ?? "Your payment was declined.";
2129
+ onErrorChange?.(message);
2130
+ onDecline?.(buildDeclineEvent("paypal", message, {
2131
+ code: getProviderErrorCode(confirmError),
2132
+ declineCode: getProviderDeclineCode(confirmError)
2133
+ }));
2134
+ } else {
2135
+ recoverTechnicalFailure(confirmError, "stripe_paypal_confirm_failed", attemptContext?.generation);
2136
+ }
1712
2137
  return;
1713
2138
  }
1714
2139
  if (typeof window !== "undefined") {
@@ -1730,11 +2155,11 @@ function PayPalButtonInner({
1730
2155
  });
1731
2156
  return;
1732
2157
  } catch (err) {
1733
- onErrorChange?.(err instanceof Error ? err.message : "PayPal payment failed. Please try again.");
2158
+ recoverTechnicalFailure(err, "stripe_paypal_failed", attemptContext?.generation);
1734
2159
  } finally {
1735
2160
  setSubmitting(false);
1736
2161
  }
1737
- }, [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]);
2162
+ }, [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]);
1738
2163
  return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1739
2164
  /* @__PURE__ */ jsx7(
1740
2165
  ExpressCheckoutReadySwap,
@@ -1742,29 +2167,57 @@ function PayPalButtonInner({
1742
2167
  state: loadState,
1743
2168
  placeholderTestId: "flopay-paypal-placeholder",
1744
2169
  borderRadius: placeholderBorderRadius,
1745
- children: /* @__PURE__ */ jsx7(
1746
- ExpressCheckoutElement,
2170
+ children: /* @__PURE__ */ jsxs4(
2171
+ "div",
1747
2172
  {
1748
- onReady: (event) => setLoadState(resolveExpressCheckoutLoadState(event, ["paypal"])),
1749
- onLoadError: () => setLoadState("load_error"),
1750
- onClick: handlePayPalClick,
1751
- onConfirm: handlePayPalConfirm,
1752
- onCancel: () => {
1753
- beforeClickRef.current = null;
1754
- onDecline?.(buildDeclineEvent("paypal", "PayPal checkout was cancelled."));
1755
- },
1756
- options: {
1757
- buttonType: { paypal: "paypal" },
1758
- billingAddressRequired: false,
1759
- phoneNumberRequired: false,
1760
- shippingAddressRequired: false,
1761
- paymentMethods: {
1762
- applePay: "never",
1763
- googlePay: "never",
1764
- paypal: "auto",
1765
- link: "never"
1766
- }
1767
- }
2173
+ ref: focusTargetRef,
2174
+ tabIndex: -1,
2175
+ "data-testid": "flopay-paypal-focus-target",
2176
+ "aria-label": "PayPal payment method",
2177
+ style: { borderRadius: 8, outlineOffset: 4 },
2178
+ children: [
2179
+ showRecoveryAction && /* @__PURE__ */ jsx7(
2180
+ "button",
2181
+ {
2182
+ ref: recoveryActionRef,
2183
+ type: "button",
2184
+ "data-testid": "flopay-paypal-retry-button",
2185
+ onClick: handleRecoveryActionClick,
2186
+ style: EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE,
2187
+ children: "Try PayPal again"
2188
+ }
2189
+ ),
2190
+ /* @__PURE__ */ jsx7(
2191
+ ExpressCheckoutElement,
2192
+ {
2193
+ onReady: (event) => setLoadState(resolveExpressCheckoutLoadState(event, ["paypal"])),
2194
+ onLoadError: () => setLoadState("load_error"),
2195
+ onClick: handlePayPalClick,
2196
+ onConfirm: handlePayPalConfirm,
2197
+ onCancel: () => {
2198
+ const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
2199
+ attemptContextBySurfaceRef.current.delete(surfaceKey);
2200
+ invalidateAttempt(attemptContext?.generation);
2201
+ setShowRecoveryAction(false);
2202
+ pendingProviderFocusRef.current = true;
2203
+ resetSurface();
2204
+ },
2205
+ options: {
2206
+ buttonType: { paypal: "paypal" },
2207
+ billingAddressRequired: false,
2208
+ phoneNumberRequired: false,
2209
+ shippingAddressRequired: false,
2210
+ paymentMethods: {
2211
+ applePay: "never",
2212
+ googlePay: "never",
2213
+ paypal: "auto",
2214
+ link: "never"
2215
+ }
2216
+ }
2217
+ },
2218
+ surfaceKey
2219
+ )
2220
+ ]
1768
2221
  }
1769
2222
  )
1770
2223
  }
@@ -1782,6 +2235,7 @@ function WalletButtonInner({
1782
2235
  onErrorChange,
1783
2236
  onButtonClick,
1784
2237
  onDecline,
2238
+ onTechnicalFailure,
1785
2239
  runBeforeButtonClick,
1786
2240
  onLoadStateChange,
1787
2241
  placeholderBorderRadius
@@ -1794,15 +2248,74 @@ function WalletButtonInner({
1794
2248
  }, [loadState, onLoadStateChange]);
1795
2249
  const [submitting, setSubmitting] = useState3(false);
1796
2250
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
1797
- const lastWalletMethodRef = useRef4("card");
1798
- const beforeClickRef = useRef4(null);
1799
- const handleWalletConfirm = useCallback(
2251
+ const focusTargetRef = useRef4(null);
2252
+ const recoveryActionRef = useRef4(null);
2253
+ const [surfaceKey, setSurfaceKey] = useState3(0);
2254
+ const [showRecoveryAction, setShowRecoveryAction] = useState3(false);
2255
+ const [recoveryActionMethod, setRecoveryActionMethod] = useState3("google_pay");
2256
+ const pendingProviderFocusRef = useRef4(false);
2257
+ const lastWalletProviderMethodRef = useRef4("google_pay");
2258
+ const lastWalletMethodRef = useRef4("google_pay");
2259
+ const attemptContextBySurfaceRef = useRef4(/* @__PURE__ */ new Map());
2260
+ const {
2261
+ startAttempt,
2262
+ finishAttempt,
2263
+ invalidateAttempt,
2264
+ isAttemptInvalidated,
2265
+ isAttemptCurrent
2266
+ } = useExternalAttemptReconciliation((method, err, options) => {
2267
+ onTechnicalFailure?.(method, err, {
2268
+ ...options,
2269
+ popupBlocked: options?.popupBlocked ?? isPopupBlockedError(err)
2270
+ });
2271
+ setRecoveryActionMethod(method);
2272
+ setShowRecoveryAction(true);
2273
+ setSurfaceKey((key) => key + 1);
2274
+ });
2275
+ useEffect5(() => {
2276
+ if (showRecoveryAction) recoveryActionRef.current?.focus();
2277
+ }, [showRecoveryAction, surfaceKey]);
2278
+ const resetSurface = useCallback2(() => {
2279
+ setSurfaceKey((key) => key + 1);
2280
+ }, []);
2281
+ const recoverTechnicalFailure = useCallback2((method, err, code, generation) => {
2282
+ invalidateAttempt(generation);
2283
+ onTechnicalFailure?.(method, err, { code, popupBlocked: isPopupBlockedError(err) });
2284
+ setRecoveryActionMethod(method);
2285
+ setShowRecoveryAction(true);
2286
+ resetSurface();
2287
+ }, [invalidateAttempt, onTechnicalFailure, resetSurface]);
2288
+ const focusProviderSurface = useCallback2(() => {
2289
+ window.setTimeout(() => {
2290
+ const target = focusTargetRef.current?.querySelector("iframe");
2291
+ (target ?? focusTargetRef.current)?.focus();
2292
+ }, 0);
2293
+ }, []);
2294
+ useEffect5(() => {
2295
+ if (!pendingProviderFocusRef.current) return;
2296
+ pendingProviderFocusRef.current = false;
2297
+ focusProviderSurface();
2298
+ }, [focusProviderSurface, surfaceKey]);
2299
+ const handleRecoveryActionClick = useCallback2(() => {
2300
+ setShowRecoveryAction(false);
2301
+ onErrorChange?.(null);
2302
+ focusProviderSurface();
2303
+ }, [focusProviderSurface, onErrorChange]);
2304
+ const handleWalletConfirm = useCallback2(
1800
2305
  async (event) => {
1801
2306
  if (!stripe || !elements) return;
1802
- const walletType = event.expressPaymentType;
1803
- let prepared = beforeClickRef.current;
1804
- beforeClickRef.current = null;
1805
- const buttonMethod = walletType === "apple_pay" ? "apple_pay" : "google_pay";
2307
+ const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
2308
+ if (attemptContext && !isAttemptCurrent(attemptContext.generation)) {
2309
+ attemptContextBySurfaceRef.current.delete(surfaceKey);
2310
+ return;
2311
+ }
2312
+ if (!attemptContext && isAttemptInvalidated()) return;
2313
+ if (attemptContext && !finishAttempt(attemptContext.generation)) return;
2314
+ if (!attemptContext) finishAttempt();
2315
+ const walletType = event.expressPaymentType ?? attemptContext?.method ?? lastWalletProviderMethodRef.current;
2316
+ let prepared = attemptContext ?? null;
2317
+ attemptContextBySurfaceRef.current.delete(surfaceKey);
2318
+ const buttonMethod = checkoutButtonMethodFromProviderMethod(walletType);
1806
2319
  if (!prepared && runBeforeButtonClick) {
1807
2320
  const beforeClick = await runBeforeButtonClick(buttonMethod);
1808
2321
  if (!beforeClick.proceed) {
@@ -1824,12 +2337,12 @@ function WalletButtonInner({
1824
2337
  onErrorChange?.(null);
1825
2338
  const { error: submitError } = await elements.submit();
1826
2339
  if (submitError) {
1827
- onErrorChange?.(submitError.message ?? "Wallet payment failed.");
2340
+ recoverTechnicalFailure(walletType, submitError, "stripe_wallet_submit_failed", attemptContext?.generation);
1828
2341
  return;
1829
2342
  }
1830
2343
  const { error: pmError, paymentMethod } = await stripe.createPaymentMethod({ elements });
1831
2344
  if (pmError || !paymentMethod) {
1832
- onErrorChange?.(pmError?.message ?? "Failed to create payment method.");
2345
+ recoverTechnicalFailure(walletType, pmError ?? new Error("Failed to create payment method."), "stripe_wallet_create_payment_method_failed", attemptContext?.generation);
1833
2346
  return;
1834
2347
  }
1835
2348
  if (!effectiveSessionId || !effectiveEmail) {
@@ -1862,11 +2375,16 @@ function WalletButtonInner({
1862
2375
  if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
1863
2376
  const { error: confirmError, intentId } = isSetupIntentClientSecret2(intentClientSecret) ? await stripe.confirmCardSetup(intentClientSecret, { payment_method: paymentMethod.id }).then((r) => ({ error: r.error, intentId: r.setupIntent?.id })) : await stripe.confirmCardPayment(intentClientSecret, { payment_method: paymentMethod.id }).then((r) => ({ error: r.error, intentId: r.paymentIntent?.id }));
1864
2377
  if (confirmError) {
1865
- const message = confirmError.message ?? "Wallet payment failed.";
1866
- onErrorChange?.(message);
1867
- onDecline?.(buildDeclineEvent(method, message, {
1868
- code: confirmError.code
1869
- }));
2378
+ if (isProviderDecline(confirmError)) {
2379
+ const message = confirmError.message ?? "Your payment was declined.";
2380
+ onErrorChange?.(message);
2381
+ onDecline?.(buildDeclineEvent(method, message, {
2382
+ code: getProviderErrorCode(confirmError),
2383
+ declineCode: getProviderDeclineCode(confirmError)
2384
+ }));
2385
+ } else {
2386
+ recoverTechnicalFailure(walletType, confirmError, "stripe_wallet_confirm_failed", attemptContext?.generation);
2387
+ }
1870
2388
  return;
1871
2389
  }
1872
2390
  onTokenizedBody({
@@ -1879,12 +2397,12 @@ function WalletButtonInner({
1879
2397
  nonce: effectiveNonce
1880
2398
  });
1881
2399
  } catch (err) {
1882
- onErrorChange?.(err instanceof Error ? err.message : "Wallet payment failed. Please try again.");
2400
+ recoverTechnicalFailure(walletType, err, "stripe_wallet_failed", attemptContext?.generation);
1883
2401
  } finally {
1884
2402
  setSubmitting(false);
1885
2403
  }
1886
2404
  },
1887
- [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]
2405
+ [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]
1888
2406
  );
1889
2407
  const expressMethodMap = useMemo3(() => {
1890
2408
  const allKeys = ["applePay", "googlePay", "paypal", "link", "amazonPay", "klarna"];
@@ -1911,41 +2429,78 @@ function WalletButtonInner({
1911
2429
  state: loadState,
1912
2430
  placeholderTestId: "flopay-wallet-placeholder",
1913
2431
  borderRadius: placeholderBorderRadius,
1914
- children: /* @__PURE__ */ jsx7(
1915
- ExpressCheckoutElement,
2432
+ children: /* @__PURE__ */ jsxs4(
2433
+ "div",
1916
2434
  {
1917
- onReady: (event) => {
1918
- setLoadState(resolveExpressCheckoutLoadState(event, availableMethodKeys));
1919
- },
1920
- onLoadError: (_event) => {
1921
- setLoadState("load_error");
1922
- },
1923
- onClick: async (event) => {
1924
- lastWalletMethodRef.current = event.expressPaymentType === "apple_pay" ? "apple_pay" : "google_pay";
1925
- const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick(lastWalletMethodRef.current) : { proceed: true };
1926
- if (!beforeClick.proceed) {
1927
- beforeClickRef.current = null;
1928
- event.reject();
1929
- return;
1930
- }
1931
- beforeClickRef.current = {
1932
- accountPatch: beforeClick.accountPatch,
1933
- sessionId: beforeClick.sessionId,
1934
- nonce: beforeClick.nonce
1935
- };
1936
- onButtonClick?.(lastWalletMethodRef.current);
1937
- event.resolve();
1938
- },
1939
- onConfirm: handleWalletConfirm,
1940
- onCancel: () => {
1941
- beforeClickRef.current = null;
1942
- onDecline?.(buildDeclineEvent(lastWalletMethodRef.current, "Wallet checkout was cancelled."));
1943
- },
1944
- options: {
1945
- buttonType: { applePay: "plain", googlePay: "plain" },
1946
- paymentMethods: expressMethodMap,
1947
- layout: { maxColumns: 1, overflow: "never" }
1948
- }
2435
+ ref: focusTargetRef,
2436
+ tabIndex: -1,
2437
+ "data-testid": "flopay-wallet-focus-target",
2438
+ "aria-label": "Wallet payment methods",
2439
+ style: { borderRadius: 8, outlineOffset: 4 },
2440
+ children: [
2441
+ showRecoveryAction && /* @__PURE__ */ jsxs4(
2442
+ "button",
2443
+ {
2444
+ ref: recoveryActionRef,
2445
+ type: "button",
2446
+ "data-testid": "flopay-wallet-retry-button",
2447
+ onClick: handleRecoveryActionClick,
2448
+ style: EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE,
2449
+ children: [
2450
+ "Try ",
2451
+ getExternalMethodDisplayName(recoveryActionMethod),
2452
+ " again"
2453
+ ]
2454
+ }
2455
+ ),
2456
+ /* @__PURE__ */ jsx7(
2457
+ ExpressCheckoutElement,
2458
+ {
2459
+ onReady: (event) => {
2460
+ setLoadState(resolveExpressCheckoutLoadState(event, availableMethodKeys));
2461
+ },
2462
+ onLoadError: (_event) => {
2463
+ setLoadState("load_error");
2464
+ },
2465
+ onClick: async (event) => {
2466
+ lastWalletProviderMethodRef.current = event.expressPaymentType;
2467
+ lastWalletMethodRef.current = checkoutButtonMethodFromProviderMethod(event.expressPaymentType);
2468
+ const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick(lastWalletMethodRef.current) : { proceed: true };
2469
+ if (!beforeClick.proceed) {
2470
+ attemptContextBySurfaceRef.current.delete(surfaceKey);
2471
+ event.reject();
2472
+ return;
2473
+ }
2474
+ const generation = startAttempt(event.expressPaymentType);
2475
+ attemptContextBySurfaceRef.current.set(surfaceKey, {
2476
+ generation,
2477
+ method: event.expressPaymentType,
2478
+ accountPatch: beforeClick.accountPatch,
2479
+ sessionId: beforeClick.sessionId,
2480
+ nonce: beforeClick.nonce
2481
+ });
2482
+ setShowRecoveryAction(false);
2483
+ onButtonClick?.(lastWalletMethodRef.current);
2484
+ event.resolve();
2485
+ },
2486
+ onConfirm: handleWalletConfirm,
2487
+ onCancel: () => {
2488
+ const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
2489
+ attemptContextBySurfaceRef.current.delete(surfaceKey);
2490
+ invalidateAttempt(attemptContext?.generation);
2491
+ setShowRecoveryAction(false);
2492
+ pendingProviderFocusRef.current = true;
2493
+ resetSurface();
2494
+ },
2495
+ options: {
2496
+ buttonType: { applePay: "plain", googlePay: "plain" },
2497
+ paymentMethods: expressMethodMap,
2498
+ layout: { maxColumns: 1, overflow: "never" }
2499
+ }
2500
+ },
2501
+ surfaceKey
2502
+ )
2503
+ ]
1949
2504
  }
1950
2505
  )
1951
2506
  }
@@ -2109,7 +2664,7 @@ function StripeMethodInlineForm({
2109
2664
  const [isMethodComplete, setIsMethodComplete] = useState3(false);
2110
2665
  const [loadState, setLoadState] = useState3("loading");
2111
2666
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
2112
- const handlePay = useCallback(async () => {
2667
+ const handlePay = useCallback2(async () => {
2113
2668
  if (!stripe || !elements || isProcessing || submittingRef.current || !isMethodComplete) return;
2114
2669
  submittingRef.current = true;
2115
2670
  setSubmitting(true);
@@ -2350,7 +2905,7 @@ function StripePaymentElementInner({
2350
2905
  onLoadStateChange?.("loading");
2351
2906
  }
2352
2907
  }, [paymentElementMethods.length, stripeInstance, onLoadStateChange]);
2353
- const handleAutoConfirm = useCallback(async (method) => {
2908
+ const handleAutoConfirm = useCallback2(async (method) => {
2354
2909
  if (!stripeInstance) return;
2355
2910
  if (submittingRef.current || isProcessing) return;
2356
2911
  submittingRef.current = true;
@@ -2491,7 +3046,7 @@ function StripePaymentElementInner({
2491
3046
  ]);
2492
3047
  const [localExpandedMethod, setLocalExpandedMethod] = useState3(null);
2493
3048
  const activeExpandedMethod = onExpandApm ? expandedApmMethod ?? null : localExpandedMethod;
2494
- const handleMethodClick = useCallback((method) => {
3049
+ const handleMethodClick = useCallback2((method) => {
2495
3050
  if (submittingRef.current || isProcessing) return;
2496
3051
  if (activeExpandedMethod && activeExpandedMethod !== method) return;
2497
3052
  if (needsStripeMethodExplicitConfirm(method)) {
@@ -2696,20 +3251,20 @@ function SplitCardFormInner({
2696
3251
  const [expandedApmMethod, setExpandedApmMethod] = useState3(null);
2697
3252
  const showCardForm = viewState === "expanding" || viewState === "card";
2698
3253
  const TRANSITION_MS = 280;
2699
- const expandToCard = useCallback(() => {
3254
+ const expandToCard = useCallback2(() => {
2700
3255
  setViewState("expanding");
2701
3256
  setTimeout(() => setViewState("card"), TRANSITION_MS);
2702
3257
  }, []);
2703
- const collapseToButtons = useCallback(() => {
3258
+ const collapseToButtons = useCallback2(() => {
2704
3259
  setViewState("collapsing");
2705
3260
  setTimeout(() => setViewState("buttons"), TRANSITION_MS);
2706
3261
  }, []);
2707
- const expandToApm = useCallback((method) => {
3262
+ const expandToApm = useCallback2((method) => {
2708
3263
  setExpandedApmMethod(method);
2709
3264
  setViewState("apm-expanding");
2710
3265
  setTimeout(() => setViewState("apm-form"), TRANSITION_MS);
2711
3266
  }, []);
2712
- const collapseFromApm = useCallback(() => {
3267
+ const collapseFromApm = useCallback2(() => {
2713
3268
  setViewState("apm-collapsing");
2714
3269
  setTimeout(() => {
2715
3270
  setViewState("buttons");
@@ -2834,7 +3389,7 @@ function SplitCardFormInner({
2834
3389
  setupFutureUsage: "off_session",
2835
3390
  ...stripeAppearanceProp
2836
3391
  }), [amountInCents, currency, stripeAppearanceProp]);
2837
- const updateError = useCallback(
3392
+ const updateError = useCallback2(
2838
3393
  (err) => {
2839
3394
  setError(err);
2840
3395
  onErrorChange?.(err);
@@ -2850,12 +3405,36 @@ function SplitCardFormInner({
2850
3405
  updateError(null);
2851
3406
  }
2852
3407
  }, [viewState, updateError]);
2853
- const emitDecline = useCallback(
3408
+ const emitDecline = useCallback2(
2854
3409
  (method, input, overrides) => {
2855
3410
  onDecline?.(buildDeclineEvent(method, input, overrides));
2856
3411
  },
2857
3412
  [onDecline]
2858
3413
  );
3414
+ const recoverExternalMethodTechnicalFailure = useCallback2(
3415
+ (method, err, options) => {
3416
+ const popupBlocked = options?.popupBlocked ?? isPopupBlockedError(err);
3417
+ const message = buildExternalMethodRecoveryMessage(method, popupBlocked);
3418
+ const providerCode = sanitizeExternalFailureCode(options?.code) ?? getProviderErrorCode(err) ?? "external_payment_method_failed";
3419
+ const floPayError = new FloPayError3(message, "api_error", {
3420
+ code: "external_payment_method_failed",
3421
+ param: method
3422
+ });
3423
+ setOverlayStatus(null);
3424
+ if (layout === "buttons") {
3425
+ setViewState("buttons");
3426
+ setExpandedApmMethod(null);
3427
+ }
3428
+ updateError(message);
3429
+ onError?.(floPayError);
3430
+ console.error("[FloPay] External payment method failed:", {
3431
+ method,
3432
+ code: providerCode,
3433
+ popupBlocked
3434
+ });
3435
+ },
3436
+ [layout, onError, updateError]
3437
+ );
2859
3438
  useEffect5(() => {
2860
3439
  if (!vaultActive || !sessionId) {
2861
3440
  setVaultMount(null);
@@ -2921,7 +3500,7 @@ function SplitCardFormInner({
2921
3500
  baseUrl
2922
3501
  };
2923
3502
  const vaultCompletedRef = useRef4(false);
2924
- const buildVaultAccountSnapshot = useCallback(() => {
3503
+ const buildVaultAccountSnapshot = useCallback2(() => {
2925
3504
  const { resolvedAccount: resolvedAccount2, avsConfig: avsConfig2, fullName: fullName2, avsCheckProp: avsCheckProp2 } = vaultOutcomeRef.current;
2926
3505
  const cc = selectedCountryRef.current || resolvedAccount2.country || "US";
2927
3506
  const stateVisible = avsConfig2 ? isAVSFieldVisible(avsConfig2.state, cc) : false;
@@ -3111,14 +3690,14 @@ function SplitCardFormInner({
3111
3690
  `[FloPay] ${stale.join(" / ")} ${stale.length === 1 ? "is" : "are"} deprecated: the Apple Pay / Google Pay surface is now driven by \`gateways.stripe.enabledPaymentMethods\` on the session response. Remove the legacy prop(s) to silence this warning.`
3112
3691
  );
3113
3692
  }, [hasEnabledMethods, showApplePay, showGooglePay]);
3114
- const handleNameChange = useCallback((value) => {
3693
+ const handleNameChange = useCallback2((value) => {
3115
3694
  setFullName(value);
3116
3695
  onFullNameChange?.(value);
3117
3696
  const parts = value.trim().split(/\s+/);
3118
3697
  onFirstNameChange?.(parts[0] ?? "");
3119
3698
  onLastNameChange?.(parts.length > 1 ? parts.slice(1).join(" ") : "");
3120
3699
  }, [onFullNameChange, onFirstNameChange, onLastNameChange]);
3121
- const applyInlineSessionPatch = useCallback(
3700
+ const applyInlineSessionPatch = useCallback2(
3122
3701
  (patch, method) => {
3123
3702
  if (!checkout.applyInlineSessionPatch) {
3124
3703
  return Promise.resolve({ error: null, sessionId, nonce });
@@ -3140,7 +3719,7 @@ function SplitCardFormInner({
3140
3719
  },
3141
3720
  [checkout.applyInlineSessionPatch, nonce, onError, sessionId, updateError]
3142
3721
  );
3143
- const runBeforeButtonClick = useCallback(async (method) => {
3722
+ const runBeforeButtonClick = useCallback2(async (method) => {
3144
3723
  if (!onBeforeButtonClick) return { proceed: true };
3145
3724
  try {
3146
3725
  const result = await onBeforeButtonClick({
@@ -3177,7 +3756,7 @@ function SplitCardFormInner({
3177
3756
  return { proceed: false };
3178
3757
  }
3179
3758
  }, [applyInlineSessionPatch, checkout.inlineSessionDraft, onBeforeButtonClick, onError, sessionId, updateError]);
3180
- const processPaymentInternal = useCallback(
3759
+ const processPaymentInternal = useCallback2(
3181
3760
  async (tokenizedBody, overrides) => {
3182
3761
  if (processingRef.current) return;
3183
3762
  processingRef.current = true;
@@ -3388,7 +3967,7 @@ function SplitCardFormInner({
3388
3967
  },
3389
3968
  [baseUrl, sessionId, nonce, resolvedAccount, fullName, chv, flopay, paypalFlopay, onComplete, onError, updateError, emitDecline]
3390
3969
  );
3391
- const dispatchTokenizedBody = useCallback(
3970
+ const dispatchTokenizedBody = useCallback2(
3392
3971
  (tokenizedBody, overrides) => {
3393
3972
  if (onTokenizedBody) {
3394
3973
  onTokenizedBody(tokenizedBody);
@@ -3504,7 +4083,7 @@ function SplitCardFormInner({
3504
4083
  }, persistedOverrides);
3505
4084
  })();
3506
4085
  }, [sessionId, dispatchTokenizedBody, flopay, updateError, emitDecline]);
3507
- const handleSubmit = useCallback(
4086
+ const handleSubmit = useCallback2(
3508
4087
  async (e) => {
3509
4088
  e.preventDefault();
3510
4089
  if (!flopay || !elements || isSubmitting || processingRef.current) return;
@@ -4312,6 +4891,7 @@ function SplitCardFormInner({
4312
4891
  onComplete,
4313
4892
  onErrorChange: updateError,
4314
4893
  onDecline,
4894
+ onTechnicalFailure: recoverExternalMethodTechnicalFailure,
4315
4895
  onButtonClick,
4316
4896
  runBeforeButtonClick,
4317
4897
  isProcessing: isSubmitting,
@@ -4335,6 +4915,7 @@ function SplitCardFormInner({
4335
4915
  isProcessing: isSubmitting,
4336
4916
  onButtonClick,
4337
4917
  onDecline,
4918
+ onTechnicalFailure: recoverExternalMethodTechnicalFailure,
4338
4919
  runBeforeButtonClick,
4339
4920
  onLoadStateChange: setPaypalLoadState,
4340
4921
  placeholderBorderRadius: buttonBorderRadius
@@ -4352,6 +4933,7 @@ function SplitCardFormInner({
4352
4933
  onErrorChange: updateError,
4353
4934
  onButtonClick,
4354
4935
  onDecline,
4936
+ onTechnicalFailure: recoverExternalMethodTechnicalFailure,
4355
4937
  runBeforeButtonClick,
4356
4938
  onLoadStateChange: setWalletLoadState,
4357
4939
  placeholderBorderRadius: buttonBorderRadius
@@ -4707,6 +5289,7 @@ function SplitCardFormInner({
4707
5289
  onComplete,
4708
5290
  onErrorChange: updateError,
4709
5291
  onDecline,
5292
+ onTechnicalFailure: recoverExternalMethodTechnicalFailure,
4710
5293
  onButtonClick,
4711
5294
  runBeforeButtonClick,
4712
5295
  isProcessing: isSubmitting,
@@ -4730,6 +5313,7 @@ function SplitCardFormInner({
4730
5313
  isProcessing: isSubmitting,
4731
5314
  onButtonClick,
4732
5315
  onDecline,
5316
+ onTechnicalFailure: recoverExternalMethodTechnicalFailure,
4733
5317
  runBeforeButtonClick,
4734
5318
  onLoadStateChange: setPaypalLoadState,
4735
5319
  placeholderBorderRadius: buttonBorderRadius
@@ -4747,6 +5331,7 @@ function SplitCardFormInner({
4747
5331
  onErrorChange: updateError,
4748
5332
  onButtonClick,
4749
5333
  onDecline,
5334
+ onTechnicalFailure: recoverExternalMethodTechnicalFailure,
4750
5335
  runBeforeButtonClick,
4751
5336
  onLoadStateChange: setWalletLoadState,
4752
5337
  placeholderBorderRadius: buttonBorderRadius
@@ -5702,13 +6287,13 @@ function FloPayCheckout({
5702
6287
  useEffect6(() => {
5703
6288
  setModeError(initialErrorMessage);
5704
6289
  }, [initialErrorMessage]);
5705
- const emitDecline = useCallback2(
6290
+ const emitDecline = useCallback3(
5706
6291
  (method, input, overrides) => {
5707
6292
  onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
5708
6293
  },
5709
6294
  []
5710
6295
  );
5711
- const runSavedPaymentFlow = useCallback2(
6296
+ const runSavedPaymentFlow = useCallback3(
5712
6297
  async (sess, options) => {
5713
6298
  setModeError(null);
5714
6299
  setModeOverlayError(null);
@@ -6029,7 +6614,7 @@ function FloPayCheckout({
6029
6614
  }
6030
6615
  return { sid: sid ?? "", result: realResult };
6031
6616
  }
6032
- const bootstrapInlineSession = useCallback2(
6617
+ const bootstrapInlineSession = useCallback3(
6033
6618
  async (patch) => {
6034
6619
  const baseParams = createSessionParamsRef.current;
6035
6620
  if (!baseParams) {
@@ -6090,7 +6675,7 @@ function FloPayCheckout({
6090
6675
  },
6091
6676
  [locale, resolvedBillingUrl]
6092
6677
  );
6093
- const handleInlineSessionPatch = useCallback2(async (patch) => {
6678
+ const handleInlineSessionPatch = useCallback3(async (patch) => {
6094
6679
  if (!hasInlineSessionPatchData(patch) || cardBootstrapPending) {
6095
6680
  return {
6096
6681
  sessionId: resolvedSessionId,
@@ -6279,7 +6864,7 @@ function FloPayCheckout({
6279
6864
  nonceProp,
6280
6865
  runSavedPaymentFlow
6281
6866
  ]);
6282
- const handleConfirmCheckout = useCallback2(async () => {
6867
+ const handleConfirmCheckout = useCallback3(async () => {
6283
6868
  if (confirmProcessing || !session) return;
6284
6869
  setConfirmProcessing(true);
6285
6870
  setModeError(null);
@@ -6846,7 +7431,7 @@ function InterimButtonsView({
6846
7431
  // src/checkout-form.tsx
6847
7432
  import { PaymentAPI as PaymentAPI5 } from "@flopay/js";
6848
7433
  import { FloPayError as FloPayError6 } from "@flopay/shared";
6849
- import { forwardRef as forwardRef2, useCallback as useCallback3, useEffect as useEffect7, useImperativeHandle as useImperativeHandle2, useState as useState5 } from "react";
7434
+ import { forwardRef as forwardRef2, useCallback as useCallback4, useEffect as useEffect7, useImperativeHandle as useImperativeHandle2, useState as useState5 } from "react";
6850
7435
  import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
6851
7436
  var WALLET_RESUME_KEY = "flopay_wallet_resume";
6852
7437
  var CheckoutForm = forwardRef2(
@@ -6888,20 +7473,20 @@ function CheckoutFormInner({
6888
7473
  const isSubmitting = externalProcessing ?? processing;
6889
7474
  const isSelfContained = !onTokenizedBody;
6890
7475
  const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
6891
- const updateError = useCallback3(
7476
+ const updateError = useCallback4(
6892
7477
  (err) => {
6893
7478
  setError(err);
6894
7479
  onErrorChange?.(err);
6895
7480
  },
6896
7481
  [onErrorChange]
6897
7482
  );
6898
- const emitDecline = useCallback3(
7483
+ const emitDecline = useCallback4(
6899
7484
  (input, overrides) => {
6900
7485
  onDecline?.(buildDeclineEvent("card", input, overrides));
6901
7486
  },
6902
7487
  [onDecline]
6903
7488
  );
6904
- const processPaymentInternal = useCallback3(
7489
+ const processPaymentInternal = useCallback4(
6905
7490
  async (tokenizedBody, completionPaymentMethodId) => {
6906
7491
  setProcessing(true);
6907
7492
  updateError(null);
@@ -7013,7 +7598,7 @@ function CheckoutFormInner({
7013
7598
  },
7014
7599
  [baseUrl, sessionId, nonce, userId, email, firstName, lastName, chv, flopay, paypalFlopay, onComplete, onError, onDecline, updateError, emitDecline]
7015
7600
  );
7016
- const dispatchTokenizedBody = useCallback3(
7601
+ const dispatchTokenizedBody = useCallback4(
7017
7602
  (tokenizedBody) => {
7018
7603
  if (onTokenizedBody) {
7019
7604
  onTokenizedBody(tokenizedBody);
@@ -7068,7 +7653,7 @@ function CheckoutFormInner({
7068
7653
  localStorage.removeItem(WALLET_RESUME_KEY);
7069
7654
  }
7070
7655
  }, [sessionId, dispatchTokenizedBody]);
7071
- const handleSubmit = useCallback3(
7656
+ const handleSubmit = useCallback4(
7072
7657
  async (e) => {
7073
7658
  e.preventDefault();
7074
7659
  if (!flopay || !elements || isSubmitting) return;
@@ -7191,7 +7776,7 @@ function CheckoutFormInner({
7191
7776
  // src/paypal-button.tsx
7192
7777
  import { PaymentAPI as PaymentAPI6 } from "@flopay/js";
7193
7778
  import { FloPayError as FloPayError7 } from "@flopay/shared";
7194
- import { useCallback as useCallback4, useEffect as useEffect8, useRef as useRef6, useState as useState6 } from "react";
7779
+ import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef6, useState as useState6 } from "react";
7195
7780
  import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
7196
7781
  function PayPalButton({
7197
7782
  sessionId,
@@ -7214,7 +7799,7 @@ function PayPalButton({
7214
7799
  const [submitting, setSubmitting] = useState6(false);
7215
7800
  const paypalResumeAttempted = useRef6(false);
7216
7801
  const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
7217
- const processPaymentInternal = useCallback4(
7802
+ const processPaymentInternal = useCallback5(
7218
7803
  async (tokenizedBody) => {
7219
7804
  try {
7220
7805
  const api = new PaymentAPI6(baseUrl);
@@ -7242,7 +7827,7 @@ function PayPalButton({
7242
7827
  },
7243
7828
  [baseUrl, sessionId, nonce, userId, email, firstName, lastName, chv, onComplete, onErrorChange]
7244
7829
  );
7245
- const dispatchTokenizedBody = useCallback4(
7830
+ const dispatchTokenizedBody = useCallback5(
7246
7831
  (body) => {
7247
7832
  if (onTokenizedBody) {
7248
7833
  onTokenizedBody(body);
@@ -7300,7 +7885,7 @@ function PayPalButton({
7300
7885
  }
7301
7886
  })();
7302
7887
  }, [flopay, dispatchTokenizedBody, onErrorChange]);
7303
- const handlePayPalConfirm = useCallback4(async () => {
7888
+ const handlePayPalConfirm = useCallback5(async () => {
7304
7889
  if (!flopay || !elements) return;
7305
7890
  try {
7306
7891
  setSubmitting(true);
@@ -7380,7 +7965,7 @@ function PayPalButton({
7380
7965
  }
7381
7966
 
7382
7967
  // src/automatic-payment-button.tsx
7383
- import { useCallback as useCallback5, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef7, useState as useState7 } from "react";
7968
+ import { useCallback as useCallback6, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef7, useState as useState7 } from "react";
7384
7969
  import { PaymentAPI as PaymentAPI7 } from "@flopay/js";
7385
7970
  import { FloPayError as FloPayError8, resolveBillingApiUrl as resolveBillingApiUrl4, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme3, resolveTheme as resolveTheme3 } from "@flopay/shared";
7386
7971
  import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -7538,13 +8123,13 @@ function FloPayAutomaticPaymentButton({
7538
8123
  window.removeEventListener("keydown", handleKeyDown);
7539
8124
  };
7540
8125
  }, [fallbackSession]);
7541
- const emitDecline = useCallback5((error, method = DEFAULT_SAVED_PAYMENT_DECLINE_METHOD) => {
8126
+ const emitDecline = useCallback6((error, method = DEFAULT_SAVED_PAYMENT_DECLINE_METHOD) => {
7542
8127
  onDeclineRef.current?.(buildDeclineEvent(method, error, {
7543
8128
  code: error.code,
7544
8129
  declineCode: error.declineCode
7545
8130
  }));
7546
8131
  }, []);
7547
- const showSuccess = useCallback5(async (event) => {
8132
+ const showSuccess = useCallback6(async (event) => {
7548
8133
  if (!isMountedRef.current) return;
7549
8134
  setOverlayError(null);
7550
8135
  setOverlayStatus("success");
@@ -7552,7 +8137,7 @@ function FloPayAutomaticPaymentButton({
7552
8137
  if (!isMountedRef.current) return;
7553
8138
  onSuccessRef.current?.(event);
7554
8139
  }, []);
7555
- const showError = useCallback5(async (error, options) => {
8140
+ const showError = useCallback6(async (error, options) => {
7556
8141
  if (!isMountedRef.current) return;
7557
8142
  onErrorRef.current?.(error);
7558
8143
  if (options?.emitDecline) {
@@ -7562,7 +8147,7 @@ function FloPayAutomaticPaymentButton({
7562
8147
  setOverlayStatus("error");
7563
8148
  await sleep2(PROCESSING_OVERLAY_ERROR_DELAY_MS);
7564
8149
  }, [emitDecline]);
7565
- const processResolvedSession = useCallback5(async (apiResult, resolvedSessionId, options) => {
8150
+ const processResolvedSession = useCallback6(async (apiResult, resolvedSessionId, options) => {
7566
8151
  const session = apiResult.data.session ?? null;
7567
8152
  if (!session) {
7568
8153
  throw new FloPayError8("No session data returned", "api_error");
@@ -7718,7 +8303,7 @@ function FloPayAutomaticPaymentButton({
7718
8303
  showError,
7719
8304
  showSuccess
7720
8305
  ]);
7721
- const handleButtonClick = useCallback5(async (event) => {
8306
+ const handleButtonClick = useCallback6(async (event) => {
7722
8307
  buttonProps.onClick?.(event);
7723
8308
  if (event.defaultPrevented || disabled || isProcessing) {
7724
8309
  return;
@@ -7800,7 +8385,7 @@ function FloPayAutomaticPaymentButton({
7800
8385
  showError,
7801
8386
  showSuccess
7802
8387
  ]);
7803
- const handleFallbackComplete = useCallback5((result) => {
8388
+ const handleFallbackComplete = useCallback6((result) => {
7804
8389
  const activeFallback = fallbackSessionRef.current;
7805
8390
  setFallbackSession(null);
7806
8391
  onSuccessRef.current?.({
@@ -7810,10 +8395,10 @@ function FloPayAutomaticPaymentButton({
7810
8395
  autoCompleted: false
7811
8396
  });
7812
8397
  }, []);
7813
- const handleFallbackError = useCallback5((error) => {
8398
+ const handleFallbackError = useCallback6((error) => {
7814
8399
  onErrorRef.current?.(error);
7815
8400
  }, []);
7816
- const handleFallbackDecline = useCallback5((decline) => {
8401
+ const handleFallbackDecline = useCallback6((decline) => {
7817
8402
  onDeclineRef.current?.(decline);
7818
8403
  }, []);
7819
8404
  const themeBundle = useMemo5(() => resolveTheme3(theme), [theme]);