@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/README.md +38 -0
- package/dist/index.cjs +692 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.mjs +738 -153
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -802,8 +802,63 @@ var import_react8 = require("react");
|
|
|
802
802
|
var import_paypal_js = require("@paypal/paypal-js");
|
|
803
803
|
var import_js = require("@flopay/js");
|
|
804
804
|
var import_shared4 = require("@flopay/shared");
|
|
805
|
+
|
|
806
|
+
// src/external-method-recovery.ts
|
|
807
|
+
var EXTERNAL_METHOD_CALLBACK_GRACE_MS = 600;
|
|
808
|
+
function getProviderErrorMessage(err) {
|
|
809
|
+
if (err instanceof Error) return err.message;
|
|
810
|
+
if (typeof err === "object" && err !== null) {
|
|
811
|
+
const message = err.message;
|
|
812
|
+
return typeof message === "string" ? message : void 0;
|
|
813
|
+
}
|
|
814
|
+
return typeof err === "string" ? err : void 0;
|
|
815
|
+
}
|
|
816
|
+
function sanitizeExternalFailureCode(value) {
|
|
817
|
+
if (typeof value !== "string") return void 0;
|
|
818
|
+
const trimmed = value.trim();
|
|
819
|
+
return /^[a-z0-9_.-]{1,64}$/i.test(trimmed) ? trimmed : void 0;
|
|
820
|
+
}
|
|
821
|
+
function getProviderErrorCode(err) {
|
|
822
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
823
|
+
const record = err;
|
|
824
|
+
return sanitizeExternalFailureCode(record.code) ?? sanitizeExternalFailureCode(record.type) ?? sanitizeExternalFailureCode(record.name);
|
|
825
|
+
}
|
|
826
|
+
function getProviderDeclineCode(err) {
|
|
827
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
828
|
+
return sanitizeExternalFailureCode(err.decline_code);
|
|
829
|
+
}
|
|
830
|
+
function isProviderDecline(err) {
|
|
831
|
+
if (typeof err !== "object" || err === null) return false;
|
|
832
|
+
const type = sanitizeExternalFailureCode(err.type)?.toLowerCase();
|
|
833
|
+
return type === "card_error" || getProviderDeclineCode(err) !== void 0;
|
|
834
|
+
}
|
|
835
|
+
function isPopupBlockedError(err) {
|
|
836
|
+
const message = getProviderErrorMessage(err)?.toLowerCase() ?? "";
|
|
837
|
+
const code = getProviderErrorCode(err)?.toLowerCase() ?? "";
|
|
838
|
+
const signal = `${code} ${message}`;
|
|
839
|
+
return signal.includes("popup") && signal.includes("block");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/direct-paypal-button.tsx
|
|
805
843
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
806
844
|
var DEFAULT_BUTTON_HEIGHT = 45;
|
|
845
|
+
var DIRECT_PAYPAL_RECOVERY_MESSAGE = "We couldn't open PayPal. Try again or choose another payment method.";
|
|
846
|
+
var DIRECT_PAYPAL_RECOVERY_ACTION_STYLE = {
|
|
847
|
+
width: "100%",
|
|
848
|
+
minHeight: DEFAULT_BUTTON_HEIGHT,
|
|
849
|
+
margin: "0 0 0.5rem",
|
|
850
|
+
padding: "0.75rem 0.875rem",
|
|
851
|
+
border: "1px solid #2563eb",
|
|
852
|
+
borderRadius: 8,
|
|
853
|
+
background: "#eff6ff",
|
|
854
|
+
color: "#1d4ed8",
|
|
855
|
+
fontSize: "0.95rem",
|
|
856
|
+
fontWeight: 700,
|
|
857
|
+
cursor: "pointer"
|
|
858
|
+
};
|
|
859
|
+
function buildDirectPayPalRecoveryMessage(popupBlocked) {
|
|
860
|
+
return popupBlocked ? `${DIRECT_PAYPAL_RECOVERY_MESSAGE} Allow pop-ups for this site, then try again.` : DIRECT_PAYPAL_RECOVERY_MESSAGE;
|
|
861
|
+
}
|
|
807
862
|
function DirectPayPalButton({
|
|
808
863
|
sessionId,
|
|
809
864
|
nonce,
|
|
@@ -817,6 +872,7 @@ function DirectPayPalButton({
|
|
|
817
872
|
onComplete,
|
|
818
873
|
onErrorChange,
|
|
819
874
|
onDecline,
|
|
875
|
+
onTechnicalFailure,
|
|
820
876
|
isProcessing = false,
|
|
821
877
|
onLoadStateChange,
|
|
822
878
|
onButtonClick,
|
|
@@ -826,7 +882,12 @@ function DirectPayPalButton({
|
|
|
826
882
|
debug = false
|
|
827
883
|
}) {
|
|
828
884
|
const containerRef = (0, import_react8.useRef)(null);
|
|
885
|
+
const focusTargetRef = (0, import_react8.useRef)(null);
|
|
886
|
+
const pendingProviderFocusRef = (0, import_react8.useRef)(false);
|
|
829
887
|
const [ready, setReady] = (0, import_react8.useState)(false);
|
|
888
|
+
const [renderGeneration, setRenderGeneration] = (0, import_react8.useState)(0);
|
|
889
|
+
const activeRenderGenerationRef = (0, import_react8.useRef)(0);
|
|
890
|
+
const [showRetryFocusTarget, setShowRetryFocusTarget] = (0, import_react8.useState)(false);
|
|
830
891
|
const [failed, setFailed] = (0, import_react8.useState)(false);
|
|
831
892
|
const [submitting, setSubmitting] = (0, import_react8.useState)(false);
|
|
832
893
|
const baseUrl = (0, import_react8.useMemo)(() => billingApiUrl.replace(/\/+$/, ""), [billingApiUrl]);
|
|
@@ -839,6 +900,7 @@ function DirectPayPalButton({
|
|
|
839
900
|
const onCompleteRef = (0, import_react8.useRef)(onComplete);
|
|
840
901
|
const onErrorChangeRef = (0, import_react8.useRef)(onErrorChange);
|
|
841
902
|
const onDeclineRef = (0, import_react8.useRef)(onDecline);
|
|
903
|
+
const onTechnicalFailureRef = (0, import_react8.useRef)(onTechnicalFailure);
|
|
842
904
|
const onButtonClickRef = (0, import_react8.useRef)(onButtonClick);
|
|
843
905
|
const onLoadStateChangeRef = (0, import_react8.useRef)(onLoadStateChange);
|
|
844
906
|
const runBeforeButtonClickRef = (0, import_react8.useRef)(runBeforeButtonClick);
|
|
@@ -846,6 +908,11 @@ function DirectPayPalButton({
|
|
|
846
908
|
const emailRef = (0, import_react8.useRef)(email);
|
|
847
909
|
const nonceRef = (0, import_react8.useRef)(nonce);
|
|
848
910
|
const beforeClickRef = (0, import_react8.useRef)(null);
|
|
911
|
+
const attemptGenerationRef = (0, import_react8.useRef)(0);
|
|
912
|
+
const attemptRef = (0, import_react8.useRef)(null);
|
|
913
|
+
const invalidatedAttemptGenerationRef = (0, import_react8.useRef)(null);
|
|
914
|
+
const attemptContextBySurfaceRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
|
|
915
|
+
const approvalContextByTokenRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
|
|
849
916
|
(0, import_react8.useEffect)(() => {
|
|
850
917
|
onTokenizedBodyRef.current = onTokenizedBody;
|
|
851
918
|
}, [onTokenizedBody]);
|
|
@@ -858,6 +925,9 @@ function DirectPayPalButton({
|
|
|
858
925
|
(0, import_react8.useEffect)(() => {
|
|
859
926
|
onDeclineRef.current = onDecline;
|
|
860
927
|
}, [onDecline]);
|
|
928
|
+
(0, import_react8.useEffect)(() => {
|
|
929
|
+
onTechnicalFailureRef.current = onTechnicalFailure;
|
|
930
|
+
}, [onTechnicalFailure]);
|
|
861
931
|
(0, import_react8.useEffect)(() => {
|
|
862
932
|
onButtonClickRef.current = onButtonClick;
|
|
863
933
|
}, [onButtonClick]);
|
|
@@ -876,6 +946,129 @@ function DirectPayPalButton({
|
|
|
876
946
|
(0, import_react8.useEffect)(() => {
|
|
877
947
|
nonceRef.current = nonce;
|
|
878
948
|
}, [nonce]);
|
|
949
|
+
(0, import_react8.useEffect)(() => {
|
|
950
|
+
activeRenderGenerationRef.current = renderGeneration;
|
|
951
|
+
}, [renderGeneration]);
|
|
952
|
+
(0, import_react8.useEffect)(() => {
|
|
953
|
+
if (showRetryFocusTarget) focusTargetRef.current?.focus();
|
|
954
|
+
}, [showRetryFocusTarget, renderGeneration]);
|
|
955
|
+
const focusRetryTarget = (0, import_react8.useCallback)(() => {
|
|
956
|
+
window.setTimeout(() => {
|
|
957
|
+
focusTargetRef.current?.focus();
|
|
958
|
+
}, 0);
|
|
959
|
+
}, []);
|
|
960
|
+
const remountPayPalButtons = (0, import_react8.useCallback)(() => {
|
|
961
|
+
setReady(false);
|
|
962
|
+
setRenderGeneration((current) => current + 1);
|
|
963
|
+
}, []);
|
|
964
|
+
const focusPayPalSurface = (0, import_react8.useCallback)(() => {
|
|
965
|
+
setShowRetryFocusTarget(false);
|
|
966
|
+
const target = containerRef.current?.querySelector(
|
|
967
|
+
'iframe, button, [tabindex]:not([tabindex="-1"])'
|
|
968
|
+
);
|
|
969
|
+
(target ?? containerRef.current)?.focus?.();
|
|
970
|
+
}, []);
|
|
971
|
+
(0, import_react8.useEffect)(() => {
|
|
972
|
+
if (!ready || !pendingProviderFocusRef.current) return;
|
|
973
|
+
pendingProviderFocusRef.current = false;
|
|
974
|
+
focusPayPalSurface();
|
|
975
|
+
}, [focusPayPalSurface, ready, renderGeneration]);
|
|
976
|
+
const notifyTechnicalFailure = (0, import_react8.useCallback)((err, options) => {
|
|
977
|
+
const handler = onTechnicalFailureRef.current;
|
|
978
|
+
if (handler) {
|
|
979
|
+
handler("paypal", err, options);
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
onErrorChangeRef.current?.(
|
|
983
|
+
buildDirectPayPalRecoveryMessage(options?.popupBlocked ?? isPopupBlockedError(err))
|
|
984
|
+
);
|
|
985
|
+
}, []);
|
|
986
|
+
const clearAttemptTimer = () => {
|
|
987
|
+
if (attemptRef.current?.timer) {
|
|
988
|
+
clearTimeout(attemptRef.current.timer);
|
|
989
|
+
attemptRef.current.timer = null;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const startAttempt = () => {
|
|
993
|
+
clearAttemptTimer();
|
|
994
|
+
const generation = attemptGenerationRef.current + 1;
|
|
995
|
+
attemptGenerationRef.current = generation;
|
|
996
|
+
invalidatedAttemptGenerationRef.current = null;
|
|
997
|
+
attemptRef.current = { generation, timer: null, yieldedControl: false };
|
|
998
|
+
setShowRetryFocusTarget(false);
|
|
999
|
+
return generation;
|
|
1000
|
+
};
|
|
1001
|
+
const finishAttempt = (generation) => {
|
|
1002
|
+
if (generation !== void 0 && attemptRef.current?.generation !== generation) return false;
|
|
1003
|
+
clearAttemptTimer();
|
|
1004
|
+
attemptRef.current = null;
|
|
1005
|
+
invalidatedAttemptGenerationRef.current = null;
|
|
1006
|
+
return true;
|
|
1007
|
+
};
|
|
1008
|
+
const invalidateAttempt = (options) => {
|
|
1009
|
+
const generation = options?.generation ?? attemptRef.current?.generation ?? attemptGenerationRef.current;
|
|
1010
|
+
if (!options?.generation || attemptRef.current?.generation === generation) {
|
|
1011
|
+
clearAttemptTimer();
|
|
1012
|
+
attemptRef.current = null;
|
|
1013
|
+
}
|
|
1014
|
+
invalidatedAttemptGenerationRef.current = generation;
|
|
1015
|
+
if (options?.showRetry) setShowRetryFocusTarget(true);
|
|
1016
|
+
if (options?.remount) remountPayPalButtons();
|
|
1017
|
+
if (options?.focus !== false) focusRetryTarget();
|
|
1018
|
+
};
|
|
1019
|
+
const isAttemptActive = (generation) => attemptRef.current?.generation === generation && invalidatedAttemptGenerationRef.current !== generation;
|
|
1020
|
+
const armMissingCallbackRecovery = (attempt) => {
|
|
1021
|
+
if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
|
|
1022
|
+
clearAttemptTimer();
|
|
1023
|
+
attempt.timer = setTimeout(() => {
|
|
1024
|
+
if (attemptRef.current?.generation !== attempt.generation) return;
|
|
1025
|
+
attemptRef.current = null;
|
|
1026
|
+
invalidatedAttemptGenerationRef.current = attempt.generation;
|
|
1027
|
+
notifyTechnicalFailure(
|
|
1028
|
+
new Error("External payment method returned without a terminal callback."),
|
|
1029
|
+
{ code: "external_method_missing_terminal_callback" }
|
|
1030
|
+
);
|
|
1031
|
+
setShowRetryFocusTarget(true);
|
|
1032
|
+
remountPayPalButtons();
|
|
1033
|
+
focusRetryTarget();
|
|
1034
|
+
}, EXTERNAL_METHOD_CALLBACK_GRACE_MS);
|
|
1035
|
+
};
|
|
1036
|
+
const scheduleMissingCallbackRecovery = () => {
|
|
1037
|
+
const attempt = attemptRef.current;
|
|
1038
|
+
if (!attempt?.yieldedControl) return;
|
|
1039
|
+
armMissingCallbackRecovery(attempt);
|
|
1040
|
+
};
|
|
1041
|
+
const markAttemptYieldedControl = () => {
|
|
1042
|
+
const attempt = attemptRef.current;
|
|
1043
|
+
if (!attempt) return;
|
|
1044
|
+
attempt.yieldedControl = true;
|
|
1045
|
+
clearAttemptTimer();
|
|
1046
|
+
};
|
|
1047
|
+
const registerApprovalContext = (token) => {
|
|
1048
|
+
const context = beforeClickRef.current;
|
|
1049
|
+
if (context) {
|
|
1050
|
+
approvalContextByTokenRef.current.set(token, context);
|
|
1051
|
+
}
|
|
1052
|
+
return token;
|
|
1053
|
+
};
|
|
1054
|
+
(0, import_react8.useEffect)(() => {
|
|
1055
|
+
const handleVisibilityChange = () => {
|
|
1056
|
+
if (document.visibilityState === "visible") {
|
|
1057
|
+
scheduleMissingCallbackRecovery();
|
|
1058
|
+
} else {
|
|
1059
|
+
markAttemptYieldedControl();
|
|
1060
|
+
}
|
|
1061
|
+
};
|
|
1062
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
1063
|
+
window.addEventListener("focus", scheduleMissingCallbackRecovery);
|
|
1064
|
+
window.addEventListener("blur", markAttemptYieldedControl);
|
|
1065
|
+
return () => {
|
|
1066
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
1067
|
+
window.removeEventListener("focus", scheduleMissingCallbackRecovery);
|
|
1068
|
+
window.removeEventListener("blur", markAttemptYieldedControl);
|
|
1069
|
+
clearAttemptTimer();
|
|
1070
|
+
};
|
|
1071
|
+
}, []);
|
|
879
1072
|
(0, import_react8.useEffect)(() => {
|
|
880
1073
|
onLoadStateChangeRef.current?.(ready && !failed);
|
|
881
1074
|
}, [ready, failed]);
|
|
@@ -1012,13 +1205,13 @@ function DirectPayPalButton({
|
|
|
1012
1205
|
console.error("[FloPay] DirectPayPal load/render failure:", message);
|
|
1013
1206
|
};
|
|
1014
1207
|
setFailed(false);
|
|
1015
|
-
const dispatchTokenizedBody = async (body) => {
|
|
1016
|
-
const prepared = beforeClickRef.current;
|
|
1208
|
+
const dispatchTokenizedBody = async (body, prepared = beforeClickRef.current) => {
|
|
1017
1209
|
const effectiveSessionId = prepared?.sessionId ?? sessionId;
|
|
1018
1210
|
if (onTokenizedBodyRef.current) {
|
|
1019
1211
|
onTokenizedBodyRef.current(body, {
|
|
1020
1212
|
sessionId: effectiveSessionId,
|
|
1021
|
-
accountPatch: prepared?.accountPatch
|
|
1213
|
+
accountPatch: prepared?.accountPatch,
|
|
1214
|
+
nonce: prepared?.nonce
|
|
1022
1215
|
});
|
|
1023
1216
|
return;
|
|
1024
1217
|
}
|
|
@@ -1031,7 +1224,7 @@ function DirectPayPalButton({
|
|
|
1031
1224
|
effectiveUserId,
|
|
1032
1225
|
{
|
|
1033
1226
|
sessionId: effectiveSessionId,
|
|
1034
|
-
nonce: nonceRef.current,
|
|
1227
|
+
nonce: prepared?.nonce ?? nonceRef.current,
|
|
1035
1228
|
tokenizedData: body,
|
|
1036
1229
|
accountData: {
|
|
1037
1230
|
userId: effectiveUserId,
|
|
@@ -1067,7 +1260,7 @@ function DirectPayPalButton({
|
|
|
1067
1260
|
const effectiveSessionId = prepared?.sessionId ?? sessionId;
|
|
1068
1261
|
const effectiveEmail = prepared?.accountPatch?.email ?? emailRef.current;
|
|
1069
1262
|
const intentHeaders = { "Content-Type": "application/json" };
|
|
1070
|
-
const currentNonce = nonceRef.current;
|
|
1263
|
+
const currentNonce = prepared?.nonce ?? nonceRef.current;
|
|
1071
1264
|
if (currentNonce) intentHeaders["x-checkout-session-token"] = currentNonce;
|
|
1072
1265
|
const response = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
1073
1266
|
method: "POST",
|
|
@@ -1089,6 +1282,7 @@ function DirectPayPalButton({
|
|
|
1089
1282
|
}
|
|
1090
1283
|
return id;
|
|
1091
1284
|
};
|
|
1285
|
+
const effectRenderGeneration = renderGeneration;
|
|
1092
1286
|
appendDebug("loadScript:scheduled (deferred 1 tick)");
|
|
1093
1287
|
let loadPromise = null;
|
|
1094
1288
|
const startTimer = setTimeout(() => {
|
|
@@ -1121,10 +1315,16 @@ function DirectPayPalButton({
|
|
|
1121
1315
|
return;
|
|
1122
1316
|
}
|
|
1123
1317
|
const handleApprove = async (data) => {
|
|
1318
|
+
if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
|
|
1319
|
+
const token = data.subscriptionID ?? data.orderID ?? "";
|
|
1320
|
+
const context = token ? approvalContextByTokenRef.current.get(token) : attemptContextBySurfaceRef.current.get(effectRenderGeneration);
|
|
1321
|
+
if (!context || !isAttemptActive(context.generation)) return;
|
|
1322
|
+
if (!finishAttempt(context.generation)) return;
|
|
1323
|
+
approvalContextByTokenRef.current.delete(token);
|
|
1324
|
+
attemptContextBySurfaceRef.current.delete(context.surfaceKey);
|
|
1124
1325
|
try {
|
|
1125
1326
|
setSubmitting(true);
|
|
1126
1327
|
onErrorChangeRef.current?.(null);
|
|
1127
|
-
const token = data.subscriptionID ?? data.orderID ?? "";
|
|
1128
1328
|
if (!token) {
|
|
1129
1329
|
throw new import_shared4.FloPayError(
|
|
1130
1330
|
"PayPal did not return an approval token.",
|
|
@@ -1135,7 +1335,7 @@ function DirectPayPalButton({
|
|
|
1135
1335
|
await dispatchTokenizedBody({
|
|
1136
1336
|
id: token,
|
|
1137
1337
|
isPaypal: true
|
|
1138
|
-
});
|
|
1338
|
+
}, context);
|
|
1139
1339
|
} catch (err) {
|
|
1140
1340
|
forwardError(err instanceof Error ? err.message : "PayPal capture failed.");
|
|
1141
1341
|
} finally {
|
|
@@ -1151,47 +1351,70 @@ function DirectPayPalButton({
|
|
|
1151
1351
|
// matching the Stripe-rendered PayPal flow.
|
|
1152
1352
|
onClick: async (_data, actions) => {
|
|
1153
1353
|
const runner = runBeforeButtonClickRef.current;
|
|
1354
|
+
let prepared = {};
|
|
1154
1355
|
if (runner) {
|
|
1155
1356
|
try {
|
|
1156
1357
|
const beforeClick = await runner("paypal");
|
|
1157
1358
|
if (!beforeClick.proceed) {
|
|
1158
1359
|
beforeClickRef.current = null;
|
|
1360
|
+
attemptContextBySurfaceRef.current.delete(effectRenderGeneration);
|
|
1159
1361
|
await actions.reject();
|
|
1160
1362
|
return;
|
|
1161
1363
|
}
|
|
1162
|
-
|
|
1364
|
+
prepared = {
|
|
1163
1365
|
sessionId: beforeClick.sessionId,
|
|
1164
|
-
accountPatch: beforeClick.accountPatch
|
|
1366
|
+
accountPatch: beforeClick.accountPatch,
|
|
1367
|
+
nonce: beforeClick.nonce
|
|
1165
1368
|
};
|
|
1166
1369
|
} catch (err) {
|
|
1167
1370
|
appendDebug(`onClick:runBeforeButtonClick rejected msg=${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`);
|
|
1168
1371
|
beforeClickRef.current = null;
|
|
1372
|
+
attemptContextBySurfaceRef.current.delete(effectRenderGeneration);
|
|
1169
1373
|
await actions.reject();
|
|
1170
1374
|
return;
|
|
1171
1375
|
}
|
|
1172
|
-
} else {
|
|
1173
|
-
beforeClickRef.current = null;
|
|
1174
1376
|
}
|
|
1175
1377
|
onButtonClickRef.current?.("paypal");
|
|
1378
|
+
const generation = startAttempt();
|
|
1379
|
+
const context = {
|
|
1380
|
+
generation,
|
|
1381
|
+
surfaceKey: effectRenderGeneration,
|
|
1382
|
+
...prepared
|
|
1383
|
+
};
|
|
1384
|
+
beforeClickRef.current = context;
|
|
1385
|
+
attemptContextBySurfaceRef.current.set(effectRenderGeneration, context);
|
|
1176
1386
|
await actions.resolve();
|
|
1177
1387
|
},
|
|
1178
1388
|
// When the SDK is already holding an order/subscription id from a
|
|
1179
1389
|
// prior backend round-trip (the `paypal_direct_required` retry
|
|
1180
1390
|
// path), feed it straight to PayPal instead of creating a new one.
|
|
1181
1391
|
// Otherwise fall back to the normal create-intent call.
|
|
1182
|
-
createOrder: isSubscription ? void 0 : existingOrderId ? () => Promise.resolve(existingOrderId) : () => createPaypalIntent("Failed to create PayPal order."),
|
|
1183
|
-
createSubscription: isSubscription ? existingOrderId ? () => Promise.resolve(existingOrderId) : () => createPaypalIntent("Failed to create PayPal subscription.") : void 0,
|
|
1392
|
+
createOrder: isSubscription ? void 0 : existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal order.").then(registerApprovalContext),
|
|
1393
|
+
createSubscription: isSubscription ? existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal subscription.").then(registerApprovalContext) : void 0,
|
|
1184
1394
|
onApprove: handleApprove,
|
|
1185
1395
|
onCancel: () => {
|
|
1396
|
+
const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
|
|
1397
|
+
if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
|
|
1186
1398
|
beforeClickRef.current = null;
|
|
1187
|
-
|
|
1399
|
+
pendingProviderFocusRef.current = true;
|
|
1400
|
+
invalidateAttempt({ generation: context?.generation, remount: true, focus: false });
|
|
1188
1401
|
},
|
|
1189
1402
|
onError: (err) => {
|
|
1403
|
+
if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
|
|
1190
1404
|
const message = err instanceof Error ? err.message : "PayPal failed to render.";
|
|
1191
1405
|
appendDebug(`onError rendered=${rendered} msg=${message.slice(0, 120)}`);
|
|
1192
1406
|
if (rendered) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1407
|
+
const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
|
|
1408
|
+
if (context && !isAttemptActive(context.generation)) return;
|
|
1409
|
+
if (!context && invalidatedAttemptGenerationRef.current === attemptGenerationRef.current) return;
|
|
1410
|
+
if (isZoidLifecycleMessage(message)) {
|
|
1411
|
+
finishAttempt(context?.generation);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
|
|
1415
|
+
beforeClickRef.current = null;
|
|
1416
|
+
invalidateAttempt({ generation: context?.generation, remount: true, showRetry: true, focus: true });
|
|
1417
|
+
notifyTechnicalFailure(err, { code: "paypal_runtime_failed" });
|
|
1195
1418
|
return;
|
|
1196
1419
|
}
|
|
1197
1420
|
markRenderFailed(message);
|
|
@@ -1291,7 +1514,7 @@ function DirectPayPalButton({
|
|
|
1291
1514
|
});
|
|
1292
1515
|
}
|
|
1293
1516
|
};
|
|
1294
|
-
}, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId]);
|
|
1517
|
+
}, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration]);
|
|
1295
1518
|
const debugPanel = debug ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1296
1519
|
"pre",
|
|
1297
1520
|
{
|
|
@@ -1346,15 +1569,29 @@ ${debugLines.join("\n")}`
|
|
|
1346
1569
|
{
|
|
1347
1570
|
ref: containerRef,
|
|
1348
1571
|
"data-testid": "flopay-direct-paypal-container",
|
|
1572
|
+
tabIndex: -1,
|
|
1573
|
+
"aria-label": "PayPal payment method",
|
|
1349
1574
|
style: {
|
|
1350
1575
|
minHeight: DEFAULT_BUTTON_HEIGHT,
|
|
1351
1576
|
display: "flex",
|
|
1352
1577
|
opacity: ready ? 1 : 0
|
|
1353
1578
|
},
|
|
1354
1579
|
"aria-busy": submitting || isProcessing
|
|
1355
|
-
}
|
|
1580
|
+
},
|
|
1581
|
+
renderGeneration
|
|
1356
1582
|
)
|
|
1357
|
-
] })
|
|
1583
|
+
] }),
|
|
1584
|
+
showRetryFocusTarget && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1585
|
+
"button",
|
|
1586
|
+
{
|
|
1587
|
+
ref: focusTargetRef,
|
|
1588
|
+
type: "button",
|
|
1589
|
+
"data-testid": "flopay-direct-paypal-focus-target",
|
|
1590
|
+
onClick: focusPayPalSurface,
|
|
1591
|
+
style: DIRECT_PAYPAL_RECOVERY_ACTION_STYLE,
|
|
1592
|
+
children: "Try PayPal again"
|
|
1593
|
+
}
|
|
1594
|
+
)
|
|
1358
1595
|
] })
|
|
1359
1596
|
);
|
|
1360
1597
|
}
|
|
@@ -1438,6 +1675,19 @@ var BUTTONS_PANEL_HIDDEN_STYLE = {
|
|
|
1438
1675
|
height: 0,
|
|
1439
1676
|
overflow: "hidden"
|
|
1440
1677
|
};
|
|
1678
|
+
var EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE = {
|
|
1679
|
+
width: "100%",
|
|
1680
|
+
minHeight: DEFAULT_BUTTONS_LAYOUT_BUTTON_HEIGHT,
|
|
1681
|
+
margin: "0 0 0.5rem",
|
|
1682
|
+
padding: "0.75rem 0.875rem",
|
|
1683
|
+
border: "1px solid #2563eb",
|
|
1684
|
+
borderRadius: 8,
|
|
1685
|
+
background: "#eff6ff",
|
|
1686
|
+
color: "#1d4ed8",
|
|
1687
|
+
fontSize: "0.95rem",
|
|
1688
|
+
fontWeight: 700,
|
|
1689
|
+
cursor: "pointer"
|
|
1690
|
+
};
|
|
1441
1691
|
function getButtonMethodLabel(method) {
|
|
1442
1692
|
switch (method) {
|
|
1443
1693
|
case "paypal":
|
|
@@ -1450,6 +1700,118 @@ function getButtonMethodLabel(method) {
|
|
|
1450
1700
|
return "Card";
|
|
1451
1701
|
}
|
|
1452
1702
|
}
|
|
1703
|
+
function checkoutButtonMethodFromProviderMethod(method) {
|
|
1704
|
+
if (method === "paypal") return "paypal";
|
|
1705
|
+
if (method === "apple_pay") return "apple_pay";
|
|
1706
|
+
return "google_pay";
|
|
1707
|
+
}
|
|
1708
|
+
function getExternalMethodDisplayName(method) {
|
|
1709
|
+
return method === "paypal" ? "PayPal" : (0, import_shared5.getStripeMethodDisplayName)(method);
|
|
1710
|
+
}
|
|
1711
|
+
function buildExternalMethodRecoveryMessage(method, popupBlocked) {
|
|
1712
|
+
const base = `We couldn't open ${getExternalMethodDisplayName(method)}. Try again or choose another payment method.`;
|
|
1713
|
+
return popupBlocked ? `${base} Allow pop-ups for this site, then try again.` : base;
|
|
1714
|
+
}
|
|
1715
|
+
function useExternalAttemptReconciliation(onMissingTerminal) {
|
|
1716
|
+
const generationRef = (0, import_react9.useRef)(0);
|
|
1717
|
+
const invalidatedGenerationRef = (0, import_react9.useRef)(null);
|
|
1718
|
+
const attemptRef = (0, import_react9.useRef)(null);
|
|
1719
|
+
const clearAttemptTimer = (0, import_react9.useCallback)((targetAttempt = attemptRef.current) => {
|
|
1720
|
+
const attempt = targetAttempt;
|
|
1721
|
+
if (attempt?.timer) {
|
|
1722
|
+
clearTimeout(attempt.timer);
|
|
1723
|
+
attempt.timer = null;
|
|
1724
|
+
}
|
|
1725
|
+
}, []);
|
|
1726
|
+
const armAttemptTimer = (0, import_react9.useCallback)((attempt, delayMs) => {
|
|
1727
|
+
if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
|
|
1728
|
+
clearAttemptTimer(attempt);
|
|
1729
|
+
attempt.timer = setTimeout(() => {
|
|
1730
|
+
attempt.timer = null;
|
|
1731
|
+
if (attemptRef.current?.generation !== attempt.generation) return;
|
|
1732
|
+
attemptRef.current = null;
|
|
1733
|
+
invalidatedGenerationRef.current = attempt.generation;
|
|
1734
|
+
onMissingTerminal(
|
|
1735
|
+
attempt.method,
|
|
1736
|
+
new Error("External payment method returned without a terminal callback."),
|
|
1737
|
+
{ code: "external_method_missing_terminal_callback" }
|
|
1738
|
+
);
|
|
1739
|
+
}, delayMs);
|
|
1740
|
+
}, [clearAttemptTimer, onMissingTerminal]);
|
|
1741
|
+
const armRecoveryTimer = (0, import_react9.useCallback)((attempt) => {
|
|
1742
|
+
if (!attempt.yieldedControl) return;
|
|
1743
|
+
armAttemptTimer(attempt, EXTERNAL_METHOD_CALLBACK_GRACE_MS);
|
|
1744
|
+
}, [armAttemptTimer]);
|
|
1745
|
+
const startAttempt = (0, import_react9.useCallback)((method) => {
|
|
1746
|
+
clearAttemptTimer();
|
|
1747
|
+
const generation = generationRef.current + 1;
|
|
1748
|
+
generationRef.current = generation;
|
|
1749
|
+
invalidatedGenerationRef.current = null;
|
|
1750
|
+
const attempt = { generation, method, timer: null, yieldedControl: false };
|
|
1751
|
+
attemptRef.current = attempt;
|
|
1752
|
+
return generation;
|
|
1753
|
+
}, [clearAttemptTimer]);
|
|
1754
|
+
const finishAttempt = (0, import_react9.useCallback)((generation) => {
|
|
1755
|
+
const attempt = attemptRef.current;
|
|
1756
|
+
if (typeof generation === "number" && attempt?.generation !== generation) return false;
|
|
1757
|
+
clearAttemptTimer(attempt);
|
|
1758
|
+
attemptRef.current = null;
|
|
1759
|
+
if (typeof generation !== "number" || invalidatedGenerationRef.current === generation) {
|
|
1760
|
+
invalidatedGenerationRef.current = null;
|
|
1761
|
+
}
|
|
1762
|
+
return true;
|
|
1763
|
+
}, [clearAttemptTimer]);
|
|
1764
|
+
const invalidateAttempt = (0, import_react9.useCallback)((generation) => {
|
|
1765
|
+
const attempt = attemptRef.current;
|
|
1766
|
+
const targetGeneration = generation ?? attempt?.generation ?? generationRef.current;
|
|
1767
|
+
if (!generation || attempt?.generation === generation) {
|
|
1768
|
+
clearAttemptTimer(attempt);
|
|
1769
|
+
attemptRef.current = null;
|
|
1770
|
+
}
|
|
1771
|
+
invalidatedGenerationRef.current = targetGeneration;
|
|
1772
|
+
}, [clearAttemptTimer]);
|
|
1773
|
+
const isAttemptInvalidated = (0, import_react9.useCallback)(
|
|
1774
|
+
(generation) => invalidatedGenerationRef.current === (generation ?? generationRef.current),
|
|
1775
|
+
[]
|
|
1776
|
+
);
|
|
1777
|
+
const isAttemptCurrent = (0, import_react9.useCallback)(
|
|
1778
|
+
(generation) => attemptRef.current?.generation === generation && invalidatedGenerationRef.current !== generation,
|
|
1779
|
+
[]
|
|
1780
|
+
);
|
|
1781
|
+
const scheduleRecoveryIfReturned = (0, import_react9.useCallback)(() => {
|
|
1782
|
+
const attempt = attemptRef.current;
|
|
1783
|
+
if (!attempt) return;
|
|
1784
|
+
armRecoveryTimer(attempt);
|
|
1785
|
+
}, [armRecoveryTimer]);
|
|
1786
|
+
const markAttemptYieldedControl = (0, import_react9.useCallback)(() => {
|
|
1787
|
+
const attempt = attemptRef.current;
|
|
1788
|
+
if (!attempt) return;
|
|
1789
|
+
attempt.yieldedControl = true;
|
|
1790
|
+
clearAttemptTimer(attempt);
|
|
1791
|
+
}, [clearAttemptTimer]);
|
|
1792
|
+
(0, import_react9.useEffect)(() => {
|
|
1793
|
+
const handleVisibilityChange = () => {
|
|
1794
|
+
if (document.visibilityState === "visible") {
|
|
1795
|
+
scheduleRecoveryIfReturned();
|
|
1796
|
+
} else {
|
|
1797
|
+
markAttemptYieldedControl();
|
|
1798
|
+
}
|
|
1799
|
+
};
|
|
1800
|
+
const handleBlur = () => {
|
|
1801
|
+
markAttemptYieldedControl();
|
|
1802
|
+
};
|
|
1803
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
1804
|
+
window.addEventListener("blur", handleBlur);
|
|
1805
|
+
window.addEventListener("focus", scheduleRecoveryIfReturned);
|
|
1806
|
+
return () => {
|
|
1807
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
1808
|
+
window.removeEventListener("blur", handleBlur);
|
|
1809
|
+
window.removeEventListener("focus", scheduleRecoveryIfReturned);
|
|
1810
|
+
clearAttemptTimer();
|
|
1811
|
+
};
|
|
1812
|
+
}, [clearAttemptTimer, markAttemptYieldedControl, scheduleRecoveryIfReturned]);
|
|
1813
|
+
return { startAttempt, finishAttempt, invalidateAttempt, isAttemptInvalidated, isAttemptCurrent };
|
|
1814
|
+
}
|
|
1453
1815
|
function malformedPostcodeMessage(country) {
|
|
1454
1816
|
const example = (0, import_shared5.getPostalCodeExample)(country);
|
|
1455
1817
|
return `Enter a valid ${(0, import_shared5.getPostalCodeLabel)(country)}${example ? ` (e.g. ${example})` : ""}`;
|
|
@@ -1544,6 +1906,7 @@ function PayPalButtonInner({
|
|
|
1544
1906
|
isProcessing = false,
|
|
1545
1907
|
onButtonClick,
|
|
1546
1908
|
onDecline,
|
|
1909
|
+
onTechnicalFailure,
|
|
1547
1910
|
runBeforeButtonClick,
|
|
1548
1911
|
onLoadStateChange,
|
|
1549
1912
|
placeholderBorderRadius
|
|
@@ -1556,8 +1919,55 @@ function PayPalButtonInner({
|
|
|
1556
1919
|
}, [loadState, onLoadStateChange]);
|
|
1557
1920
|
const [submitting, setSubmitting] = (0, import_react9.useState)(false);
|
|
1558
1921
|
const paypalResumeAttempted = (0, import_react9.useRef)(false);
|
|
1559
|
-
const
|
|
1922
|
+
const focusTargetRef = (0, import_react9.useRef)(null);
|
|
1923
|
+
const recoveryActionRef = (0, import_react9.useRef)(null);
|
|
1924
|
+
const [surfaceKey, setSurfaceKey] = (0, import_react9.useState)(0);
|
|
1925
|
+
const [showRecoveryAction, setShowRecoveryAction] = (0, import_react9.useState)(false);
|
|
1926
|
+
const pendingProviderFocusRef = (0, import_react9.useRef)(false);
|
|
1927
|
+
const attemptContextBySurfaceRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
|
|
1560
1928
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
1929
|
+
const {
|
|
1930
|
+
startAttempt,
|
|
1931
|
+
finishAttempt,
|
|
1932
|
+
invalidateAttempt,
|
|
1933
|
+
isAttemptInvalidated,
|
|
1934
|
+
isAttemptCurrent
|
|
1935
|
+
} = useExternalAttemptReconciliation((method, err, options) => {
|
|
1936
|
+
onTechnicalFailure?.(method, err, {
|
|
1937
|
+
...options,
|
|
1938
|
+
popupBlocked: options?.popupBlocked ?? isPopupBlockedError(err)
|
|
1939
|
+
});
|
|
1940
|
+
setShowRecoveryAction(true);
|
|
1941
|
+
setSurfaceKey((key) => key + 1);
|
|
1942
|
+
});
|
|
1943
|
+
(0, import_react9.useEffect)(() => {
|
|
1944
|
+
if (showRecoveryAction) recoveryActionRef.current?.focus();
|
|
1945
|
+
}, [showRecoveryAction, surfaceKey]);
|
|
1946
|
+
const resetSurface = (0, import_react9.useCallback)(() => {
|
|
1947
|
+
setSurfaceKey((key) => key + 1);
|
|
1948
|
+
}, []);
|
|
1949
|
+
const recoverTechnicalFailure = (0, import_react9.useCallback)((err, code, generation) => {
|
|
1950
|
+
invalidateAttempt(generation);
|
|
1951
|
+
onTechnicalFailure?.("paypal", err, { code, popupBlocked: isPopupBlockedError(err) });
|
|
1952
|
+
setShowRecoveryAction(true);
|
|
1953
|
+
resetSurface();
|
|
1954
|
+
}, [invalidateAttempt, onTechnicalFailure, resetSurface]);
|
|
1955
|
+
const focusProviderSurface = (0, import_react9.useCallback)(() => {
|
|
1956
|
+
window.setTimeout(() => {
|
|
1957
|
+
const target = focusTargetRef.current?.querySelector("iframe");
|
|
1958
|
+
(target ?? focusTargetRef.current)?.focus();
|
|
1959
|
+
}, 0);
|
|
1960
|
+
}, []);
|
|
1961
|
+
(0, import_react9.useEffect)(() => {
|
|
1962
|
+
if (!pendingProviderFocusRef.current) return;
|
|
1963
|
+
pendingProviderFocusRef.current = false;
|
|
1964
|
+
focusProviderSurface();
|
|
1965
|
+
}, [focusProviderSurface, surfaceKey]);
|
|
1966
|
+
const handleRecoveryActionClick = (0, import_react9.useCallback)(() => {
|
|
1967
|
+
setShowRecoveryAction(false);
|
|
1968
|
+
onErrorChange?.(null);
|
|
1969
|
+
focusProviderSurface();
|
|
1970
|
+
}, [focusProviderSurface, onErrorChange]);
|
|
1561
1971
|
(0, import_react9.useEffect)(() => {
|
|
1562
1972
|
if (!stripe || paypalResumeAttempted.current) return;
|
|
1563
1973
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -1631,22 +2041,33 @@ function PayPalButtonInner({
|
|
|
1631
2041
|
}
|
|
1632
2042
|
const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick("paypal") : { proceed: true };
|
|
1633
2043
|
if (!beforeClick.proceed) {
|
|
1634
|
-
|
|
2044
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
1635
2045
|
event.reject();
|
|
1636
2046
|
return;
|
|
1637
2047
|
}
|
|
1638
|
-
|
|
2048
|
+
const generation = startAttempt("paypal");
|
|
2049
|
+
attemptContextBySurfaceRef.current.set(surfaceKey, {
|
|
2050
|
+
generation,
|
|
1639
2051
|
accountPatch: beforeClick.accountPatch,
|
|
1640
2052
|
sessionId: beforeClick.sessionId,
|
|
1641
2053
|
nonce: beforeClick.nonce
|
|
1642
|
-
};
|
|
2054
|
+
});
|
|
2055
|
+
setShowRecoveryAction(false);
|
|
1643
2056
|
onButtonClick?.("paypal");
|
|
1644
2057
|
event.resolve();
|
|
1645
|
-
}, [isProcessing, onButtonClick, runBeforeButtonClick, submitting]);
|
|
2058
|
+
}, [attemptContextBySurfaceRef, isProcessing, onButtonClick, runBeforeButtonClick, startAttempt, submitting, surfaceKey]);
|
|
1646
2059
|
const handlePayPalConfirm = (0, import_react9.useCallback)(async (event) => {
|
|
1647
2060
|
if (!stripe || !elements) return;
|
|
1648
|
-
|
|
1649
|
-
|
|
2061
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2062
|
+
if (attemptContext && !isAttemptCurrent(attemptContext.generation)) {
|
|
2063
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
if (!attemptContext && isAttemptInvalidated()) return;
|
|
2067
|
+
if (attemptContext && !finishAttempt(attemptContext.generation)) return;
|
|
2068
|
+
if (!attemptContext) finishAttempt();
|
|
2069
|
+
let prepared = attemptContext ?? null;
|
|
2070
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
1650
2071
|
if (!prepared && runBeforeButtonClick) {
|
|
1651
2072
|
const beforeClick = await runBeforeButtonClick("paypal");
|
|
1652
2073
|
if (!beforeClick.proceed) {
|
|
@@ -1671,9 +2092,8 @@ function PayPalButtonInner({
|
|
|
1671
2092
|
const createPM = stripe.createPaymentMethod;
|
|
1672
2093
|
const { error: pmError, paymentMethod } = await createPM({ type: "paypal" });
|
|
1673
2094
|
if (pmError) {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
event.paymentFailed({ reason: "fail", message });
|
|
2095
|
+
recoverTechnicalFailure(pmError, "stripe_paypal_create_payment_method_failed", attemptContext?.generation);
|
|
2096
|
+
event.paymentFailed({ reason: "fail" });
|
|
1677
2097
|
return;
|
|
1678
2098
|
}
|
|
1679
2099
|
const intentHeaders = { "Content-Type": "application/json" };
|
|
@@ -1732,11 +2152,16 @@ function PayPalButtonInner({
|
|
|
1732
2152
|
} catch {
|
|
1733
2153
|
}
|
|
1734
2154
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
2155
|
+
if (isProviderDecline(confirmError)) {
|
|
2156
|
+
const message = confirmError.message ?? "Your payment was declined.";
|
|
2157
|
+
onErrorChange?.(message);
|
|
2158
|
+
onDecline?.(buildDeclineEvent("paypal", message, {
|
|
2159
|
+
code: getProviderErrorCode(confirmError),
|
|
2160
|
+
declineCode: getProviderDeclineCode(confirmError)
|
|
2161
|
+
}));
|
|
2162
|
+
} else {
|
|
2163
|
+
recoverTechnicalFailure(confirmError, "stripe_paypal_confirm_failed", attemptContext?.generation);
|
|
2164
|
+
}
|
|
1740
2165
|
return;
|
|
1741
2166
|
}
|
|
1742
2167
|
if (typeof window !== "undefined") {
|
|
@@ -1758,11 +2183,11 @@ function PayPalButtonInner({
|
|
|
1758
2183
|
});
|
|
1759
2184
|
return;
|
|
1760
2185
|
} catch (err) {
|
|
1761
|
-
|
|
2186
|
+
recoverTechnicalFailure(err, "stripe_paypal_failed", attemptContext?.generation);
|
|
1762
2187
|
} finally {
|
|
1763
2188
|
setSubmitting(false);
|
|
1764
2189
|
}
|
|
1765
|
-
}, [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]);
|
|
2190
|
+
}, [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]);
|
|
1766
2191
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1767
2192
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1768
2193
|
ExpressCheckoutReadySwap,
|
|
@@ -1770,29 +2195,57 @@ function PayPalButtonInner({
|
|
|
1770
2195
|
state: loadState,
|
|
1771
2196
|
placeholderTestId: "flopay-paypal-placeholder",
|
|
1772
2197
|
borderRadius: placeholderBorderRadius,
|
|
1773
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime7.
|
|
1774
|
-
|
|
2198
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2199
|
+
"div",
|
|
1775
2200
|
{
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2201
|
+
ref: focusTargetRef,
|
|
2202
|
+
tabIndex: -1,
|
|
2203
|
+
"data-testid": "flopay-paypal-focus-target",
|
|
2204
|
+
"aria-label": "PayPal payment method",
|
|
2205
|
+
style: { borderRadius: 8, outlineOffset: 4 },
|
|
2206
|
+
children: [
|
|
2207
|
+
showRecoveryAction && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2208
|
+
"button",
|
|
2209
|
+
{
|
|
2210
|
+
ref: recoveryActionRef,
|
|
2211
|
+
type: "button",
|
|
2212
|
+
"data-testid": "flopay-paypal-retry-button",
|
|
2213
|
+
onClick: handleRecoveryActionClick,
|
|
2214
|
+
style: EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE,
|
|
2215
|
+
children: "Try PayPal again"
|
|
2216
|
+
}
|
|
2217
|
+
),
|
|
2218
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2219
|
+
import_react_stripe_js.ExpressCheckoutElement,
|
|
2220
|
+
{
|
|
2221
|
+
onReady: (event) => setLoadState(resolveExpressCheckoutLoadState(event, ["paypal"])),
|
|
2222
|
+
onLoadError: () => setLoadState("load_error"),
|
|
2223
|
+
onClick: handlePayPalClick,
|
|
2224
|
+
onConfirm: handlePayPalConfirm,
|
|
2225
|
+
onCancel: () => {
|
|
2226
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2227
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2228
|
+
invalidateAttempt(attemptContext?.generation);
|
|
2229
|
+
setShowRecoveryAction(false);
|
|
2230
|
+
pendingProviderFocusRef.current = true;
|
|
2231
|
+
resetSurface();
|
|
2232
|
+
},
|
|
2233
|
+
options: {
|
|
2234
|
+
buttonType: { paypal: "paypal" },
|
|
2235
|
+
billingAddressRequired: false,
|
|
2236
|
+
phoneNumberRequired: false,
|
|
2237
|
+
shippingAddressRequired: false,
|
|
2238
|
+
paymentMethods: {
|
|
2239
|
+
applePay: "never",
|
|
2240
|
+
googlePay: "never",
|
|
2241
|
+
paypal: "auto",
|
|
2242
|
+
link: "never"
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
},
|
|
2246
|
+
surfaceKey
|
|
2247
|
+
)
|
|
2248
|
+
]
|
|
1796
2249
|
}
|
|
1797
2250
|
)
|
|
1798
2251
|
}
|
|
@@ -1810,6 +2263,7 @@ function WalletButtonInner({
|
|
|
1810
2263
|
onErrorChange,
|
|
1811
2264
|
onButtonClick,
|
|
1812
2265
|
onDecline,
|
|
2266
|
+
onTechnicalFailure,
|
|
1813
2267
|
runBeforeButtonClick,
|
|
1814
2268
|
onLoadStateChange,
|
|
1815
2269
|
placeholderBorderRadius
|
|
@@ -1822,15 +2276,74 @@ function WalletButtonInner({
|
|
|
1822
2276
|
}, [loadState, onLoadStateChange]);
|
|
1823
2277
|
const [submitting, setSubmitting] = (0, import_react9.useState)(false);
|
|
1824
2278
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
1825
|
-
const
|
|
1826
|
-
const
|
|
2279
|
+
const focusTargetRef = (0, import_react9.useRef)(null);
|
|
2280
|
+
const recoveryActionRef = (0, import_react9.useRef)(null);
|
|
2281
|
+
const [surfaceKey, setSurfaceKey] = (0, import_react9.useState)(0);
|
|
2282
|
+
const [showRecoveryAction, setShowRecoveryAction] = (0, import_react9.useState)(false);
|
|
2283
|
+
const [recoveryActionMethod, setRecoveryActionMethod] = (0, import_react9.useState)("google_pay");
|
|
2284
|
+
const pendingProviderFocusRef = (0, import_react9.useRef)(false);
|
|
2285
|
+
const lastWalletProviderMethodRef = (0, import_react9.useRef)("google_pay");
|
|
2286
|
+
const lastWalletMethodRef = (0, import_react9.useRef)("google_pay");
|
|
2287
|
+
const attemptContextBySurfaceRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
|
|
2288
|
+
const {
|
|
2289
|
+
startAttempt,
|
|
2290
|
+
finishAttempt,
|
|
2291
|
+
invalidateAttempt,
|
|
2292
|
+
isAttemptInvalidated,
|
|
2293
|
+
isAttemptCurrent
|
|
2294
|
+
} = useExternalAttemptReconciliation((method, err, options) => {
|
|
2295
|
+
onTechnicalFailure?.(method, err, {
|
|
2296
|
+
...options,
|
|
2297
|
+
popupBlocked: options?.popupBlocked ?? isPopupBlockedError(err)
|
|
2298
|
+
});
|
|
2299
|
+
setRecoveryActionMethod(method);
|
|
2300
|
+
setShowRecoveryAction(true);
|
|
2301
|
+
setSurfaceKey((key) => key + 1);
|
|
2302
|
+
});
|
|
2303
|
+
(0, import_react9.useEffect)(() => {
|
|
2304
|
+
if (showRecoveryAction) recoveryActionRef.current?.focus();
|
|
2305
|
+
}, [showRecoveryAction, surfaceKey]);
|
|
2306
|
+
const resetSurface = (0, import_react9.useCallback)(() => {
|
|
2307
|
+
setSurfaceKey((key) => key + 1);
|
|
2308
|
+
}, []);
|
|
2309
|
+
const recoverTechnicalFailure = (0, import_react9.useCallback)((method, err, code, generation) => {
|
|
2310
|
+
invalidateAttempt(generation);
|
|
2311
|
+
onTechnicalFailure?.(method, err, { code, popupBlocked: isPopupBlockedError(err) });
|
|
2312
|
+
setRecoveryActionMethod(method);
|
|
2313
|
+
setShowRecoveryAction(true);
|
|
2314
|
+
resetSurface();
|
|
2315
|
+
}, [invalidateAttempt, onTechnicalFailure, resetSurface]);
|
|
2316
|
+
const focusProviderSurface = (0, import_react9.useCallback)(() => {
|
|
2317
|
+
window.setTimeout(() => {
|
|
2318
|
+
const target = focusTargetRef.current?.querySelector("iframe");
|
|
2319
|
+
(target ?? focusTargetRef.current)?.focus();
|
|
2320
|
+
}, 0);
|
|
2321
|
+
}, []);
|
|
2322
|
+
(0, import_react9.useEffect)(() => {
|
|
2323
|
+
if (!pendingProviderFocusRef.current) return;
|
|
2324
|
+
pendingProviderFocusRef.current = false;
|
|
2325
|
+
focusProviderSurface();
|
|
2326
|
+
}, [focusProviderSurface, surfaceKey]);
|
|
2327
|
+
const handleRecoveryActionClick = (0, import_react9.useCallback)(() => {
|
|
2328
|
+
setShowRecoveryAction(false);
|
|
2329
|
+
onErrorChange?.(null);
|
|
2330
|
+
focusProviderSurface();
|
|
2331
|
+
}, [focusProviderSurface, onErrorChange]);
|
|
1827
2332
|
const handleWalletConfirm = (0, import_react9.useCallback)(
|
|
1828
2333
|
async (event) => {
|
|
1829
2334
|
if (!stripe || !elements) return;
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2335
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2336
|
+
if (attemptContext && !isAttemptCurrent(attemptContext.generation)) {
|
|
2337
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2338
|
+
return;
|
|
2339
|
+
}
|
|
2340
|
+
if (!attemptContext && isAttemptInvalidated()) return;
|
|
2341
|
+
if (attemptContext && !finishAttempt(attemptContext.generation)) return;
|
|
2342
|
+
if (!attemptContext) finishAttempt();
|
|
2343
|
+
const walletType = event.expressPaymentType ?? attemptContext?.method ?? lastWalletProviderMethodRef.current;
|
|
2344
|
+
let prepared = attemptContext ?? null;
|
|
2345
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2346
|
+
const buttonMethod = checkoutButtonMethodFromProviderMethod(walletType);
|
|
1834
2347
|
if (!prepared && runBeforeButtonClick) {
|
|
1835
2348
|
const beforeClick = await runBeforeButtonClick(buttonMethod);
|
|
1836
2349
|
if (!beforeClick.proceed) {
|
|
@@ -1852,12 +2365,12 @@ function WalletButtonInner({
|
|
|
1852
2365
|
onErrorChange?.(null);
|
|
1853
2366
|
const { error: submitError } = await elements.submit();
|
|
1854
2367
|
if (submitError) {
|
|
1855
|
-
|
|
2368
|
+
recoverTechnicalFailure(walletType, submitError, "stripe_wallet_submit_failed", attemptContext?.generation);
|
|
1856
2369
|
return;
|
|
1857
2370
|
}
|
|
1858
2371
|
const { error: pmError, paymentMethod } = await stripe.createPaymentMethod({ elements });
|
|
1859
2372
|
if (pmError || !paymentMethod) {
|
|
1860
|
-
|
|
2373
|
+
recoverTechnicalFailure(walletType, pmError ?? new Error("Failed to create payment method."), "stripe_wallet_create_payment_method_failed", attemptContext?.generation);
|
|
1861
2374
|
return;
|
|
1862
2375
|
}
|
|
1863
2376
|
if (!effectiveSessionId || !effectiveEmail) {
|
|
@@ -1890,11 +2403,16 @@ function WalletButtonInner({
|
|
|
1890
2403
|
if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
|
|
1891
2404
|
const { error: confirmError, intentId } = (0, import_shared6.isSetupIntentClientSecret)(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 }));
|
|
1892
2405
|
if (confirmError) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2406
|
+
if (isProviderDecline(confirmError)) {
|
|
2407
|
+
const message = confirmError.message ?? "Your payment was declined.";
|
|
2408
|
+
onErrorChange?.(message);
|
|
2409
|
+
onDecline?.(buildDeclineEvent(method, message, {
|
|
2410
|
+
code: getProviderErrorCode(confirmError),
|
|
2411
|
+
declineCode: getProviderDeclineCode(confirmError)
|
|
2412
|
+
}));
|
|
2413
|
+
} else {
|
|
2414
|
+
recoverTechnicalFailure(walletType, confirmError, "stripe_wallet_confirm_failed", attemptContext?.generation);
|
|
2415
|
+
}
|
|
1898
2416
|
return;
|
|
1899
2417
|
}
|
|
1900
2418
|
onTokenizedBody({
|
|
@@ -1907,12 +2425,12 @@ function WalletButtonInner({
|
|
|
1907
2425
|
nonce: effectiveNonce
|
|
1908
2426
|
});
|
|
1909
2427
|
} catch (err) {
|
|
1910
|
-
|
|
2428
|
+
recoverTechnicalFailure(walletType, err, "stripe_wallet_failed", attemptContext?.generation);
|
|
1911
2429
|
} finally {
|
|
1912
2430
|
setSubmitting(false);
|
|
1913
2431
|
}
|
|
1914
2432
|
},
|
|
1915
|
-
[stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]
|
|
2433
|
+
[stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]
|
|
1916
2434
|
);
|
|
1917
2435
|
const expressMethodMap = (0, import_react9.useMemo)(() => {
|
|
1918
2436
|
const allKeys = ["applePay", "googlePay", "paypal", "link", "amazonPay", "klarna"];
|
|
@@ -1939,41 +2457,78 @@ function WalletButtonInner({
|
|
|
1939
2457
|
state: loadState,
|
|
1940
2458
|
placeholderTestId: "flopay-wallet-placeholder",
|
|
1941
2459
|
borderRadius: placeholderBorderRadius,
|
|
1942
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime7.
|
|
1943
|
-
|
|
2460
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2461
|
+
"div",
|
|
1944
2462
|
{
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2463
|
+
ref: focusTargetRef,
|
|
2464
|
+
tabIndex: -1,
|
|
2465
|
+
"data-testid": "flopay-wallet-focus-target",
|
|
2466
|
+
"aria-label": "Wallet payment methods",
|
|
2467
|
+
style: { borderRadius: 8, outlineOffset: 4 },
|
|
2468
|
+
children: [
|
|
2469
|
+
showRecoveryAction && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2470
|
+
"button",
|
|
2471
|
+
{
|
|
2472
|
+
ref: recoveryActionRef,
|
|
2473
|
+
type: "button",
|
|
2474
|
+
"data-testid": "flopay-wallet-retry-button",
|
|
2475
|
+
onClick: handleRecoveryActionClick,
|
|
2476
|
+
style: EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE,
|
|
2477
|
+
children: [
|
|
2478
|
+
"Try ",
|
|
2479
|
+
getExternalMethodDisplayName(recoveryActionMethod),
|
|
2480
|
+
" again"
|
|
2481
|
+
]
|
|
2482
|
+
}
|
|
2483
|
+
),
|
|
2484
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2485
|
+
import_react_stripe_js.ExpressCheckoutElement,
|
|
2486
|
+
{
|
|
2487
|
+
onReady: (event) => {
|
|
2488
|
+
setLoadState(resolveExpressCheckoutLoadState(event, availableMethodKeys));
|
|
2489
|
+
},
|
|
2490
|
+
onLoadError: (_event) => {
|
|
2491
|
+
setLoadState("load_error");
|
|
2492
|
+
},
|
|
2493
|
+
onClick: async (event) => {
|
|
2494
|
+
lastWalletProviderMethodRef.current = event.expressPaymentType;
|
|
2495
|
+
lastWalletMethodRef.current = checkoutButtonMethodFromProviderMethod(event.expressPaymentType);
|
|
2496
|
+
const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick(lastWalletMethodRef.current) : { proceed: true };
|
|
2497
|
+
if (!beforeClick.proceed) {
|
|
2498
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2499
|
+
event.reject();
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
const generation = startAttempt(event.expressPaymentType);
|
|
2503
|
+
attemptContextBySurfaceRef.current.set(surfaceKey, {
|
|
2504
|
+
generation,
|
|
2505
|
+
method: event.expressPaymentType,
|
|
2506
|
+
accountPatch: beforeClick.accountPatch,
|
|
2507
|
+
sessionId: beforeClick.sessionId,
|
|
2508
|
+
nonce: beforeClick.nonce
|
|
2509
|
+
});
|
|
2510
|
+
setShowRecoveryAction(false);
|
|
2511
|
+
onButtonClick?.(lastWalletMethodRef.current);
|
|
2512
|
+
event.resolve();
|
|
2513
|
+
},
|
|
2514
|
+
onConfirm: handleWalletConfirm,
|
|
2515
|
+
onCancel: () => {
|
|
2516
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2517
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2518
|
+
invalidateAttempt(attemptContext?.generation);
|
|
2519
|
+
setShowRecoveryAction(false);
|
|
2520
|
+
pendingProviderFocusRef.current = true;
|
|
2521
|
+
resetSurface();
|
|
2522
|
+
},
|
|
2523
|
+
options: {
|
|
2524
|
+
buttonType: { applePay: "plain", googlePay: "plain" },
|
|
2525
|
+
paymentMethods: expressMethodMap,
|
|
2526
|
+
layout: { maxColumns: 1, overflow: "never" }
|
|
2527
|
+
}
|
|
2528
|
+
},
|
|
2529
|
+
surfaceKey
|
|
2530
|
+
)
|
|
2531
|
+
]
|
|
1977
2532
|
}
|
|
1978
2533
|
)
|
|
1979
2534
|
}
|
|
@@ -2884,6 +3439,30 @@ function SplitCardFormInner({
|
|
|
2884
3439
|
},
|
|
2885
3440
|
[onDecline]
|
|
2886
3441
|
);
|
|
3442
|
+
const recoverExternalMethodTechnicalFailure = (0, import_react9.useCallback)(
|
|
3443
|
+
(method, err, options) => {
|
|
3444
|
+
const popupBlocked = options?.popupBlocked ?? isPopupBlockedError(err);
|
|
3445
|
+
const message = buildExternalMethodRecoveryMessage(method, popupBlocked);
|
|
3446
|
+
const providerCode = sanitizeExternalFailureCode(options?.code) ?? getProviderErrorCode(err) ?? "external_payment_method_failed";
|
|
3447
|
+
const floPayError = new import_shared6.FloPayError(message, "api_error", {
|
|
3448
|
+
code: "external_payment_method_failed",
|
|
3449
|
+
param: method
|
|
3450
|
+
});
|
|
3451
|
+
setOverlayStatus(null);
|
|
3452
|
+
if (layout === "buttons") {
|
|
3453
|
+
setViewState("buttons");
|
|
3454
|
+
setExpandedApmMethod(null);
|
|
3455
|
+
}
|
|
3456
|
+
updateError(message);
|
|
3457
|
+
onError?.(floPayError);
|
|
3458
|
+
console.error("[FloPay] External payment method failed:", {
|
|
3459
|
+
method,
|
|
3460
|
+
code: providerCode,
|
|
3461
|
+
popupBlocked
|
|
3462
|
+
});
|
|
3463
|
+
},
|
|
3464
|
+
[layout, onError, updateError]
|
|
3465
|
+
);
|
|
2887
3466
|
(0, import_react9.useEffect)(() => {
|
|
2888
3467
|
if (!vaultActive || !sessionId) {
|
|
2889
3468
|
setVaultMount(null);
|
|
@@ -4340,6 +4919,7 @@ function SplitCardFormInner({
|
|
|
4340
4919
|
onComplete,
|
|
4341
4920
|
onErrorChange: updateError,
|
|
4342
4921
|
onDecline,
|
|
4922
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4343
4923
|
onButtonClick,
|
|
4344
4924
|
runBeforeButtonClick,
|
|
4345
4925
|
isProcessing: isSubmitting,
|
|
@@ -4363,6 +4943,7 @@ function SplitCardFormInner({
|
|
|
4363
4943
|
isProcessing: isSubmitting,
|
|
4364
4944
|
onButtonClick,
|
|
4365
4945
|
onDecline,
|
|
4946
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4366
4947
|
runBeforeButtonClick,
|
|
4367
4948
|
onLoadStateChange: setPaypalLoadState,
|
|
4368
4949
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -4380,6 +4961,7 @@ function SplitCardFormInner({
|
|
|
4380
4961
|
onErrorChange: updateError,
|
|
4381
4962
|
onButtonClick,
|
|
4382
4963
|
onDecline,
|
|
4964
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4383
4965
|
runBeforeButtonClick,
|
|
4384
4966
|
onLoadStateChange: setWalletLoadState,
|
|
4385
4967
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -4735,6 +5317,7 @@ function SplitCardFormInner({
|
|
|
4735
5317
|
onComplete,
|
|
4736
5318
|
onErrorChange: updateError,
|
|
4737
5319
|
onDecline,
|
|
5320
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4738
5321
|
onButtonClick,
|
|
4739
5322
|
runBeforeButtonClick,
|
|
4740
5323
|
isProcessing: isSubmitting,
|
|
@@ -4758,6 +5341,7 @@ function SplitCardFormInner({
|
|
|
4758
5341
|
isProcessing: isSubmitting,
|
|
4759
5342
|
onButtonClick,
|
|
4760
5343
|
onDecline,
|
|
5344
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4761
5345
|
runBeforeButtonClick,
|
|
4762
5346
|
onLoadStateChange: setPaypalLoadState,
|
|
4763
5347
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -4775,6 +5359,7 @@ function SplitCardFormInner({
|
|
|
4775
5359
|
onErrorChange: updateError,
|
|
4776
5360
|
onButtonClick,
|
|
4777
5361
|
onDecline,
|
|
5362
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4778
5363
|
runBeforeButtonClick,
|
|
4779
5364
|
onLoadStateChange: setWalletLoadState,
|
|
4780
5365
|
placeholderBorderRadius: buttonBorderRadius
|