@flopay/react 1.3.3 → 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 +11 -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 +3 -3
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1166
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2016
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
1607
2017
|
event.reject();
|
|
1608
2018
|
return;
|
|
1609
2019
|
}
|
|
1610
|
-
|
|
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 =
|
|
2030
|
+
}, [attemptContextBySurfaceRef, isProcessing, onButtonClick, runBeforeButtonClick, startAttempt, submitting, surfaceKey]);
|
|
2031
|
+
const handlePayPalConfirm = useCallback2(async (event) => {
|
|
1619
2032
|
if (!stripe || !elements) return;
|
|
1620
|
-
|
|
1621
|
-
|
|
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
|
-
|
|
1647
|
-
|
|
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
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
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
|
-
|
|
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__ */
|
|
1746
|
-
|
|
2170
|
+
children: /* @__PURE__ */ jsxs4(
|
|
2171
|
+
"div",
|
|
1747
2172
|
{
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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
|
|
1798
|
-
const
|
|
1799
|
-
const
|
|
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
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
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
|
-
|
|
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__ */
|
|
1915
|
-
|
|
2432
|
+
children: /* @__PURE__ */ jsxs4(
|
|
2433
|
+
"div",
|
|
1916
2434
|
{
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3254
|
+
const expandToCard = useCallback2(() => {
|
|
2700
3255
|
setViewState("expanding");
|
|
2701
3256
|
setTimeout(() => setViewState("card"), TRANSITION_MS);
|
|
2702
3257
|
}, []);
|
|
2703
|
-
const collapseToButtons =
|
|
3258
|
+
const collapseToButtons = useCallback2(() => {
|
|
2704
3259
|
setViewState("collapsing");
|
|
2705
3260
|
setTimeout(() => setViewState("buttons"), TRANSITION_MS);
|
|
2706
3261
|
}, []);
|
|
2707
|
-
const expandToApm =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
8398
|
+
const handleFallbackError = useCallback6((error) => {
|
|
7814
8399
|
onErrorRef.current?.(error);
|
|
7815
8400
|
}, []);
|
|
7816
|
-
const handleFallbackDecline =
|
|
8401
|
+
const handleFallbackDecline = useCallback6((decline) => {
|
|
7817
8402
|
onDeclineRef.current?.(decline);
|
|
7818
8403
|
}, []);
|
|
7819
8404
|
const themeBundle = useMemo5(() => resolveTheme3(theme), [theme]);
|