@flopay/react 1.3.3 → 1.4.0
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 +29 -0
- package/dist/index.cjs +1517 -193
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -10
- package/dist/index.d.ts +15 -10
- package/dist/index.mjs +1622 -298
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -69,6 +69,59 @@ var CheckoutContext = (0, import_react.createContext)({
|
|
|
69
69
|
error: null
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
// src/telemetry-bridge.ts
|
|
73
|
+
var FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
|
|
74
|
+
var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
|
|
75
|
+
function noopTelemetryBridge() {
|
|
76
|
+
return {
|
|
77
|
+
error: () => {
|
|
78
|
+
},
|
|
79
|
+
log: () => {
|
|
80
|
+
},
|
|
81
|
+
performance: () => {
|
|
82
|
+
},
|
|
83
|
+
terminal: () => {
|
|
84
|
+
},
|
|
85
|
+
now: () => globalThis.performance?.now() ?? 0,
|
|
86
|
+
elapsed: (startedAt) => Math.max(0, (globalThis.performance?.now() ?? startedAt) - startedAt),
|
|
87
|
+
setCheckoutContext: () => {
|
|
88
|
+
},
|
|
89
|
+
beginCheckout: () => globalThis.performance?.now() ?? 0,
|
|
90
|
+
disable: () => {
|
|
91
|
+
},
|
|
92
|
+
flush: async () => {
|
|
93
|
+
},
|
|
94
|
+
destroy: () => {
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function normalizeTelemetryBridge(source) {
|
|
99
|
+
const fallback = noopTelemetryBridge();
|
|
100
|
+
const bind = (candidate, defaultValue) => candidate ? candidate.bind(source) : defaultValue;
|
|
101
|
+
const now = bind(source.now, fallback.now);
|
|
102
|
+
return {
|
|
103
|
+
error: bind(source.error, fallback.error),
|
|
104
|
+
log: bind(source.log, fallback.log),
|
|
105
|
+
performance: bind(source.performance, fallback.performance),
|
|
106
|
+
terminal: bind(source.terminal, fallback.terminal),
|
|
107
|
+
now,
|
|
108
|
+
elapsed: source.elapsed ? source.elapsed.bind(source) : (startedAt) => Math.max(0, now() - startedAt),
|
|
109
|
+
setCheckoutContext: bind(source.setCheckoutContext, fallback.setCheckoutContext),
|
|
110
|
+
beginCheckout: bind(source.beginCheckout, fallback.beginCheckout),
|
|
111
|
+
disable: bind(source.disable, fallback.disable),
|
|
112
|
+
flush: bind(source.flush, fallback.flush),
|
|
113
|
+
destroy: bind(source.destroy, fallback.destroy)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function createTelemetryBridge(options) {
|
|
117
|
+
const factory = globalThis[TELEMETRY_REPORTER_FACTORY];
|
|
118
|
+
return normalizeTelemetryBridge(factory?.(options) ?? {});
|
|
119
|
+
}
|
|
120
|
+
function getFloPayTelemetryBridge(floPay) {
|
|
121
|
+
if (!floPay) return void 0;
|
|
122
|
+
return floPay[FLOPAY_TELEMETRY_BRIDGE];
|
|
123
|
+
}
|
|
124
|
+
|
|
72
125
|
// src/provider.tsx
|
|
73
126
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
74
127
|
function FloPayProvider({
|
|
@@ -84,6 +137,18 @@ function FloPayProvider({
|
|
|
84
137
|
paypalFloPayProp instanceof Promise || !paypalFloPayProp ? null : paypalFloPayProp
|
|
85
138
|
);
|
|
86
139
|
const [elements, setElements] = (0, import_react2.useState)(null);
|
|
140
|
+
const mountedAt = (0, import_react2.useRef)(null);
|
|
141
|
+
const renderedElements = (0, import_react2.useRef)(null);
|
|
142
|
+
const interactiveElements = (0, import_react2.useRef)(null);
|
|
143
|
+
(0, import_react2.useEffect)(() => {
|
|
144
|
+
if (!flopay) return;
|
|
145
|
+
const telemetry = getFloPayTelemetryBridge(flopay);
|
|
146
|
+
mountedAt.current = telemetry?.beginCheckout() ?? telemetry?.now() ?? 0;
|
|
147
|
+
telemetry?.log({ name: "checkout.mount", stage: "checkout_mount" });
|
|
148
|
+
return () => {
|
|
149
|
+
telemetry?.log({ name: "checkout.unmount", stage: "unmount" });
|
|
150
|
+
};
|
|
151
|
+
}, [flopay]);
|
|
87
152
|
(0, import_react2.useEffect)(() => {
|
|
88
153
|
let cancelled = false;
|
|
89
154
|
if (floPayProp instanceof Promise) {
|
|
@@ -144,10 +209,38 @@ function FloPayProvider({
|
|
|
144
209
|
options?.paymentMethodCreation,
|
|
145
210
|
options?.setupFutureUsage
|
|
146
211
|
]);
|
|
212
|
+
(0, import_react2.useEffect)(() => {
|
|
213
|
+
if (!flopay || !elements || renderedElements.current === elements) return;
|
|
214
|
+
renderedElements.current = elements;
|
|
215
|
+
const telemetry = getFloPayTelemetryBridge(flopay);
|
|
216
|
+
telemetry?.log({ name: "checkout.rendered", stage: "checkout_render" });
|
|
217
|
+
telemetry?.performance({
|
|
218
|
+
stage: "checkout_render",
|
|
219
|
+
durationMs: telemetry.elapsed(mountedAt.current ?? 0),
|
|
220
|
+
durationMode: "machine"
|
|
221
|
+
});
|
|
222
|
+
}, [elements, flopay]);
|
|
223
|
+
const reportInteractive = (0, import_react2.useCallback)(() => {
|
|
224
|
+
if (!flopay || !elements || interactiveElements.current === elements) return;
|
|
225
|
+
interactiveElements.current = elements;
|
|
226
|
+
const telemetry = getFloPayTelemetryBridge(flopay);
|
|
227
|
+
telemetry?.log({ name: "checkout.interactive", stage: "checkout_interactive" });
|
|
228
|
+
telemetry?.performance({
|
|
229
|
+
stage: "checkout_interactive",
|
|
230
|
+
durationMs: telemetry.elapsed(mountedAt.current ?? 0),
|
|
231
|
+
durationMode: "machine"
|
|
232
|
+
});
|
|
233
|
+
}, [elements, flopay]);
|
|
147
234
|
const resolvedBillingApiUrl = (0, import_shared.resolveBillingApiUrl)(options?.billingApiUrl);
|
|
148
235
|
const value = (0, import_react2.useMemo)(
|
|
149
|
-
() => ({
|
|
150
|
-
|
|
236
|
+
() => ({
|
|
237
|
+
flopay,
|
|
238
|
+
paypalFlopay,
|
|
239
|
+
elements,
|
|
240
|
+
billingApiUrl: resolvedBillingApiUrl,
|
|
241
|
+
reportInteractive
|
|
242
|
+
}),
|
|
243
|
+
[flopay, paypalFlopay, elements, resolvedBillingApiUrl, reportInteractive]
|
|
151
244
|
);
|
|
152
245
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FloPayContext.Provider, { value, children });
|
|
153
246
|
}
|
|
@@ -241,7 +334,7 @@ function createElementComponent(elementType, displayName) {
|
|
|
241
334
|
}) {
|
|
242
335
|
const containerRef = (0, import_react4.useRef)(null);
|
|
243
336
|
const elementRef = (0, import_react4.useRef)(null);
|
|
244
|
-
const { elements } = (0, import_react4.useContext)(FloPayContext);
|
|
337
|
+
const { elements, reportInteractive } = (0, import_react4.useContext)(FloPayContext);
|
|
245
338
|
(0, import_react4.useEffect)(() => {
|
|
246
339
|
if (!elements || !containerRef.current) return;
|
|
247
340
|
let mounted = true;
|
|
@@ -256,7 +349,10 @@ function createElementComponent(elementType, displayName) {
|
|
|
256
349
|
element.mount(containerRef.current);
|
|
257
350
|
elementRef.current = element;
|
|
258
351
|
if (onChange) element.on("change", onChange);
|
|
259
|
-
|
|
352
|
+
element.on("ready", () => {
|
|
353
|
+
reportInteractive?.();
|
|
354
|
+
onReady?.();
|
|
355
|
+
});
|
|
260
356
|
if (onFocus) element.on("focus", onFocus);
|
|
261
357
|
if (onBlur) element.on("blur", onBlur);
|
|
262
358
|
if (onEscape) element.on("escape", onEscape);
|
|
@@ -271,7 +367,7 @@ function createElementComponent(elementType, displayName) {
|
|
|
271
367
|
elementRef.current = null;
|
|
272
368
|
}
|
|
273
369
|
};
|
|
274
|
-
}, [elements]);
|
|
370
|
+
}, [elements, reportInteractive]);
|
|
275
371
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: containerRef, className, id, style });
|
|
276
372
|
}
|
|
277
373
|
ElementComponent.displayName = displayName;
|
|
@@ -802,9 +898,77 @@ var import_react8 = require("react");
|
|
|
802
898
|
var import_paypal_js = require("@paypal/paypal-js");
|
|
803
899
|
var import_js = require("@flopay/js");
|
|
804
900
|
var import_shared4 = require("@flopay/shared");
|
|
901
|
+
|
|
902
|
+
// src/merchant-callback.ts
|
|
903
|
+
function invokeMerchantCallback(callback) {
|
|
904
|
+
if (!callback) return;
|
|
905
|
+
const reportFailure = (error) => {
|
|
906
|
+
console.error("[FloPay] Merchant callback failed; checkout continued.", error);
|
|
907
|
+
};
|
|
908
|
+
try {
|
|
909
|
+
void Promise.resolve(callback()).catch(reportFailure);
|
|
910
|
+
} catch (error) {
|
|
911
|
+
reportFailure(error);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// src/external-method-recovery.ts
|
|
916
|
+
var EXTERNAL_METHOD_CALLBACK_GRACE_MS = 600;
|
|
917
|
+
function getProviderErrorMessage(err) {
|
|
918
|
+
if (err instanceof Error) return err.message;
|
|
919
|
+
if (typeof err === "object" && err !== null) {
|
|
920
|
+
const message = err.message;
|
|
921
|
+
return typeof message === "string" ? message : void 0;
|
|
922
|
+
}
|
|
923
|
+
return typeof err === "string" ? err : void 0;
|
|
924
|
+
}
|
|
925
|
+
function sanitizeExternalFailureCode(value) {
|
|
926
|
+
if (typeof value !== "string") return void 0;
|
|
927
|
+
const trimmed = value.trim();
|
|
928
|
+
return /^[a-z0-9_.-]{1,64}$/i.test(trimmed) ? trimmed : void 0;
|
|
929
|
+
}
|
|
930
|
+
function getProviderErrorCode(err) {
|
|
931
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
932
|
+
const record = err;
|
|
933
|
+
return sanitizeExternalFailureCode(record.code) ?? sanitizeExternalFailureCode(record.type) ?? sanitizeExternalFailureCode(record.name);
|
|
934
|
+
}
|
|
935
|
+
function getProviderDeclineCode(err) {
|
|
936
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
937
|
+
return sanitizeExternalFailureCode(err.decline_code);
|
|
938
|
+
}
|
|
939
|
+
function isProviderDecline(err) {
|
|
940
|
+
if (typeof err !== "object" || err === null) return false;
|
|
941
|
+
const type = sanitizeExternalFailureCode(err.type)?.toLowerCase();
|
|
942
|
+
return type === "card_error" || getProviderDeclineCode(err) !== void 0;
|
|
943
|
+
}
|
|
944
|
+
function isPopupBlockedError(err) {
|
|
945
|
+
const message = getProviderErrorMessage(err)?.toLowerCase() ?? "";
|
|
946
|
+
const code = getProviderErrorCode(err)?.toLowerCase() ?? "";
|
|
947
|
+
const signal = `${code} ${message}`;
|
|
948
|
+
return signal.includes("popup") && signal.includes("block");
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/direct-paypal-button.tsx
|
|
805
952
|
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
806
953
|
var DEFAULT_BUTTON_HEIGHT = 45;
|
|
807
|
-
|
|
954
|
+
var DIRECT_PAYPAL_RECOVERY_MESSAGE = "We couldn't open PayPal. Try again or choose another payment method.";
|
|
955
|
+
var DIRECT_PAYPAL_RECOVERY_ACTION_STYLE = {
|
|
956
|
+
width: "100%",
|
|
957
|
+
minHeight: DEFAULT_BUTTON_HEIGHT,
|
|
958
|
+
margin: "0 0 0.5rem",
|
|
959
|
+
padding: "0.75rem 0.875rem",
|
|
960
|
+
border: "1px solid #2563eb",
|
|
961
|
+
borderRadius: 8,
|
|
962
|
+
background: "#eff6ff",
|
|
963
|
+
color: "#1d4ed8",
|
|
964
|
+
fontSize: "0.95rem",
|
|
965
|
+
fontWeight: 700,
|
|
966
|
+
cursor: "pointer"
|
|
967
|
+
};
|
|
968
|
+
function buildDirectPayPalRecoveryMessage(popupBlocked) {
|
|
969
|
+
return popupBlocked ? `${DIRECT_PAYPAL_RECOVERY_MESSAGE} Allow pop-ups for this site, then try again.` : DIRECT_PAYPAL_RECOVERY_MESSAGE;
|
|
970
|
+
}
|
|
971
|
+
function DirectPayPalButtonImplementation({
|
|
808
972
|
sessionId,
|
|
809
973
|
nonce,
|
|
810
974
|
billingApiUrl,
|
|
@@ -817,16 +981,70 @@ function DirectPayPalButton({
|
|
|
817
981
|
onComplete,
|
|
818
982
|
onErrorChange,
|
|
819
983
|
onDecline,
|
|
984
|
+
onTechnicalFailure,
|
|
820
985
|
isProcessing = false,
|
|
821
986
|
onLoadStateChange,
|
|
822
987
|
onButtonClick,
|
|
823
988
|
runBeforeButtonClick,
|
|
824
989
|
session,
|
|
825
990
|
existingOrderId,
|
|
991
|
+
telemetry,
|
|
992
|
+
telemetryContext,
|
|
826
993
|
debug = false
|
|
827
994
|
}) {
|
|
995
|
+
const flopay = useFloPay();
|
|
996
|
+
const standaloneTelemetry = (0, import_react8.useMemo)(() => {
|
|
997
|
+
if (flopay) return null;
|
|
998
|
+
const reporter = createTelemetryBridge({
|
|
999
|
+
billingApiUrl,
|
|
1000
|
+
sdkPackage: "@flopay/react",
|
|
1001
|
+
sdkVersion: import_shared4.SDK_VERSION,
|
|
1002
|
+
enabled: telemetry !== false
|
|
1003
|
+
});
|
|
1004
|
+
reporter.setCheckoutContext(telemetryContext ?? {});
|
|
1005
|
+
reporter.beginCheckout(telemetryContext ?? {});
|
|
1006
|
+
return reporter;
|
|
1007
|
+
}, [
|
|
1008
|
+
billingApiUrl,
|
|
1009
|
+
flopay,
|
|
1010
|
+
telemetry,
|
|
1011
|
+
telemetryContext?.checkoutMode,
|
|
1012
|
+
telemetryContext?.layout
|
|
1013
|
+
]);
|
|
1014
|
+
const floPayTelemetry = (0, import_react8.useMemo)(() => getFloPayTelemetryBridge(flopay), [flopay]);
|
|
1015
|
+
(0, import_react8.useEffect)(() => () => {
|
|
1016
|
+
if (!standaloneTelemetry) return;
|
|
1017
|
+
void standaloneTelemetry.flush().catch(() => {
|
|
1018
|
+
}).finally(() => standaloneTelemetry.destroy());
|
|
1019
|
+
}, [standaloneTelemetry]);
|
|
1020
|
+
const telemetrySource = (0, import_react8.useMemo)(() => ({
|
|
1021
|
+
error: (input) => {
|
|
1022
|
+
if (floPayTelemetry) floPayTelemetry.error(input);
|
|
1023
|
+
else standaloneTelemetry?.error(input);
|
|
1024
|
+
},
|
|
1025
|
+
log: (input) => {
|
|
1026
|
+
if (floPayTelemetry) floPayTelemetry.log(input);
|
|
1027
|
+
else standaloneTelemetry?.log(input);
|
|
1028
|
+
},
|
|
1029
|
+
performance: (input) => {
|
|
1030
|
+
if (floPayTelemetry) floPayTelemetry.performance(input);
|
|
1031
|
+
else standaloneTelemetry?.performance(input);
|
|
1032
|
+
},
|
|
1033
|
+
terminal: (input) => {
|
|
1034
|
+
if (floPayTelemetry) floPayTelemetry.terminal(input);
|
|
1035
|
+
else standaloneTelemetry?.terminal(input);
|
|
1036
|
+
},
|
|
1037
|
+
startTiming: () => floPayTelemetry?.now() ?? standaloneTelemetry?.now() ?? 0,
|
|
1038
|
+
elapsed: (startedAt) => floPayTelemetry?.elapsed(startedAt) ?? Math.max(0, (standaloneTelemetry?.now() ?? startedAt) - startedAt)
|
|
1039
|
+
}), [floPayTelemetry, standaloneTelemetry]);
|
|
828
1040
|
const containerRef = (0, import_react8.useRef)(null);
|
|
1041
|
+
const providerStartedAt = (0, import_react8.useRef)(0);
|
|
1042
|
+
const focusTargetRef = (0, import_react8.useRef)(null);
|
|
1043
|
+
const pendingProviderFocusRef = (0, import_react8.useRef)(false);
|
|
829
1044
|
const [ready, setReady] = (0, import_react8.useState)(false);
|
|
1045
|
+
const [renderGeneration, setRenderGeneration] = (0, import_react8.useState)(0);
|
|
1046
|
+
const activeRenderGenerationRef = (0, import_react8.useRef)(0);
|
|
1047
|
+
const [showRetryFocusTarget, setShowRetryFocusTarget] = (0, import_react8.useState)(false);
|
|
830
1048
|
const [failed, setFailed] = (0, import_react8.useState)(false);
|
|
831
1049
|
const [submitting, setSubmitting] = (0, import_react8.useState)(false);
|
|
832
1050
|
const baseUrl = (0, import_react8.useMemo)(() => billingApiUrl.replace(/\/+$/, ""), [billingApiUrl]);
|
|
@@ -839,6 +1057,7 @@ function DirectPayPalButton({
|
|
|
839
1057
|
const onCompleteRef = (0, import_react8.useRef)(onComplete);
|
|
840
1058
|
const onErrorChangeRef = (0, import_react8.useRef)(onErrorChange);
|
|
841
1059
|
const onDeclineRef = (0, import_react8.useRef)(onDecline);
|
|
1060
|
+
const onTechnicalFailureRef = (0, import_react8.useRef)(onTechnicalFailure);
|
|
842
1061
|
const onButtonClickRef = (0, import_react8.useRef)(onButtonClick);
|
|
843
1062
|
const onLoadStateChangeRef = (0, import_react8.useRef)(onLoadStateChange);
|
|
844
1063
|
const runBeforeButtonClickRef = (0, import_react8.useRef)(runBeforeButtonClick);
|
|
@@ -846,6 +1065,11 @@ function DirectPayPalButton({
|
|
|
846
1065
|
const emailRef = (0, import_react8.useRef)(email);
|
|
847
1066
|
const nonceRef = (0, import_react8.useRef)(nonce);
|
|
848
1067
|
const beforeClickRef = (0, import_react8.useRef)(null);
|
|
1068
|
+
const attemptGenerationRef = (0, import_react8.useRef)(0);
|
|
1069
|
+
const attemptRef = (0, import_react8.useRef)(null);
|
|
1070
|
+
const invalidatedAttemptGenerationRef = (0, import_react8.useRef)(null);
|
|
1071
|
+
const attemptContextBySurfaceRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
|
|
1072
|
+
const approvalContextByTokenRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
|
|
849
1073
|
(0, import_react8.useEffect)(() => {
|
|
850
1074
|
onTokenizedBodyRef.current = onTokenizedBody;
|
|
851
1075
|
}, [onTokenizedBody]);
|
|
@@ -858,6 +1082,9 @@ function DirectPayPalButton({
|
|
|
858
1082
|
(0, import_react8.useEffect)(() => {
|
|
859
1083
|
onDeclineRef.current = onDecline;
|
|
860
1084
|
}, [onDecline]);
|
|
1085
|
+
(0, import_react8.useEffect)(() => {
|
|
1086
|
+
onTechnicalFailureRef.current = onTechnicalFailure;
|
|
1087
|
+
}, [onTechnicalFailure]);
|
|
861
1088
|
(0, import_react8.useEffect)(() => {
|
|
862
1089
|
onButtonClickRef.current = onButtonClick;
|
|
863
1090
|
}, [onButtonClick]);
|
|
@@ -877,7 +1104,130 @@ function DirectPayPalButton({
|
|
|
877
1104
|
nonceRef.current = nonce;
|
|
878
1105
|
}, [nonce]);
|
|
879
1106
|
(0, import_react8.useEffect)(() => {
|
|
880
|
-
|
|
1107
|
+
activeRenderGenerationRef.current = renderGeneration;
|
|
1108
|
+
}, [renderGeneration]);
|
|
1109
|
+
(0, import_react8.useEffect)(() => {
|
|
1110
|
+
if (showRetryFocusTarget) focusTargetRef.current?.focus();
|
|
1111
|
+
}, [showRetryFocusTarget, renderGeneration]);
|
|
1112
|
+
const focusRetryTarget = (0, import_react8.useCallback)(() => {
|
|
1113
|
+
window.setTimeout(() => {
|
|
1114
|
+
focusTargetRef.current?.focus();
|
|
1115
|
+
}, 0);
|
|
1116
|
+
}, []);
|
|
1117
|
+
const remountPayPalButtons = (0, import_react8.useCallback)(() => {
|
|
1118
|
+
setReady(false);
|
|
1119
|
+
setRenderGeneration((current) => current + 1);
|
|
1120
|
+
}, []);
|
|
1121
|
+
const focusPayPalSurface = (0, import_react8.useCallback)(() => {
|
|
1122
|
+
setShowRetryFocusTarget(false);
|
|
1123
|
+
const target = containerRef.current?.querySelector(
|
|
1124
|
+
'iframe, button, [tabindex]:not([tabindex="-1"])'
|
|
1125
|
+
);
|
|
1126
|
+
(target ?? containerRef.current)?.focus?.();
|
|
1127
|
+
}, []);
|
|
1128
|
+
(0, import_react8.useEffect)(() => {
|
|
1129
|
+
if (!ready || !pendingProviderFocusRef.current) return;
|
|
1130
|
+
pendingProviderFocusRef.current = false;
|
|
1131
|
+
focusPayPalSurface();
|
|
1132
|
+
}, [focusPayPalSurface, ready, renderGeneration]);
|
|
1133
|
+
const notifyTechnicalFailure = (0, import_react8.useCallback)((err, options) => {
|
|
1134
|
+
const handler = onTechnicalFailureRef.current;
|
|
1135
|
+
if (handler) {
|
|
1136
|
+
handler("paypal", err, options);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
onErrorChangeRef.current?.(
|
|
1140
|
+
buildDirectPayPalRecoveryMessage(options?.popupBlocked ?? isPopupBlockedError(err))
|
|
1141
|
+
);
|
|
1142
|
+
}, []);
|
|
1143
|
+
const clearAttemptTimer = () => {
|
|
1144
|
+
if (attemptRef.current?.timer) {
|
|
1145
|
+
clearTimeout(attemptRef.current.timer);
|
|
1146
|
+
attemptRef.current.timer = null;
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
const startAttempt = () => {
|
|
1150
|
+
clearAttemptTimer();
|
|
1151
|
+
const generation = attemptGenerationRef.current + 1;
|
|
1152
|
+
attemptGenerationRef.current = generation;
|
|
1153
|
+
invalidatedAttemptGenerationRef.current = null;
|
|
1154
|
+
attemptRef.current = { generation, timer: null, yieldedControl: false };
|
|
1155
|
+
setShowRetryFocusTarget(false);
|
|
1156
|
+
return generation;
|
|
1157
|
+
};
|
|
1158
|
+
const finishAttempt = (generation) => {
|
|
1159
|
+
if (generation !== void 0 && attemptRef.current?.generation !== generation) return false;
|
|
1160
|
+
clearAttemptTimer();
|
|
1161
|
+
attemptRef.current = null;
|
|
1162
|
+
invalidatedAttemptGenerationRef.current = null;
|
|
1163
|
+
return true;
|
|
1164
|
+
};
|
|
1165
|
+
const invalidateAttempt = (options) => {
|
|
1166
|
+
const generation = options?.generation ?? attemptRef.current?.generation ?? attemptGenerationRef.current;
|
|
1167
|
+
if (!options?.generation || attemptRef.current?.generation === generation) {
|
|
1168
|
+
clearAttemptTimer();
|
|
1169
|
+
attemptRef.current = null;
|
|
1170
|
+
}
|
|
1171
|
+
invalidatedAttemptGenerationRef.current = generation;
|
|
1172
|
+
if (options?.showRetry) setShowRetryFocusTarget(true);
|
|
1173
|
+
if (options?.remount) remountPayPalButtons();
|
|
1174
|
+
if (options?.focus !== false) focusRetryTarget();
|
|
1175
|
+
};
|
|
1176
|
+
const isAttemptActive = (generation) => attemptRef.current?.generation === generation && invalidatedAttemptGenerationRef.current !== generation;
|
|
1177
|
+
const armMissingCallbackRecovery = (attempt) => {
|
|
1178
|
+
if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
|
|
1179
|
+
clearAttemptTimer();
|
|
1180
|
+
attempt.timer = setTimeout(() => {
|
|
1181
|
+
if (attemptRef.current?.generation !== attempt.generation) return;
|
|
1182
|
+
attemptRef.current = null;
|
|
1183
|
+
invalidatedAttemptGenerationRef.current = attempt.generation;
|
|
1184
|
+
notifyTechnicalFailure(
|
|
1185
|
+
new Error("External payment method returned without a terminal callback."),
|
|
1186
|
+
{ code: "external_method_missing_terminal_callback" }
|
|
1187
|
+
);
|
|
1188
|
+
setShowRetryFocusTarget(true);
|
|
1189
|
+
remountPayPalButtons();
|
|
1190
|
+
focusRetryTarget();
|
|
1191
|
+
}, EXTERNAL_METHOD_CALLBACK_GRACE_MS);
|
|
1192
|
+
};
|
|
1193
|
+
const scheduleMissingCallbackRecovery = () => {
|
|
1194
|
+
const attempt = attemptRef.current;
|
|
1195
|
+
if (!attempt?.yieldedControl) return;
|
|
1196
|
+
armMissingCallbackRecovery(attempt);
|
|
1197
|
+
};
|
|
1198
|
+
const markAttemptYieldedControl = () => {
|
|
1199
|
+
const attempt = attemptRef.current;
|
|
1200
|
+
if (!attempt) return;
|
|
1201
|
+
attempt.yieldedControl = true;
|
|
1202
|
+
clearAttemptTimer();
|
|
1203
|
+
};
|
|
1204
|
+
const registerApprovalContext = (token) => {
|
|
1205
|
+
const context = beforeClickRef.current;
|
|
1206
|
+
if (context) {
|
|
1207
|
+
approvalContextByTokenRef.current.set(token, context);
|
|
1208
|
+
}
|
|
1209
|
+
return token;
|
|
1210
|
+
};
|
|
1211
|
+
(0, import_react8.useEffect)(() => {
|
|
1212
|
+
const handleVisibilityChange = () => {
|
|
1213
|
+
if (document.visibilityState === "visible") {
|
|
1214
|
+
scheduleMissingCallbackRecovery();
|
|
1215
|
+
} else {
|
|
1216
|
+
markAttemptYieldedControl();
|
|
1217
|
+
}
|
|
1218
|
+
};
|
|
1219
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
1220
|
+
window.addEventListener("focus", scheduleMissingCallbackRecovery);
|
|
1221
|
+
window.addEventListener("blur", markAttemptYieldedControl);
|
|
1222
|
+
return () => {
|
|
1223
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
1224
|
+
window.removeEventListener("focus", scheduleMissingCallbackRecovery);
|
|
1225
|
+
window.removeEventListener("blur", markAttemptYieldedControl);
|
|
1226
|
+
clearAttemptTimer();
|
|
1227
|
+
};
|
|
1228
|
+
}, []);
|
|
1229
|
+
(0, import_react8.useEffect)(() => {
|
|
1230
|
+
invokeMerchantCallback(() => onLoadStateChangeRef.current?.(ready && !failed));
|
|
881
1231
|
}, [ready, failed]);
|
|
882
1232
|
const normalizedEnv = (0, import_shared4.normalizeGatewayEnvironment)(environment);
|
|
883
1233
|
(0, import_react8.useEffect)(() => {
|
|
@@ -888,6 +1238,13 @@ function DirectPayPalButton({
|
|
|
888
1238
|
if (!clientId) {
|
|
889
1239
|
appendDebug("FAIL: clientId empty \u2014 gateway misconfigured");
|
|
890
1240
|
setFailed(true);
|
|
1241
|
+
telemetrySource.error({
|
|
1242
|
+
errorCode: "CONFIGURATION_INVALID",
|
|
1243
|
+
stage: "provider_load",
|
|
1244
|
+
provider: "paypal",
|
|
1245
|
+
paymentMethodCategory: "paypal",
|
|
1246
|
+
requestCategory: "provider_sdk"
|
|
1247
|
+
});
|
|
891
1248
|
console.error("[FloPay] DirectPayPal: clientId empty \u2014 gateway misconfigured");
|
|
892
1249
|
return;
|
|
893
1250
|
}
|
|
@@ -895,8 +1252,33 @@ function DirectPayPalButton({
|
|
|
895
1252
|
appendDebug("FAIL: containerRef not attached");
|
|
896
1253
|
return;
|
|
897
1254
|
}
|
|
1255
|
+
providerStartedAt.current = telemetrySource.startTiming();
|
|
1256
|
+
telemetrySource.log({
|
|
1257
|
+
name: "provider.load.started",
|
|
1258
|
+
stage: "provider_load",
|
|
1259
|
+
provider: "paypal",
|
|
1260
|
+
paymentMethodCategory: "paypal"
|
|
1261
|
+
});
|
|
898
1262
|
let cancelled = false;
|
|
899
1263
|
let activeButtons = null;
|
|
1264
|
+
let overlayStartedAt = null;
|
|
1265
|
+
const finishOverlay = () => {
|
|
1266
|
+
if (overlayStartedAt === null) return;
|
|
1267
|
+
telemetrySource.log({
|
|
1268
|
+
name: "provider.overlay.returned",
|
|
1269
|
+
stage: "overlay_return",
|
|
1270
|
+
provider: "paypal",
|
|
1271
|
+
paymentMethodCategory: "paypal"
|
|
1272
|
+
});
|
|
1273
|
+
telemetrySource.performance({
|
|
1274
|
+
stage: "overlay_return",
|
|
1275
|
+
durationMs: telemetrySource.elapsed(overlayStartedAt),
|
|
1276
|
+
durationMode: "buyer",
|
|
1277
|
+
provider: "paypal",
|
|
1278
|
+
paymentMethodCategory: "paypal"
|
|
1279
|
+
});
|
|
1280
|
+
overlayStartedAt = null;
|
|
1281
|
+
};
|
|
900
1282
|
let rendered = false;
|
|
901
1283
|
const container = containerRef.current;
|
|
902
1284
|
let containerObserver = null;
|
|
@@ -1000,7 +1382,7 @@ function DirectPayPalButton({
|
|
|
1000
1382
|
if (cancelled) return;
|
|
1001
1383
|
if (isZoidLifecycleMessage(message)) return;
|
|
1002
1384
|
const friendly = applyFriendlyMessageOverride(message) ?? message;
|
|
1003
|
-
onErrorChangeRef.current?.(friendly);
|
|
1385
|
+
invokeMerchantCallback(() => onErrorChangeRef.current?.(friendly));
|
|
1004
1386
|
};
|
|
1005
1387
|
const markRenderFailed = (message) => {
|
|
1006
1388
|
if (cancelled) return;
|
|
@@ -1009,29 +1391,60 @@ function DirectPayPalButton({
|
|
|
1009
1391
|
return;
|
|
1010
1392
|
}
|
|
1011
1393
|
setFailed(true);
|
|
1394
|
+
if (!message.includes("paypal_ineligible")) {
|
|
1395
|
+
telemetrySource.error({
|
|
1396
|
+
errorCode: "PROVIDER_LOAD_FAILED",
|
|
1397
|
+
stage: "provider_load",
|
|
1398
|
+
provider: "paypal",
|
|
1399
|
+
paymentMethodCategory: "paypal",
|
|
1400
|
+
requestCategory: "provider_sdk"
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1012
1403
|
console.error("[FloPay] DirectPayPal load/render failure:", message);
|
|
1013
1404
|
};
|
|
1014
1405
|
setFailed(false);
|
|
1015
|
-
const dispatchTokenizedBody = async (body) => {
|
|
1016
|
-
const prepared = beforeClickRef.current;
|
|
1406
|
+
const dispatchTokenizedBody = async (body, prepared = beforeClickRef.current) => {
|
|
1017
1407
|
const effectiveSessionId = prepared?.sessionId ?? sessionId;
|
|
1018
1408
|
if (onTokenizedBodyRef.current) {
|
|
1019
|
-
onTokenizedBodyRef.current(body, {
|
|
1409
|
+
await onTokenizedBodyRef.current(body, {
|
|
1020
1410
|
sessionId: effectiveSessionId,
|
|
1021
|
-
accountPatch: prepared?.accountPatch
|
|
1411
|
+
accountPatch: prepared?.accountPatch,
|
|
1412
|
+
nonce: prepared?.nonce
|
|
1022
1413
|
});
|
|
1023
1414
|
return;
|
|
1024
1415
|
}
|
|
1416
|
+
const processingStartedAt = telemetrySource.startTiming();
|
|
1417
|
+
telemetrySource.log({
|
|
1418
|
+
name: "payment.processing.started",
|
|
1419
|
+
stage: "processing",
|
|
1420
|
+
provider: "paypal",
|
|
1421
|
+
paymentMethodCategory: "paypal"
|
|
1422
|
+
});
|
|
1423
|
+
const finishProcessing = () => {
|
|
1424
|
+
telemetrySource.log({
|
|
1425
|
+
name: "payment.processing.completed",
|
|
1426
|
+
stage: "processing",
|
|
1427
|
+
provider: "paypal",
|
|
1428
|
+
paymentMethodCategory: "paypal"
|
|
1429
|
+
});
|
|
1430
|
+
telemetrySource.performance({
|
|
1431
|
+
stage: "processing",
|
|
1432
|
+
durationMs: telemetrySource.elapsed(processingStartedAt),
|
|
1433
|
+
durationMode: "machine",
|
|
1434
|
+
provider: "paypal",
|
|
1435
|
+
paymentMethodCategory: "paypal"
|
|
1436
|
+
});
|
|
1437
|
+
};
|
|
1025
1438
|
try {
|
|
1026
1439
|
const currentSession = sessionRef.current;
|
|
1027
1440
|
const currentEmail = prepared?.accountPatch?.email ?? emailRef.current;
|
|
1028
1441
|
const effectiveUserId = prepared?.accountPatch?.userId ?? currentSession?.customer?.id ?? currentSession?.accountData?.userId ?? "";
|
|
1029
|
-
const api = new import_js.PaymentAPI(baseUrl);
|
|
1442
|
+
const api = new import_js.PaymentAPI(baseUrl, { telemetry: false });
|
|
1030
1443
|
const response = await api.processPayment(
|
|
1031
1444
|
effectiveUserId,
|
|
1032
1445
|
{
|
|
1033
1446
|
sessionId: effectiveSessionId,
|
|
1034
|
-
nonce: nonceRef.current,
|
|
1447
|
+
nonce: prepared?.nonce ?? nonceRef.current,
|
|
1035
1448
|
tokenizedData: body,
|
|
1036
1449
|
accountData: {
|
|
1037
1450
|
userId: effectiveUserId,
|
|
@@ -1044,31 +1457,72 @@ function DirectPayPalButton({
|
|
|
1044
1457
|
}
|
|
1045
1458
|
);
|
|
1046
1459
|
if (response.ok) {
|
|
1047
|
-
|
|
1460
|
+
finishProcessing();
|
|
1461
|
+
telemetrySource.terminal({
|
|
1462
|
+
outcome: "payment_succeeded",
|
|
1463
|
+
provider: "paypal",
|
|
1464
|
+
paymentMethodCategory: "paypal"
|
|
1465
|
+
});
|
|
1466
|
+
} else {
|
|
1467
|
+
const json = await response.json().catch(() => null);
|
|
1468
|
+
const rawMessage = json?.["message"] ?? "PayPal payment failed.";
|
|
1469
|
+
const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
|
|
1470
|
+
finishProcessing();
|
|
1471
|
+
if (typeof json?.["declineCode"] === "string") {
|
|
1472
|
+
telemetrySource.terminal({
|
|
1473
|
+
outcome: "payment_declined",
|
|
1474
|
+
provider: "paypal",
|
|
1475
|
+
paymentMethodCategory: "paypal"
|
|
1476
|
+
});
|
|
1477
|
+
} else {
|
|
1478
|
+
telemetrySource.error({
|
|
1479
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
1480
|
+
stage: "processing",
|
|
1481
|
+
provider: "paypal",
|
|
1482
|
+
paymentMethodCategory: "paypal",
|
|
1483
|
+
requestCategory: "process_payment",
|
|
1484
|
+
statusClass: response.status >= 500 ? "5xx" : response.status >= 400 ? "4xx" : response.status >= 300 ? "3xx" : "unknown"
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
forwardError(message);
|
|
1488
|
+
invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
|
|
1489
|
+
code: json?.["code"],
|
|
1490
|
+
declineCode: json?.["declineCode"]
|
|
1491
|
+
})));
|
|
1048
1492
|
return;
|
|
1049
1493
|
}
|
|
1050
|
-
const json = await response.json().catch(() => null);
|
|
1051
|
-
const rawMessage = json?.["message"] ?? "PayPal payment failed.";
|
|
1052
|
-
const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
|
|
1053
|
-
forwardError(message);
|
|
1054
|
-
onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
|
|
1055
|
-
code: json?.["code"],
|
|
1056
|
-
declineCode: json?.["declineCode"]
|
|
1057
|
-
}));
|
|
1058
1494
|
} catch (err) {
|
|
1495
|
+
finishProcessing();
|
|
1496
|
+
telemetrySource.error({
|
|
1497
|
+
errorCode: "NETWORK_REQUEST_FAILED",
|
|
1498
|
+
stage: "processing",
|
|
1499
|
+
provider: "paypal",
|
|
1500
|
+
paymentMethodCategory: "paypal",
|
|
1501
|
+
requestCategory: "process_payment",
|
|
1502
|
+
statusClass: "network_error"
|
|
1503
|
+
});
|
|
1059
1504
|
const rawMessage = err instanceof Error ? err.message : "PayPal payment failed.";
|
|
1060
1505
|
const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
|
|
1061
1506
|
forwardError(message);
|
|
1062
|
-
onDeclineRef.current?.(buildDeclineEvent("paypal", message));
|
|
1507
|
+
invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message)));
|
|
1508
|
+
return;
|
|
1063
1509
|
}
|
|
1510
|
+
invokeMerchantCallback(() => onCompleteRef.current?.({ status: "succeeded", checkoutMethod: "paypal" }));
|
|
1064
1511
|
};
|
|
1065
1512
|
const createPaypalIntent = async (fallbackMessage) => {
|
|
1066
1513
|
const prepared = beforeClickRef.current;
|
|
1067
1514
|
const effectiveSessionId = prepared?.sessionId ?? sessionId;
|
|
1068
1515
|
const effectiveEmail = prepared?.accountPatch?.email ?? emailRef.current;
|
|
1069
1516
|
const intentHeaders = { "Content-Type": "application/json" };
|
|
1070
|
-
const currentNonce = nonceRef.current;
|
|
1517
|
+
const currentNonce = prepared?.nonce ?? nonceRef.current;
|
|
1071
1518
|
if (currentNonce) intentHeaders["x-checkout-session-token"] = currentNonce;
|
|
1519
|
+
telemetrySource.log({
|
|
1520
|
+
name: "payment.intent.started",
|
|
1521
|
+
stage: "processing",
|
|
1522
|
+
provider: "paypal",
|
|
1523
|
+
paymentMethodCategory: "paypal",
|
|
1524
|
+
requestCategory: "intent_create"
|
|
1525
|
+
});
|
|
1072
1526
|
const response = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
1073
1527
|
method: "POST",
|
|
1074
1528
|
headers: intentHeaders,
|
|
@@ -1087,8 +1541,17 @@ function DirectPayPalButton({
|
|
|
1087
1541
|
if (!id) {
|
|
1088
1542
|
throw new Error(fallbackMessage);
|
|
1089
1543
|
}
|
|
1544
|
+
telemetrySource.log({
|
|
1545
|
+
name: "payment.intent.completed",
|
|
1546
|
+
stage: "processing",
|
|
1547
|
+
provider: "paypal",
|
|
1548
|
+
paymentMethodCategory: "paypal",
|
|
1549
|
+
requestCategory: "intent_create",
|
|
1550
|
+
statusClass: "2xx"
|
|
1551
|
+
});
|
|
1090
1552
|
return id;
|
|
1091
1553
|
};
|
|
1554
|
+
const effectRenderGeneration = renderGeneration;
|
|
1092
1555
|
appendDebug("loadScript:scheduled (deferred 1 tick)");
|
|
1093
1556
|
let loadPromise = null;
|
|
1094
1557
|
const startTimer = setTimeout(() => {
|
|
@@ -1120,11 +1583,24 @@ function DirectPayPalButton({
|
|
|
1120
1583
|
if (!cancelled && !paypal?.Buttons) appendDebug("FAIL: namespace missing Buttons factory");
|
|
1121
1584
|
return;
|
|
1122
1585
|
}
|
|
1586
|
+
telemetrySource.log({
|
|
1587
|
+
name: "provider.availability.checked",
|
|
1588
|
+
stage: "provider_ready",
|
|
1589
|
+
provider: "paypal",
|
|
1590
|
+
paymentMethodCategory: "paypal"
|
|
1591
|
+
});
|
|
1123
1592
|
const handleApprove = async (data) => {
|
|
1593
|
+
if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
|
|
1594
|
+
const token = data.subscriptionID ?? data.orderID ?? "";
|
|
1595
|
+
const context = token ? approvalContextByTokenRef.current.get(token) : attemptContextBySurfaceRef.current.get(effectRenderGeneration);
|
|
1596
|
+
if (!context || !isAttemptActive(context.generation)) return;
|
|
1597
|
+
if (!finishAttempt(context.generation)) return;
|
|
1598
|
+
approvalContextByTokenRef.current.delete(token);
|
|
1599
|
+
attemptContextBySurfaceRef.current.delete(context.surfaceKey);
|
|
1600
|
+
finishOverlay();
|
|
1124
1601
|
try {
|
|
1125
1602
|
setSubmitting(true);
|
|
1126
|
-
onErrorChangeRef.current?.(null);
|
|
1127
|
-
const token = data.subscriptionID ?? data.orderID ?? "";
|
|
1603
|
+
invokeMerchantCallback(() => onErrorChangeRef.current?.(null));
|
|
1128
1604
|
if (!token) {
|
|
1129
1605
|
throw new import_shared4.FloPayError(
|
|
1130
1606
|
"PayPal did not return an approval token.",
|
|
@@ -1135,7 +1611,7 @@ function DirectPayPalButton({
|
|
|
1135
1611
|
await dispatchTokenizedBody({
|
|
1136
1612
|
id: token,
|
|
1137
1613
|
isPaypal: true
|
|
1138
|
-
});
|
|
1614
|
+
}, context);
|
|
1139
1615
|
} catch (err) {
|
|
1140
1616
|
forwardError(err instanceof Error ? err.message : "PayPal capture failed.");
|
|
1141
1617
|
} finally {
|
|
@@ -1151,47 +1627,104 @@ function DirectPayPalButton({
|
|
|
1151
1627
|
// matching the Stripe-rendered PayPal flow.
|
|
1152
1628
|
onClick: async (_data, actions) => {
|
|
1153
1629
|
const runner = runBeforeButtonClickRef.current;
|
|
1630
|
+
let prepared = {};
|
|
1154
1631
|
if (runner) {
|
|
1155
1632
|
try {
|
|
1156
1633
|
const beforeClick = await runner("paypal");
|
|
1157
1634
|
if (!beforeClick.proceed) {
|
|
1158
1635
|
beforeClickRef.current = null;
|
|
1636
|
+
attemptContextBySurfaceRef.current.delete(effectRenderGeneration);
|
|
1159
1637
|
await actions.reject();
|
|
1160
1638
|
return;
|
|
1161
1639
|
}
|
|
1162
|
-
|
|
1640
|
+
prepared = {
|
|
1163
1641
|
sessionId: beforeClick.sessionId,
|
|
1164
|
-
accountPatch: beforeClick.accountPatch
|
|
1642
|
+
accountPatch: beforeClick.accountPatch,
|
|
1643
|
+
nonce: beforeClick.nonce
|
|
1165
1644
|
};
|
|
1166
1645
|
} catch (err) {
|
|
1167
1646
|
appendDebug(`onClick:runBeforeButtonClick rejected msg=${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`);
|
|
1168
1647
|
beforeClickRef.current = null;
|
|
1648
|
+
attemptContextBySurfaceRef.current.delete(effectRenderGeneration);
|
|
1169
1649
|
await actions.reject();
|
|
1170
1650
|
return;
|
|
1171
1651
|
}
|
|
1172
|
-
} else {
|
|
1173
|
-
beforeClickRef.current = null;
|
|
1174
1652
|
}
|
|
1175
|
-
onButtonClickRef.current?.("paypal");
|
|
1653
|
+
invokeMerchantCallback(() => onButtonClickRef.current?.("paypal"));
|
|
1654
|
+
const generation = startAttempt();
|
|
1655
|
+
const context = {
|
|
1656
|
+
generation,
|
|
1657
|
+
surfaceKey: effectRenderGeneration,
|
|
1658
|
+
...prepared
|
|
1659
|
+
};
|
|
1660
|
+
beforeClickRef.current = context;
|
|
1661
|
+
attemptContextBySurfaceRef.current.set(effectRenderGeneration, context);
|
|
1176
1662
|
await actions.resolve();
|
|
1663
|
+
telemetrySource.log({
|
|
1664
|
+
name: "payment.method.selected",
|
|
1665
|
+
stage: "processing",
|
|
1666
|
+
provider: "paypal",
|
|
1667
|
+
paymentMethodCategory: "paypal"
|
|
1668
|
+
});
|
|
1669
|
+
telemetrySource.log({
|
|
1670
|
+
name: "provider.popup.opened",
|
|
1671
|
+
stage: "overlay_open",
|
|
1672
|
+
provider: "paypal",
|
|
1673
|
+
paymentMethodCategory: "paypal"
|
|
1674
|
+
});
|
|
1675
|
+
telemetrySource.log({
|
|
1676
|
+
name: "provider.overlay.opened",
|
|
1677
|
+
stage: "overlay_open",
|
|
1678
|
+
provider: "paypal",
|
|
1679
|
+
paymentMethodCategory: "paypal"
|
|
1680
|
+
});
|
|
1681
|
+
overlayStartedAt = telemetrySource.startTiming();
|
|
1177
1682
|
},
|
|
1178
1683
|
// When the SDK is already holding an order/subscription id from a
|
|
1179
1684
|
// prior backend round-trip (the `paypal_direct_required` retry
|
|
1180
1685
|
// path), feed it straight to PayPal instead of creating a new one.
|
|
1181
1686
|
// 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,
|
|
1687
|
+
createOrder: isSubscription ? void 0 : existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal order.").then(registerApprovalContext),
|
|
1688
|
+
createSubscription: isSubscription ? existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal subscription.").then(registerApprovalContext) : void 0,
|
|
1184
1689
|
onApprove: handleApprove,
|
|
1185
1690
|
onCancel: () => {
|
|
1691
|
+
const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
|
|
1692
|
+
if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
|
|
1186
1693
|
beforeClickRef.current = null;
|
|
1187
|
-
|
|
1694
|
+
finishOverlay();
|
|
1695
|
+
telemetrySource.terminal({
|
|
1696
|
+
outcome: "payment_cancelled",
|
|
1697
|
+
provider: "paypal",
|
|
1698
|
+
paymentMethodCategory: "paypal"
|
|
1699
|
+
});
|
|
1700
|
+
pendingProviderFocusRef.current = true;
|
|
1701
|
+
invalidateAttempt({ generation: context?.generation, remount: true, focus: false });
|
|
1188
1702
|
},
|
|
1189
1703
|
onError: (err) => {
|
|
1704
|
+
if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
|
|
1190
1705
|
const message = err instanceof Error ? err.message : "PayPal failed to render.";
|
|
1191
1706
|
appendDebug(`onError rendered=${rendered} msg=${message.slice(0, 120)}`);
|
|
1192
1707
|
if (rendered) {
|
|
1193
|
-
|
|
1194
|
-
|
|
1708
|
+
const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
|
|
1709
|
+
if (context && !isAttemptActive(context.generation)) return;
|
|
1710
|
+
if (!context && invalidatedAttemptGenerationRef.current === attemptGenerationRef.current) return;
|
|
1711
|
+
if (isZoidLifecycleMessage(message)) {
|
|
1712
|
+
finishAttempt(context?.generation);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
finishOverlay();
|
|
1716
|
+
const lower = message.toLowerCase();
|
|
1717
|
+
telemetrySource.error({
|
|
1718
|
+
errorCode: lower.includes("popup") && lower.includes("block") ? "POPUP_BLOCKED" : "PROVIDER_RUNTIME_FAILED",
|
|
1719
|
+
stage: "provider_ready",
|
|
1720
|
+
provider: "paypal",
|
|
1721
|
+
paymentMethodCategory: "paypal",
|
|
1722
|
+
requestCategory: "provider_sdk"
|
|
1723
|
+
});
|
|
1724
|
+
if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
|
|
1725
|
+
beforeClickRef.current = null;
|
|
1726
|
+
invalidateAttempt({ generation: context?.generation, remount: true, showRetry: true, focus: true });
|
|
1727
|
+
notifyTechnicalFailure(err, { code: "paypal_runtime_failed" });
|
|
1195
1728
|
return;
|
|
1196
1729
|
}
|
|
1197
1730
|
markRenderFailed(message);
|
|
@@ -1199,6 +1732,12 @@ function DirectPayPalButton({
|
|
|
1199
1732
|
});
|
|
1200
1733
|
const eligible = buttons.isEligible();
|
|
1201
1734
|
appendDebug(`isEligible=${eligible}`);
|
|
1735
|
+
telemetrySource.log({
|
|
1736
|
+
name: "provider.eligibility.checked",
|
|
1737
|
+
stage: "provider_ready",
|
|
1738
|
+
provider: "paypal",
|
|
1739
|
+
paymentMethodCategory: "paypal"
|
|
1740
|
+
});
|
|
1202
1741
|
if (!eligible) {
|
|
1203
1742
|
setReady(false);
|
|
1204
1743
|
markRenderFailed(
|
|
@@ -1270,6 +1809,19 @@ function DirectPayPalButton({
|
|
|
1270
1809
|
activeButtons = typedButtons;
|
|
1271
1810
|
rendered = true;
|
|
1272
1811
|
setReady(true);
|
|
1812
|
+
telemetrySource.log({
|
|
1813
|
+
name: "provider.ready",
|
|
1814
|
+
stage: "provider_ready",
|
|
1815
|
+
provider: "paypal",
|
|
1816
|
+
paymentMethodCategory: "paypal"
|
|
1817
|
+
});
|
|
1818
|
+
telemetrySource.performance({
|
|
1819
|
+
stage: "provider_ready",
|
|
1820
|
+
durationMs: telemetrySource.elapsed(providerStartedAt.current),
|
|
1821
|
+
durationMode: "machine",
|
|
1822
|
+
provider: "paypal",
|
|
1823
|
+
paymentMethodCategory: "paypal"
|
|
1824
|
+
});
|
|
1273
1825
|
}).catch((err) => {
|
|
1274
1826
|
const message = err instanceof Error ? err.message : "PayPal failed to render.";
|
|
1275
1827
|
appendDebug(`render:rejected msg=${message.slice(0, 120)}`);
|
|
@@ -1291,7 +1843,7 @@ function DirectPayPalButton({
|
|
|
1291
1843
|
});
|
|
1292
1844
|
}
|
|
1293
1845
|
};
|
|
1294
|
-
}, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId]);
|
|
1846
|
+
}, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration, telemetrySource]);
|
|
1295
1847
|
const debugPanel = debug ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
|
|
1296
1848
|
"pre",
|
|
1297
1849
|
{
|
|
@@ -1346,18 +1898,38 @@ ${debugLines.join("\n")}`
|
|
|
1346
1898
|
{
|
|
1347
1899
|
ref: containerRef,
|
|
1348
1900
|
"data-testid": "flopay-direct-paypal-container",
|
|
1901
|
+
tabIndex: -1,
|
|
1902
|
+
"aria-label": "PayPal payment method",
|
|
1349
1903
|
style: {
|
|
1350
1904
|
minHeight: DEFAULT_BUTTON_HEIGHT,
|
|
1351
1905
|
display: "flex",
|
|
1352
1906
|
opacity: ready ? 1 : 0
|
|
1353
1907
|
},
|
|
1354
1908
|
"aria-busy": submitting || isProcessing
|
|
1355
|
-
}
|
|
1909
|
+
},
|
|
1910
|
+
renderGeneration
|
|
1356
1911
|
)
|
|
1357
|
-
] })
|
|
1912
|
+
] }),
|
|
1913
|
+
showRetryFocusTarget && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
1914
|
+
"button",
|
|
1915
|
+
{
|
|
1916
|
+
ref: focusTargetRef,
|
|
1917
|
+
type: "button",
|
|
1918
|
+
"data-testid": "flopay-direct-paypal-focus-target",
|
|
1919
|
+
onClick: focusPayPalSurface,
|
|
1920
|
+
style: DIRECT_PAYPAL_RECOVERY_ACTION_STYLE,
|
|
1921
|
+
children: "Try PayPal again"
|
|
1922
|
+
}
|
|
1923
|
+
)
|
|
1358
1924
|
] })
|
|
1359
1925
|
);
|
|
1360
1926
|
}
|
|
1927
|
+
function DirectPayPalButton(props) {
|
|
1928
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DirectPayPalButtonImplementation, { ...props });
|
|
1929
|
+
}
|
|
1930
|
+
function InstrumentedDirectPayPalButton(props) {
|
|
1931
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DirectPayPalButtonImplementation, { ...props });
|
|
1932
|
+
}
|
|
1361
1933
|
|
|
1362
1934
|
// src/split-card-form.tsx
|
|
1363
1935
|
var import_shared6 = require("@flopay/shared");
|
|
@@ -1438,6 +2010,19 @@ var BUTTONS_PANEL_HIDDEN_STYLE = {
|
|
|
1438
2010
|
height: 0,
|
|
1439
2011
|
overflow: "hidden"
|
|
1440
2012
|
};
|
|
2013
|
+
var EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE = {
|
|
2014
|
+
width: "100%",
|
|
2015
|
+
minHeight: DEFAULT_BUTTONS_LAYOUT_BUTTON_HEIGHT,
|
|
2016
|
+
margin: "0 0 0.5rem",
|
|
2017
|
+
padding: "0.75rem 0.875rem",
|
|
2018
|
+
border: "1px solid #2563eb",
|
|
2019
|
+
borderRadius: 8,
|
|
2020
|
+
background: "#eff6ff",
|
|
2021
|
+
color: "#1d4ed8",
|
|
2022
|
+
fontSize: "0.95rem",
|
|
2023
|
+
fontWeight: 700,
|
|
2024
|
+
cursor: "pointer"
|
|
2025
|
+
};
|
|
1441
2026
|
function getButtonMethodLabel(method) {
|
|
1442
2027
|
switch (method) {
|
|
1443
2028
|
case "paypal":
|
|
@@ -1450,6 +2035,118 @@ function getButtonMethodLabel(method) {
|
|
|
1450
2035
|
return "Card";
|
|
1451
2036
|
}
|
|
1452
2037
|
}
|
|
2038
|
+
function checkoutButtonMethodFromProviderMethod(method) {
|
|
2039
|
+
if (method === "paypal") return "paypal";
|
|
2040
|
+
if (method === "apple_pay") return "apple_pay";
|
|
2041
|
+
return "google_pay";
|
|
2042
|
+
}
|
|
2043
|
+
function getExternalMethodDisplayName(method) {
|
|
2044
|
+
return method === "paypal" ? "PayPal" : (0, import_shared5.getStripeMethodDisplayName)(method);
|
|
2045
|
+
}
|
|
2046
|
+
function buildExternalMethodRecoveryMessage(method, popupBlocked) {
|
|
2047
|
+
const base = `We couldn't open ${getExternalMethodDisplayName(method)}. Try again or choose another payment method.`;
|
|
2048
|
+
return popupBlocked ? `${base} Allow pop-ups for this site, then try again.` : base;
|
|
2049
|
+
}
|
|
2050
|
+
function useExternalAttemptReconciliation(onMissingTerminal) {
|
|
2051
|
+
const generationRef = (0, import_react9.useRef)(0);
|
|
2052
|
+
const invalidatedGenerationRef = (0, import_react9.useRef)(null);
|
|
2053
|
+
const attemptRef = (0, import_react9.useRef)(null);
|
|
2054
|
+
const clearAttemptTimer = (0, import_react9.useCallback)((targetAttempt = attemptRef.current) => {
|
|
2055
|
+
const attempt = targetAttempt;
|
|
2056
|
+
if (attempt?.timer) {
|
|
2057
|
+
clearTimeout(attempt.timer);
|
|
2058
|
+
attempt.timer = null;
|
|
2059
|
+
}
|
|
2060
|
+
}, []);
|
|
2061
|
+
const armAttemptTimer = (0, import_react9.useCallback)((attempt, delayMs) => {
|
|
2062
|
+
if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
|
|
2063
|
+
clearAttemptTimer(attempt);
|
|
2064
|
+
attempt.timer = setTimeout(() => {
|
|
2065
|
+
attempt.timer = null;
|
|
2066
|
+
if (attemptRef.current?.generation !== attempt.generation) return;
|
|
2067
|
+
attemptRef.current = null;
|
|
2068
|
+
invalidatedGenerationRef.current = attempt.generation;
|
|
2069
|
+
onMissingTerminal(
|
|
2070
|
+
attempt.method,
|
|
2071
|
+
new Error("External payment method returned without a terminal callback."),
|
|
2072
|
+
{ code: "external_method_missing_terminal_callback" }
|
|
2073
|
+
);
|
|
2074
|
+
}, delayMs);
|
|
2075
|
+
}, [clearAttemptTimer, onMissingTerminal]);
|
|
2076
|
+
const armRecoveryTimer = (0, import_react9.useCallback)((attempt) => {
|
|
2077
|
+
if (!attempt.yieldedControl) return;
|
|
2078
|
+
armAttemptTimer(attempt, EXTERNAL_METHOD_CALLBACK_GRACE_MS);
|
|
2079
|
+
}, [armAttemptTimer]);
|
|
2080
|
+
const startAttempt = (0, import_react9.useCallback)((method) => {
|
|
2081
|
+
clearAttemptTimer();
|
|
2082
|
+
const generation = generationRef.current + 1;
|
|
2083
|
+
generationRef.current = generation;
|
|
2084
|
+
invalidatedGenerationRef.current = null;
|
|
2085
|
+
const attempt = { generation, method, timer: null, yieldedControl: false };
|
|
2086
|
+
attemptRef.current = attempt;
|
|
2087
|
+
return generation;
|
|
2088
|
+
}, [clearAttemptTimer]);
|
|
2089
|
+
const finishAttempt = (0, import_react9.useCallback)((generation) => {
|
|
2090
|
+
const attempt = attemptRef.current;
|
|
2091
|
+
if (typeof generation === "number" && attempt?.generation !== generation) return false;
|
|
2092
|
+
clearAttemptTimer(attempt);
|
|
2093
|
+
attemptRef.current = null;
|
|
2094
|
+
if (typeof generation !== "number" || invalidatedGenerationRef.current === generation) {
|
|
2095
|
+
invalidatedGenerationRef.current = null;
|
|
2096
|
+
}
|
|
2097
|
+
return true;
|
|
2098
|
+
}, [clearAttemptTimer]);
|
|
2099
|
+
const invalidateAttempt = (0, import_react9.useCallback)((generation) => {
|
|
2100
|
+
const attempt = attemptRef.current;
|
|
2101
|
+
const targetGeneration = generation ?? attempt?.generation ?? generationRef.current;
|
|
2102
|
+
if (!generation || attempt?.generation === generation) {
|
|
2103
|
+
clearAttemptTimer(attempt);
|
|
2104
|
+
attemptRef.current = null;
|
|
2105
|
+
}
|
|
2106
|
+
invalidatedGenerationRef.current = targetGeneration;
|
|
2107
|
+
}, [clearAttemptTimer]);
|
|
2108
|
+
const isAttemptInvalidated = (0, import_react9.useCallback)(
|
|
2109
|
+
(generation) => invalidatedGenerationRef.current === (generation ?? generationRef.current),
|
|
2110
|
+
[]
|
|
2111
|
+
);
|
|
2112
|
+
const isAttemptCurrent = (0, import_react9.useCallback)(
|
|
2113
|
+
(generation) => attemptRef.current?.generation === generation && invalidatedGenerationRef.current !== generation,
|
|
2114
|
+
[]
|
|
2115
|
+
);
|
|
2116
|
+
const scheduleRecoveryIfReturned = (0, import_react9.useCallback)(() => {
|
|
2117
|
+
const attempt = attemptRef.current;
|
|
2118
|
+
if (!attempt) return;
|
|
2119
|
+
armRecoveryTimer(attempt);
|
|
2120
|
+
}, [armRecoveryTimer]);
|
|
2121
|
+
const markAttemptYieldedControl = (0, import_react9.useCallback)(() => {
|
|
2122
|
+
const attempt = attemptRef.current;
|
|
2123
|
+
if (!attempt) return;
|
|
2124
|
+
attempt.yieldedControl = true;
|
|
2125
|
+
clearAttemptTimer(attempt);
|
|
2126
|
+
}, [clearAttemptTimer]);
|
|
2127
|
+
(0, import_react9.useEffect)(() => {
|
|
2128
|
+
const handleVisibilityChange = () => {
|
|
2129
|
+
if (document.visibilityState === "visible") {
|
|
2130
|
+
scheduleRecoveryIfReturned();
|
|
2131
|
+
} else {
|
|
2132
|
+
markAttemptYieldedControl();
|
|
2133
|
+
}
|
|
2134
|
+
};
|
|
2135
|
+
const handleBlur = () => {
|
|
2136
|
+
markAttemptYieldedControl();
|
|
2137
|
+
};
|
|
2138
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
2139
|
+
window.addEventListener("blur", handleBlur);
|
|
2140
|
+
window.addEventListener("focus", scheduleRecoveryIfReturned);
|
|
2141
|
+
return () => {
|
|
2142
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
2143
|
+
window.removeEventListener("blur", handleBlur);
|
|
2144
|
+
window.removeEventListener("focus", scheduleRecoveryIfReturned);
|
|
2145
|
+
clearAttemptTimer();
|
|
2146
|
+
};
|
|
2147
|
+
}, [clearAttemptTimer, markAttemptYieldedControl, scheduleRecoveryIfReturned]);
|
|
2148
|
+
return { startAttempt, finishAttempt, invalidateAttempt, isAttemptInvalidated, isAttemptCurrent };
|
|
2149
|
+
}
|
|
1453
2150
|
function malformedPostcodeMessage(country) {
|
|
1454
2151
|
const example = (0, import_shared5.getPostalCodeExample)(country);
|
|
1455
2152
|
return `Enter a valid ${(0, import_shared5.getPostalCodeLabel)(country)}${example ? ` (e.g. ${example})` : ""}`;
|
|
@@ -1544,10 +2241,12 @@ function PayPalButtonInner({
|
|
|
1544
2241
|
isProcessing = false,
|
|
1545
2242
|
onButtonClick,
|
|
1546
2243
|
onDecline,
|
|
2244
|
+
onTechnicalFailure,
|
|
1547
2245
|
runBeforeButtonClick,
|
|
1548
2246
|
onLoadStateChange,
|
|
1549
2247
|
placeholderBorderRadius
|
|
1550
2248
|
}) {
|
|
2249
|
+
const flopay = useFloPay();
|
|
1551
2250
|
const stripe = (0, import_react_stripe_js.useStripe)();
|
|
1552
2251
|
const elements = (0, import_react_stripe_js.useElements)();
|
|
1553
2252
|
const [loadState, setLoadState] = (0, import_react9.useState)("loading");
|
|
@@ -1556,8 +2255,55 @@ function PayPalButtonInner({
|
|
|
1556
2255
|
}, [loadState, onLoadStateChange]);
|
|
1557
2256
|
const [submitting, setSubmitting] = (0, import_react9.useState)(false);
|
|
1558
2257
|
const paypalResumeAttempted = (0, import_react9.useRef)(false);
|
|
1559
|
-
const
|
|
2258
|
+
const focusTargetRef = (0, import_react9.useRef)(null);
|
|
2259
|
+
const recoveryActionRef = (0, import_react9.useRef)(null);
|
|
2260
|
+
const [surfaceKey, setSurfaceKey] = (0, import_react9.useState)(0);
|
|
2261
|
+
const [showRecoveryAction, setShowRecoveryAction] = (0, import_react9.useState)(false);
|
|
2262
|
+
const pendingProviderFocusRef = (0, import_react9.useRef)(false);
|
|
2263
|
+
const attemptContextBySurfaceRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
|
|
1560
2264
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
2265
|
+
const {
|
|
2266
|
+
startAttempt,
|
|
2267
|
+
finishAttempt,
|
|
2268
|
+
invalidateAttempt,
|
|
2269
|
+
isAttemptInvalidated,
|
|
2270
|
+
isAttemptCurrent
|
|
2271
|
+
} = useExternalAttemptReconciliation((method, err, options) => {
|
|
2272
|
+
onTechnicalFailure?.(method, err, {
|
|
2273
|
+
...options,
|
|
2274
|
+
popupBlocked: options?.popupBlocked ?? isPopupBlockedError(err)
|
|
2275
|
+
});
|
|
2276
|
+
setShowRecoveryAction(true);
|
|
2277
|
+
setSurfaceKey((key) => key + 1);
|
|
2278
|
+
});
|
|
2279
|
+
(0, import_react9.useEffect)(() => {
|
|
2280
|
+
if (showRecoveryAction) recoveryActionRef.current?.focus();
|
|
2281
|
+
}, [showRecoveryAction, surfaceKey]);
|
|
2282
|
+
const resetSurface = (0, import_react9.useCallback)(() => {
|
|
2283
|
+
setSurfaceKey((key) => key + 1);
|
|
2284
|
+
}, []);
|
|
2285
|
+
const recoverTechnicalFailure = (0, import_react9.useCallback)((err, code, generation) => {
|
|
2286
|
+
invalidateAttempt(generation);
|
|
2287
|
+
onTechnicalFailure?.("paypal", err, { code, popupBlocked: isPopupBlockedError(err) });
|
|
2288
|
+
setShowRecoveryAction(true);
|
|
2289
|
+
resetSurface();
|
|
2290
|
+
}, [invalidateAttempt, onTechnicalFailure, resetSurface]);
|
|
2291
|
+
const focusProviderSurface = (0, import_react9.useCallback)(() => {
|
|
2292
|
+
window.setTimeout(() => {
|
|
2293
|
+
const target = focusTargetRef.current?.querySelector("iframe");
|
|
2294
|
+
(target ?? focusTargetRef.current)?.focus();
|
|
2295
|
+
}, 0);
|
|
2296
|
+
}, []);
|
|
2297
|
+
(0, import_react9.useEffect)(() => {
|
|
2298
|
+
if (!pendingProviderFocusRef.current) return;
|
|
2299
|
+
pendingProviderFocusRef.current = false;
|
|
2300
|
+
focusProviderSurface();
|
|
2301
|
+
}, [focusProviderSurface, surfaceKey]);
|
|
2302
|
+
const handleRecoveryActionClick = (0, import_react9.useCallback)(() => {
|
|
2303
|
+
setShowRecoveryAction(false);
|
|
2304
|
+
onErrorChange?.(null);
|
|
2305
|
+
focusProviderSurface();
|
|
2306
|
+
}, [focusProviderSurface, onErrorChange]);
|
|
1561
2307
|
(0, import_react9.useEffect)(() => {
|
|
1562
2308
|
if (!stripe || paypalResumeAttempted.current) return;
|
|
1563
2309
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -1631,22 +2377,33 @@ function PayPalButtonInner({
|
|
|
1631
2377
|
}
|
|
1632
2378
|
const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick("paypal") : { proceed: true };
|
|
1633
2379
|
if (!beforeClick.proceed) {
|
|
1634
|
-
|
|
2380
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
1635
2381
|
event.reject();
|
|
1636
2382
|
return;
|
|
1637
2383
|
}
|
|
1638
|
-
|
|
2384
|
+
const generation = startAttempt("paypal");
|
|
2385
|
+
attemptContextBySurfaceRef.current.set(surfaceKey, {
|
|
2386
|
+
generation,
|
|
1639
2387
|
accountPatch: beforeClick.accountPatch,
|
|
1640
2388
|
sessionId: beforeClick.sessionId,
|
|
1641
2389
|
nonce: beforeClick.nonce
|
|
1642
|
-
};
|
|
2390
|
+
});
|
|
2391
|
+
setShowRecoveryAction(false);
|
|
1643
2392
|
onButtonClick?.("paypal");
|
|
1644
2393
|
event.resolve();
|
|
1645
|
-
}, [isProcessing, onButtonClick, runBeforeButtonClick, submitting]);
|
|
2394
|
+
}, [attemptContextBySurfaceRef, isProcessing, onButtonClick, runBeforeButtonClick, startAttempt, submitting, surfaceKey]);
|
|
1646
2395
|
const handlePayPalConfirm = (0, import_react9.useCallback)(async (event) => {
|
|
1647
2396
|
if (!stripe || !elements) return;
|
|
1648
|
-
|
|
1649
|
-
|
|
2397
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2398
|
+
if (attemptContext && !isAttemptCurrent(attemptContext.generation)) {
|
|
2399
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
if (!attemptContext && isAttemptInvalidated()) return;
|
|
2403
|
+
if (attemptContext && !finishAttempt(attemptContext.generation)) return;
|
|
2404
|
+
if (!attemptContext) finishAttempt();
|
|
2405
|
+
let prepared = attemptContext ?? null;
|
|
2406
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
1650
2407
|
if (!prepared && runBeforeButtonClick) {
|
|
1651
2408
|
const beforeClick = await runBeforeButtonClick("paypal");
|
|
1652
2409
|
if (!beforeClick.proceed) {
|
|
@@ -1671,9 +2428,8 @@ function PayPalButtonInner({
|
|
|
1671
2428
|
const createPM = stripe.createPaymentMethod;
|
|
1672
2429
|
const { error: pmError, paymentMethod } = await createPM({ type: "paypal" });
|
|
1673
2430
|
if (pmError) {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
event.paymentFailed({ reason: "fail", message });
|
|
2431
|
+
recoverTechnicalFailure(pmError, "stripe_paypal_create_payment_method_failed", attemptContext?.generation);
|
|
2432
|
+
event.paymentFailed({ reason: "fail" });
|
|
1677
2433
|
return;
|
|
1678
2434
|
}
|
|
1679
2435
|
const intentHeaders = { "Content-Type": "application/json" };
|
|
@@ -1732,11 +2488,16 @@ function PayPalButtonInner({
|
|
|
1732
2488
|
} catch {
|
|
1733
2489
|
}
|
|
1734
2490
|
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
2491
|
+
if (isProviderDecline(confirmError)) {
|
|
2492
|
+
const message = confirmError.message ?? "Your payment was declined.";
|
|
2493
|
+
onErrorChange?.(message);
|
|
2494
|
+
onDecline?.(buildDeclineEvent("paypal", message, {
|
|
2495
|
+
code: getProviderErrorCode(confirmError),
|
|
2496
|
+
declineCode: getProviderDeclineCode(confirmError)
|
|
2497
|
+
}));
|
|
2498
|
+
} else {
|
|
2499
|
+
recoverTechnicalFailure(confirmError, "stripe_paypal_confirm_failed", attemptContext?.generation);
|
|
2500
|
+
}
|
|
1740
2501
|
return;
|
|
1741
2502
|
}
|
|
1742
2503
|
if (typeof window !== "undefined") {
|
|
@@ -1758,11 +2519,11 @@ function PayPalButtonInner({
|
|
|
1758
2519
|
});
|
|
1759
2520
|
return;
|
|
1760
2521
|
} catch (err) {
|
|
1761
|
-
|
|
2522
|
+
recoverTechnicalFailure(err, "stripe_paypal_failed", attemptContext?.generation);
|
|
1762
2523
|
} finally {
|
|
1763
2524
|
setSubmitting(false);
|
|
1764
2525
|
}
|
|
1765
|
-
}, [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]);
|
|
2526
|
+
}, [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]);
|
|
1766
2527
|
return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
|
|
1767
2528
|
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
1768
2529
|
ExpressCheckoutReadySwap,
|
|
@@ -1770,29 +2531,62 @@ function PayPalButtonInner({
|
|
|
1770
2531
|
state: loadState,
|
|
1771
2532
|
placeholderTestId: "flopay-paypal-placeholder",
|
|
1772
2533
|
borderRadius: placeholderBorderRadius,
|
|
1773
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime7.
|
|
1774
|
-
|
|
2534
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2535
|
+
"div",
|
|
1775
2536
|
{
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2537
|
+
ref: focusTargetRef,
|
|
2538
|
+
tabIndex: -1,
|
|
2539
|
+
"data-testid": "flopay-paypal-focus-target",
|
|
2540
|
+
"aria-label": "PayPal payment method",
|
|
2541
|
+
style: { borderRadius: 8, outlineOffset: 4 },
|
|
2542
|
+
children: [
|
|
2543
|
+
showRecoveryAction && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2544
|
+
"button",
|
|
2545
|
+
{
|
|
2546
|
+
ref: recoveryActionRef,
|
|
2547
|
+
type: "button",
|
|
2548
|
+
"data-testid": "flopay-paypal-retry-button",
|
|
2549
|
+
onClick: handleRecoveryActionClick,
|
|
2550
|
+
style: EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE,
|
|
2551
|
+
children: "Try PayPal again"
|
|
2552
|
+
}
|
|
2553
|
+
),
|
|
2554
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2555
|
+
import_react_stripe_js.ExpressCheckoutElement,
|
|
2556
|
+
{
|
|
2557
|
+
onReady: (event) => setLoadState(resolveExpressCheckoutLoadState(event, ["paypal"])),
|
|
2558
|
+
onLoadError: () => setLoadState("load_error"),
|
|
2559
|
+
onClick: handlePayPalClick,
|
|
2560
|
+
onConfirm: handlePayPalConfirm,
|
|
2561
|
+
onCancel: () => {
|
|
2562
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2563
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2564
|
+
getFloPayTelemetryBridge(flopay)?.terminal({
|
|
2565
|
+
outcome: "payment_cancelled",
|
|
2566
|
+
provider: "paypal",
|
|
2567
|
+
paymentMethodCategory: "paypal"
|
|
2568
|
+
});
|
|
2569
|
+
invalidateAttempt(attemptContext?.generation);
|
|
2570
|
+
setShowRecoveryAction(false);
|
|
2571
|
+
pendingProviderFocusRef.current = true;
|
|
2572
|
+
resetSurface();
|
|
2573
|
+
},
|
|
2574
|
+
options: {
|
|
2575
|
+
buttonType: { paypal: "paypal" },
|
|
2576
|
+
billingAddressRequired: false,
|
|
2577
|
+
phoneNumberRequired: false,
|
|
2578
|
+
shippingAddressRequired: false,
|
|
2579
|
+
paymentMethods: {
|
|
2580
|
+
applePay: "never",
|
|
2581
|
+
googlePay: "never",
|
|
2582
|
+
paypal: "auto",
|
|
2583
|
+
link: "never"
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
},
|
|
2587
|
+
surfaceKey
|
|
2588
|
+
)
|
|
2589
|
+
]
|
|
1796
2590
|
}
|
|
1797
2591
|
)
|
|
1798
2592
|
}
|
|
@@ -1810,10 +2604,12 @@ function WalletButtonInner({
|
|
|
1810
2604
|
onErrorChange,
|
|
1811
2605
|
onButtonClick,
|
|
1812
2606
|
onDecline,
|
|
2607
|
+
onTechnicalFailure,
|
|
1813
2608
|
runBeforeButtonClick,
|
|
1814
2609
|
onLoadStateChange,
|
|
1815
2610
|
placeholderBorderRadius
|
|
1816
2611
|
}) {
|
|
2612
|
+
const flopay = useFloPay();
|
|
1817
2613
|
const stripe = (0, import_react_stripe_js.useStripe)();
|
|
1818
2614
|
const elements = (0, import_react_stripe_js.useElements)();
|
|
1819
2615
|
const [loadState, setLoadState] = (0, import_react9.useState)("loading");
|
|
@@ -1822,15 +2618,74 @@ function WalletButtonInner({
|
|
|
1822
2618
|
}, [loadState, onLoadStateChange]);
|
|
1823
2619
|
const [submitting, setSubmitting] = (0, import_react9.useState)(false);
|
|
1824
2620
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
1825
|
-
const
|
|
1826
|
-
const
|
|
2621
|
+
const focusTargetRef = (0, import_react9.useRef)(null);
|
|
2622
|
+
const recoveryActionRef = (0, import_react9.useRef)(null);
|
|
2623
|
+
const [surfaceKey, setSurfaceKey] = (0, import_react9.useState)(0);
|
|
2624
|
+
const [showRecoveryAction, setShowRecoveryAction] = (0, import_react9.useState)(false);
|
|
2625
|
+
const [recoveryActionMethod, setRecoveryActionMethod] = (0, import_react9.useState)("google_pay");
|
|
2626
|
+
const pendingProviderFocusRef = (0, import_react9.useRef)(false);
|
|
2627
|
+
const lastWalletProviderMethodRef = (0, import_react9.useRef)("google_pay");
|
|
2628
|
+
const lastWalletMethodRef = (0, import_react9.useRef)("google_pay");
|
|
2629
|
+
const attemptContextBySurfaceRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
|
|
2630
|
+
const {
|
|
2631
|
+
startAttempt,
|
|
2632
|
+
finishAttempt,
|
|
2633
|
+
invalidateAttempt,
|
|
2634
|
+
isAttemptInvalidated,
|
|
2635
|
+
isAttemptCurrent
|
|
2636
|
+
} = useExternalAttemptReconciliation((method, err, options) => {
|
|
2637
|
+
onTechnicalFailure?.(method, err, {
|
|
2638
|
+
...options,
|
|
2639
|
+
popupBlocked: options?.popupBlocked ?? isPopupBlockedError(err)
|
|
2640
|
+
});
|
|
2641
|
+
setRecoveryActionMethod(method);
|
|
2642
|
+
setShowRecoveryAction(true);
|
|
2643
|
+
setSurfaceKey((key) => key + 1);
|
|
2644
|
+
});
|
|
2645
|
+
(0, import_react9.useEffect)(() => {
|
|
2646
|
+
if (showRecoveryAction) recoveryActionRef.current?.focus();
|
|
2647
|
+
}, [showRecoveryAction, surfaceKey]);
|
|
2648
|
+
const resetSurface = (0, import_react9.useCallback)(() => {
|
|
2649
|
+
setSurfaceKey((key) => key + 1);
|
|
2650
|
+
}, []);
|
|
2651
|
+
const recoverTechnicalFailure = (0, import_react9.useCallback)((method, err, code, generation) => {
|
|
2652
|
+
invalidateAttempt(generation);
|
|
2653
|
+
onTechnicalFailure?.(method, err, { code, popupBlocked: isPopupBlockedError(err) });
|
|
2654
|
+
setRecoveryActionMethod(method);
|
|
2655
|
+
setShowRecoveryAction(true);
|
|
2656
|
+
resetSurface();
|
|
2657
|
+
}, [invalidateAttempt, onTechnicalFailure, resetSurface]);
|
|
2658
|
+
const focusProviderSurface = (0, import_react9.useCallback)(() => {
|
|
2659
|
+
window.setTimeout(() => {
|
|
2660
|
+
const target = focusTargetRef.current?.querySelector("iframe");
|
|
2661
|
+
(target ?? focusTargetRef.current)?.focus();
|
|
2662
|
+
}, 0);
|
|
2663
|
+
}, []);
|
|
2664
|
+
(0, import_react9.useEffect)(() => {
|
|
2665
|
+
if (!pendingProviderFocusRef.current) return;
|
|
2666
|
+
pendingProviderFocusRef.current = false;
|
|
2667
|
+
focusProviderSurface();
|
|
2668
|
+
}, [focusProviderSurface, surfaceKey]);
|
|
2669
|
+
const handleRecoveryActionClick = (0, import_react9.useCallback)(() => {
|
|
2670
|
+
setShowRecoveryAction(false);
|
|
2671
|
+
onErrorChange?.(null);
|
|
2672
|
+
focusProviderSurface();
|
|
2673
|
+
}, [focusProviderSurface, onErrorChange]);
|
|
1827
2674
|
const handleWalletConfirm = (0, import_react9.useCallback)(
|
|
1828
2675
|
async (event) => {
|
|
1829
2676
|
if (!stripe || !elements) return;
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
2677
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2678
|
+
if (attemptContext && !isAttemptCurrent(attemptContext.generation)) {
|
|
2679
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
if (!attemptContext && isAttemptInvalidated()) return;
|
|
2683
|
+
if (attemptContext && !finishAttempt(attemptContext.generation)) return;
|
|
2684
|
+
if (!attemptContext) finishAttempt();
|
|
2685
|
+
const walletType = event.expressPaymentType ?? attemptContext?.method ?? lastWalletProviderMethodRef.current;
|
|
2686
|
+
let prepared = attemptContext ?? null;
|
|
2687
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2688
|
+
const buttonMethod = checkoutButtonMethodFromProviderMethod(walletType);
|
|
1834
2689
|
if (!prepared && runBeforeButtonClick) {
|
|
1835
2690
|
const beforeClick = await runBeforeButtonClick(buttonMethod);
|
|
1836
2691
|
if (!beforeClick.proceed) {
|
|
@@ -1852,12 +2707,12 @@ function WalletButtonInner({
|
|
|
1852
2707
|
onErrorChange?.(null);
|
|
1853
2708
|
const { error: submitError } = await elements.submit();
|
|
1854
2709
|
if (submitError) {
|
|
1855
|
-
|
|
2710
|
+
recoverTechnicalFailure(walletType, submitError, "stripe_wallet_submit_failed", attemptContext?.generation);
|
|
1856
2711
|
return;
|
|
1857
2712
|
}
|
|
1858
2713
|
const { error: pmError, paymentMethod } = await stripe.createPaymentMethod({ elements });
|
|
1859
2714
|
if (pmError || !paymentMethod) {
|
|
1860
|
-
|
|
2715
|
+
recoverTechnicalFailure(walletType, pmError ?? new Error("Failed to create payment method."), "stripe_wallet_create_payment_method_failed", attemptContext?.generation);
|
|
1861
2716
|
return;
|
|
1862
2717
|
}
|
|
1863
2718
|
if (!effectiveSessionId || !effectiveEmail) {
|
|
@@ -1890,11 +2745,16 @@ function WalletButtonInner({
|
|
|
1890
2745
|
if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
|
|
1891
2746
|
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
2747
|
if (confirmError) {
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
2748
|
+
if (isProviderDecline(confirmError)) {
|
|
2749
|
+
const message = confirmError.message ?? "Your payment was declined.";
|
|
2750
|
+
onErrorChange?.(message);
|
|
2751
|
+
onDecline?.(buildDeclineEvent(method, message, {
|
|
2752
|
+
code: getProviderErrorCode(confirmError),
|
|
2753
|
+
declineCode: getProviderDeclineCode(confirmError)
|
|
2754
|
+
}));
|
|
2755
|
+
} else {
|
|
2756
|
+
recoverTechnicalFailure(walletType, confirmError, "stripe_wallet_confirm_failed", attemptContext?.generation);
|
|
2757
|
+
}
|
|
1898
2758
|
return;
|
|
1899
2759
|
}
|
|
1900
2760
|
onTokenizedBody({
|
|
@@ -1907,12 +2767,12 @@ function WalletButtonInner({
|
|
|
1907
2767
|
nonce: effectiveNonce
|
|
1908
2768
|
});
|
|
1909
2769
|
} catch (err) {
|
|
1910
|
-
|
|
2770
|
+
recoverTechnicalFailure(walletType, err, "stripe_wallet_failed", attemptContext?.generation);
|
|
1911
2771
|
} finally {
|
|
1912
2772
|
setSubmitting(false);
|
|
1913
2773
|
}
|
|
1914
2774
|
},
|
|
1915
|
-
[stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]
|
|
2775
|
+
[stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]
|
|
1916
2776
|
);
|
|
1917
2777
|
const expressMethodMap = (0, import_react9.useMemo)(() => {
|
|
1918
2778
|
const allKeys = ["applePay", "googlePay", "paypal", "link", "amazonPay", "klarna"];
|
|
@@ -1939,41 +2799,90 @@ function WalletButtonInner({
|
|
|
1939
2799
|
state: loadState,
|
|
1940
2800
|
placeholderTestId: "flopay-wallet-placeholder",
|
|
1941
2801
|
borderRadius: placeholderBorderRadius,
|
|
1942
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime7.
|
|
1943
|
-
|
|
2802
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2803
|
+
"div",
|
|
1944
2804
|
{
|
|
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
|
-
|
|
2805
|
+
ref: focusTargetRef,
|
|
2806
|
+
tabIndex: -1,
|
|
2807
|
+
"data-testid": "flopay-wallet-focus-target",
|
|
2808
|
+
"aria-label": "Wallet payment methods",
|
|
2809
|
+
style: { borderRadius: 8, outlineOffset: 4 },
|
|
2810
|
+
children: [
|
|
2811
|
+
showRecoveryAction && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
|
|
2812
|
+
"button",
|
|
2813
|
+
{
|
|
2814
|
+
ref: recoveryActionRef,
|
|
2815
|
+
type: "button",
|
|
2816
|
+
"data-testid": "flopay-wallet-retry-button",
|
|
2817
|
+
onClick: handleRecoveryActionClick,
|
|
2818
|
+
style: EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE,
|
|
2819
|
+
children: [
|
|
2820
|
+
"Try ",
|
|
2821
|
+
getExternalMethodDisplayName(recoveryActionMethod),
|
|
2822
|
+
" again"
|
|
2823
|
+
]
|
|
2824
|
+
}
|
|
2825
|
+
),
|
|
2826
|
+
/* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
2827
|
+
import_react_stripe_js.ExpressCheckoutElement,
|
|
2828
|
+
{
|
|
2829
|
+
onReady: (event) => {
|
|
2830
|
+
setLoadState(resolveExpressCheckoutLoadState(event, availableMethodKeys));
|
|
2831
|
+
},
|
|
2832
|
+
onLoadError: (_event) => {
|
|
2833
|
+
setLoadState("load_error");
|
|
2834
|
+
getFloPayTelemetryBridge(flopay)?.error({
|
|
2835
|
+
errorCode: "PROVIDER_LOAD_FAILED",
|
|
2836
|
+
stage: "provider_load",
|
|
2837
|
+
provider: "stripe",
|
|
2838
|
+
paymentMethodCategory: "wallet",
|
|
2839
|
+
requestCategory: "provider_sdk"
|
|
2840
|
+
});
|
|
2841
|
+
},
|
|
2842
|
+
onClick: async (event) => {
|
|
2843
|
+
lastWalletProviderMethodRef.current = event.expressPaymentType;
|
|
2844
|
+
lastWalletMethodRef.current = checkoutButtonMethodFromProviderMethod(event.expressPaymentType);
|
|
2845
|
+
const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick(lastWalletMethodRef.current) : { proceed: true };
|
|
2846
|
+
if (!beforeClick.proceed) {
|
|
2847
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2848
|
+
event.reject();
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
const generation = startAttempt(event.expressPaymentType);
|
|
2852
|
+
attemptContextBySurfaceRef.current.set(surfaceKey, {
|
|
2853
|
+
generation,
|
|
2854
|
+
method: event.expressPaymentType,
|
|
2855
|
+
accountPatch: beforeClick.accountPatch,
|
|
2856
|
+
sessionId: beforeClick.sessionId,
|
|
2857
|
+
nonce: beforeClick.nonce
|
|
2858
|
+
});
|
|
2859
|
+
setShowRecoveryAction(false);
|
|
2860
|
+
onButtonClick?.(lastWalletMethodRef.current);
|
|
2861
|
+
event.resolve();
|
|
2862
|
+
},
|
|
2863
|
+
onConfirm: handleWalletConfirm,
|
|
2864
|
+
onCancel: () => {
|
|
2865
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2866
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2867
|
+
getFloPayTelemetryBridge(flopay)?.terminal({
|
|
2868
|
+
outcome: "payment_cancelled",
|
|
2869
|
+
provider: "stripe",
|
|
2870
|
+
paymentMethodCategory: "wallet"
|
|
2871
|
+
});
|
|
2872
|
+
invalidateAttempt(attemptContext?.generation);
|
|
2873
|
+
setShowRecoveryAction(false);
|
|
2874
|
+
pendingProviderFocusRef.current = true;
|
|
2875
|
+
resetSurface();
|
|
2876
|
+
},
|
|
2877
|
+
options: {
|
|
2878
|
+
buttonType: { applePay: "plain", googlePay: "plain" },
|
|
2879
|
+
paymentMethods: expressMethodMap,
|
|
2880
|
+
layout: { maxColumns: 1, overflow: "never" }
|
|
2881
|
+
}
|
|
2882
|
+
},
|
|
2883
|
+
surfaceKey
|
|
2884
|
+
)
|
|
2885
|
+
]
|
|
1977
2886
|
}
|
|
1978
2887
|
)
|
|
1979
2888
|
}
|
|
@@ -2130,6 +3039,7 @@ function StripeMethodInlineForm({
|
|
|
2130
3039
|
submitButtonStyle,
|
|
2131
3040
|
errorText
|
|
2132
3041
|
}) {
|
|
3042
|
+
const flopay = useFloPay();
|
|
2133
3043
|
const stripe = (0, import_react_stripe_js.useStripe)();
|
|
2134
3044
|
const elements = (0, import_react_stripe_js.useElements)();
|
|
2135
3045
|
const [submitting, setSubmitting] = (0, import_react9.useState)(false);
|
|
@@ -2282,7 +3192,16 @@ function StripeMethodInlineForm({
|
|
|
2282
3192
|
import_react_stripe_js.PaymentElement,
|
|
2283
3193
|
{
|
|
2284
3194
|
onReady: () => setLoadState("ready"),
|
|
2285
|
-
onLoadError: () =>
|
|
3195
|
+
onLoadError: () => {
|
|
3196
|
+
setLoadState("load_error");
|
|
3197
|
+
getFloPayTelemetryBridge(flopay)?.error({
|
|
3198
|
+
errorCode: "PROVIDER_LOAD_FAILED",
|
|
3199
|
+
stage: "provider_load",
|
|
3200
|
+
provider: "stripe",
|
|
3201
|
+
paymentMethodCategory: "apm",
|
|
3202
|
+
requestCategory: "provider_sdk"
|
|
3203
|
+
});
|
|
3204
|
+
},
|
|
2286
3205
|
onChange: (event) => {
|
|
2287
3206
|
const evRecord = event;
|
|
2288
3207
|
setIsMethodComplete(!!evRecord.complete);
|
|
@@ -2884,6 +3803,30 @@ function SplitCardFormInner({
|
|
|
2884
3803
|
},
|
|
2885
3804
|
[onDecline]
|
|
2886
3805
|
);
|
|
3806
|
+
const recoverExternalMethodTechnicalFailure = (0, import_react9.useCallback)(
|
|
3807
|
+
(method, err, options) => {
|
|
3808
|
+
const popupBlocked = options?.popupBlocked ?? isPopupBlockedError(err);
|
|
3809
|
+
const message = buildExternalMethodRecoveryMessage(method, popupBlocked);
|
|
3810
|
+
const providerCode = sanitizeExternalFailureCode(options?.code) ?? getProviderErrorCode(err) ?? "external_payment_method_failed";
|
|
3811
|
+
const floPayError = new import_shared6.FloPayError(message, "api_error", {
|
|
3812
|
+
code: "external_payment_method_failed",
|
|
3813
|
+
param: method
|
|
3814
|
+
});
|
|
3815
|
+
setOverlayStatus(null);
|
|
3816
|
+
if (layout === "buttons") {
|
|
3817
|
+
setViewState("buttons");
|
|
3818
|
+
setExpandedApmMethod(null);
|
|
3819
|
+
}
|
|
3820
|
+
updateError(message);
|
|
3821
|
+
onError?.(floPayError);
|
|
3822
|
+
console.error("[FloPay] External payment method failed:", {
|
|
3823
|
+
method,
|
|
3824
|
+
code: providerCode,
|
|
3825
|
+
popupBlocked
|
|
3826
|
+
});
|
|
3827
|
+
},
|
|
3828
|
+
[layout, onError, updateError]
|
|
3829
|
+
);
|
|
2887
3830
|
(0, import_react9.useEffect)(() => {
|
|
2888
3831
|
if (!vaultActive || !sessionId) {
|
|
2889
3832
|
setVaultMount(null);
|
|
@@ -4340,6 +5283,7 @@ function SplitCardFormInner({
|
|
|
4340
5283
|
onComplete,
|
|
4341
5284
|
onErrorChange: updateError,
|
|
4342
5285
|
onDecline,
|
|
5286
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4343
5287
|
onButtonClick,
|
|
4344
5288
|
runBeforeButtonClick,
|
|
4345
5289
|
isProcessing: isSubmitting,
|
|
@@ -4363,6 +5307,7 @@ function SplitCardFormInner({
|
|
|
4363
5307
|
isProcessing: isSubmitting,
|
|
4364
5308
|
onButtonClick,
|
|
4365
5309
|
onDecline,
|
|
5310
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4366
5311
|
runBeforeButtonClick,
|
|
4367
5312
|
onLoadStateChange: setPaypalLoadState,
|
|
4368
5313
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -4380,6 +5325,7 @@ function SplitCardFormInner({
|
|
|
4380
5325
|
onErrorChange: updateError,
|
|
4381
5326
|
onButtonClick,
|
|
4382
5327
|
onDecline,
|
|
5328
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4383
5329
|
runBeforeButtonClick,
|
|
4384
5330
|
onLoadStateChange: setWalletLoadState,
|
|
4385
5331
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -4735,6 +5681,7 @@ function SplitCardFormInner({
|
|
|
4735
5681
|
onComplete,
|
|
4736
5682
|
onErrorChange: updateError,
|
|
4737
5683
|
onDecline,
|
|
5684
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4738
5685
|
onButtonClick,
|
|
4739
5686
|
runBeforeButtonClick,
|
|
4740
5687
|
isProcessing: isSubmitting,
|
|
@@ -4758,6 +5705,7 @@ function SplitCardFormInner({
|
|
|
4758
5705
|
isProcessing: isSubmitting,
|
|
4759
5706
|
onButtonClick,
|
|
4760
5707
|
onDecline,
|
|
5708
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4761
5709
|
runBeforeButtonClick,
|
|
4762
5710
|
onLoadStateChange: setPaypalLoadState,
|
|
4763
5711
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -4775,6 +5723,7 @@ function SplitCardFormInner({
|
|
|
4775
5723
|
onErrorChange: updateError,
|
|
4776
5724
|
onButtonClick,
|
|
4777
5725
|
onDecline,
|
|
5726
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4778
5727
|
runBeforeButtonClick,
|
|
4779
5728
|
onLoadStateChange: setWalletLoadState,
|
|
4780
5729
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -5168,7 +6117,7 @@ async function recover3DSRedirectResult({
|
|
|
5168
6117
|
return null;
|
|
5169
6118
|
}
|
|
5170
6119
|
try {
|
|
5171
|
-
const api = new import_js3.PaymentAPI(billingApiUrl);
|
|
6120
|
+
const api = new import_js3.PaymentAPI(billingApiUrl, { telemetry: false });
|
|
5172
6121
|
const unified = await api.getUnifiedCheckoutSession(sessionId, nonce);
|
|
5173
6122
|
const refreshedToken = unified.data.stripe?.clientSecret;
|
|
5174
6123
|
if (isStripePaymentIntentClientSecret(refreshedToken)) {
|
|
@@ -5187,7 +6136,8 @@ async function processSavedPaymentForMode({
|
|
|
5187
6136
|
session,
|
|
5188
6137
|
nonce,
|
|
5189
6138
|
tokenizedData,
|
|
5190
|
-
returnUrl
|
|
6139
|
+
returnUrl,
|
|
6140
|
+
telemetry
|
|
5191
6141
|
}) {
|
|
5192
6142
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
5193
6143
|
const resolvedSessionId = sessionId ?? session.id;
|
|
@@ -5198,7 +6148,10 @@ async function processSavedPaymentForMode({
|
|
|
5198
6148
|
const lastName = session.customer?.lastName ?? session.accountData?.lastName ?? "";
|
|
5199
6149
|
const country = session.customer?.country ?? session.accountData?.country ?? void 0;
|
|
5200
6150
|
const zip = session.customer?.zip ?? session.accountData?.zip ?? void 0;
|
|
5201
|
-
const api = new import_js3.PaymentAPI(
|
|
6151
|
+
const api = new import_js3.PaymentAPI(
|
|
6152
|
+
baseUrl,
|
|
6153
|
+
telemetry === false ? { telemetry: false } : void 0
|
|
6154
|
+
);
|
|
5202
6155
|
const response = await retryOnceOnFetchFailure(() => api.processPayment(customerId, {
|
|
5203
6156
|
sessionId: resolvedSessionId,
|
|
5204
6157
|
nonce: resolvedNonce,
|
|
@@ -5275,7 +6228,8 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
|
|
|
5275
6228
|
billingApiUrl,
|
|
5276
6229
|
sessionId,
|
|
5277
6230
|
session,
|
|
5278
|
-
returnUrl
|
|
6231
|
+
returnUrl,
|
|
6232
|
+
telemetry
|
|
5279
6233
|
}) {
|
|
5280
6234
|
const stripe = flopay?.getRawProvider();
|
|
5281
6235
|
if (!stripe) {
|
|
@@ -5358,6 +6312,7 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
|
|
|
5358
6312
|
billingApiUrl,
|
|
5359
6313
|
sessionId,
|
|
5360
6314
|
session,
|
|
6315
|
+
telemetry,
|
|
5361
6316
|
tokenizedData: {
|
|
5362
6317
|
id: paymentIntent.id,
|
|
5363
6318
|
type: "card",
|
|
@@ -5379,7 +6334,8 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
|
|
|
5379
6334
|
billingApiUrl,
|
|
5380
6335
|
sessionId,
|
|
5381
6336
|
session,
|
|
5382
|
-
returnUrl
|
|
6337
|
+
returnUrl,
|
|
6338
|
+
telemetry
|
|
5383
6339
|
});
|
|
5384
6340
|
}
|
|
5385
6341
|
throw Object.assign(
|
|
@@ -5485,7 +6441,8 @@ async function loadSavedPaymentProviders({
|
|
|
5485
6441
|
publishableKey,
|
|
5486
6442
|
paypalPublishableKey,
|
|
5487
6443
|
billingApiUrl,
|
|
5488
|
-
locale
|
|
6444
|
+
locale,
|
|
6445
|
+
telemetry
|
|
5489
6446
|
}) {
|
|
5490
6447
|
if (!publishableKey) {
|
|
5491
6448
|
return { flopay: null, paypalFlopay: null };
|
|
@@ -5494,11 +6451,13 @@ async function loadSavedPaymentProviders({
|
|
|
5494
6451
|
const [instance, paypalInstanceOrError] = await Promise.all([
|
|
5495
6452
|
(0, import_js3.loadFloPay)(publishableKey, {
|
|
5496
6453
|
billingApiUrl,
|
|
5497
|
-
locale
|
|
6454
|
+
locale,
|
|
6455
|
+
telemetry
|
|
5498
6456
|
}),
|
|
5499
6457
|
needsSeparatePaypal ? (0, import_js3.loadFloPay)(paypalPublishableKey, {
|
|
5500
6458
|
billingApiUrl,
|
|
5501
|
-
locale
|
|
6459
|
+
locale,
|
|
6460
|
+
telemetry
|
|
5502
6461
|
}).catch((err) => {
|
|
5503
6462
|
console.warn("[FloPay] Failed to load PayPal Stripe instance:", err);
|
|
5504
6463
|
return null;
|
|
@@ -5518,6 +6477,33 @@ var sessionInflightMap = /* @__PURE__ */ new Map();
|
|
|
5518
6477
|
function sleep(ms) {
|
|
5519
6478
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5520
6479
|
}
|
|
6480
|
+
function createStandaloneTelemetryReporter(billingApiUrl, enabled, context) {
|
|
6481
|
+
const reporter = createTelemetryBridge({
|
|
6482
|
+
billingApiUrl,
|
|
6483
|
+
sdkPackage: "@flopay/react",
|
|
6484
|
+
sdkVersion: import_shared8.SDK_VERSION,
|
|
6485
|
+
enabled: enabled !== false
|
|
6486
|
+
});
|
|
6487
|
+
reporter.beginCheckout(context);
|
|
6488
|
+
return reporter;
|
|
6489
|
+
}
|
|
6490
|
+
function finishStandaloneTelemetry(reporter) {
|
|
6491
|
+
void reporter.flush().catch(() => {
|
|
6492
|
+
}).finally(() => reporter.destroy());
|
|
6493
|
+
}
|
|
6494
|
+
function reportStandaloneTelemetryError(billingApiUrl, enabled, context, errorCode, stage) {
|
|
6495
|
+
const reporter = createStandaloneTelemetryReporter(billingApiUrl, enabled, context);
|
|
6496
|
+
reporter.error({
|
|
6497
|
+
errorCode,
|
|
6498
|
+
stage,
|
|
6499
|
+
paymentMethodCategory: "unknown",
|
|
6500
|
+
...stage === "session_read" ? { requestCategory: "session_read" } : {}
|
|
6501
|
+
});
|
|
6502
|
+
finishStandaloneTelemetry(reporter);
|
|
6503
|
+
}
|
|
6504
|
+
function isExpectedExistingSessionError(error) {
|
|
6505
|
+
return error.type === "validation_error" || error.code === "checkout_session_not_found" || error.code === "checkout_session_expired" || error.code === "checkout_session_completed" || error.code === "session_auto_completed";
|
|
6506
|
+
}
|
|
5521
6507
|
function resolveDirectPaypalConfig(unified) {
|
|
5522
6508
|
const clientId = unified?.data.paypal?.publishableKey;
|
|
5523
6509
|
if (!clientId) return void 0;
|
|
@@ -5622,6 +6608,7 @@ function FloPayCheckout({
|
|
|
5622
6608
|
nonce: nonceProp,
|
|
5623
6609
|
createSession: createSessionParams,
|
|
5624
6610
|
billingApiUrl,
|
|
6611
|
+
telemetry,
|
|
5625
6612
|
appearance: appearanceOverride,
|
|
5626
6613
|
locale,
|
|
5627
6614
|
loading: loadingNode,
|
|
@@ -5687,12 +6674,22 @@ function FloPayCheckout({
|
|
|
5687
6674
|
const autoCheckoutAttempted = (0, import_react10.useRef)(false);
|
|
5688
6675
|
const paypalResumeAttempted = (0, import_react10.useRef)(false);
|
|
5689
6676
|
const savedPaymentKeysRef = (0, import_react10.useRef)(null);
|
|
6677
|
+
const telemetryCheckoutContext = (0, import_react10.useMemo)(() => ({
|
|
6678
|
+
checkoutMode: checkoutModeProp ?? currentMode,
|
|
6679
|
+
layout: children ? "unknown" : layout === "buttons" ? "buttons" : "embedded"
|
|
6680
|
+
}), [checkoutModeProp, children, currentMode, layout]);
|
|
5690
6681
|
const onCompleteRef = (0, import_react10.useRef)(onComplete);
|
|
5691
6682
|
onCompleteRef.current = onComplete;
|
|
5692
6683
|
const onErrorRef = (0, import_react10.useRef)(onError);
|
|
5693
6684
|
onErrorRef.current = onError;
|
|
5694
6685
|
const onDeclineRef = (0, import_react10.useRef)(onDecline);
|
|
5695
6686
|
onDeclineRef.current = onDecline;
|
|
6687
|
+
(0, import_react10.useEffect)(() => {
|
|
6688
|
+
getFloPayTelemetryBridge(flopay)?.setCheckoutContext(telemetryCheckoutContext);
|
|
6689
|
+
if (paypalFlopay && paypalFlopay !== flopay) {
|
|
6690
|
+
getFloPayTelemetryBridge(paypalFlopay)?.setCheckoutContext(telemetryCheckoutContext);
|
|
6691
|
+
}
|
|
6692
|
+
}, [flopay, paypalFlopay, telemetryCheckoutContext]);
|
|
5696
6693
|
const onSessionCompletedRef = (0, import_react10.useRef)(onSessionCompleted);
|
|
5697
6694
|
onSessionCompletedRef.current = onSessionCompleted;
|
|
5698
6695
|
(0, import_react10.useEffect)(() => {
|
|
@@ -5732,9 +6729,11 @@ function FloPayCheckout({
|
|
|
5732
6729
|
}, [initialErrorMessage]);
|
|
5733
6730
|
const emitDecline = (0, import_react10.useCallback)(
|
|
5734
6731
|
(method, input, overrides) => {
|
|
5735
|
-
|
|
6732
|
+
invokeMerchantCallback(() => {
|
|
6733
|
+
onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
|
|
6734
|
+
});
|
|
5736
6735
|
},
|
|
5737
|
-
[]
|
|
6736
|
+
[invokeMerchantCallback]
|
|
5738
6737
|
);
|
|
5739
6738
|
const runSavedPaymentFlow = (0, import_react10.useCallback)(
|
|
5740
6739
|
async (sess, options) => {
|
|
@@ -5742,10 +6741,98 @@ function FloPayCheckout({
|
|
|
5742
6741
|
setModeOverlayError(null);
|
|
5743
6742
|
setModeOverlayStatus("processing");
|
|
5744
6743
|
const activeSessionId2 = options?.sessionId ?? sess.id;
|
|
6744
|
+
const activeFloPay = flopayRef.current ?? paypalFlopayRef.current;
|
|
6745
|
+
const activeTelemetry = getFloPayTelemetryBridge(activeFloPay);
|
|
6746
|
+
const standaloneTelemetry = activeFloPay ? null : createStandaloneTelemetryReporter(
|
|
6747
|
+
resolvedBillingUrl,
|
|
6748
|
+
telemetry,
|
|
6749
|
+
telemetryCheckoutContext
|
|
6750
|
+
);
|
|
6751
|
+
const telemetrySource = {
|
|
6752
|
+
log: (input) => {
|
|
6753
|
+
if (activeTelemetry) activeTelemetry.log(input);
|
|
6754
|
+
else standaloneTelemetry?.log(input);
|
|
6755
|
+
},
|
|
6756
|
+
error: (input) => {
|
|
6757
|
+
if (activeTelemetry) activeTelemetry.error(input);
|
|
6758
|
+
else standaloneTelemetry?.error(input);
|
|
6759
|
+
},
|
|
6760
|
+
terminal: (input) => {
|
|
6761
|
+
if (activeTelemetry) activeTelemetry.terminal(input);
|
|
6762
|
+
else standaloneTelemetry?.terminal(input);
|
|
6763
|
+
},
|
|
6764
|
+
performance: (input) => {
|
|
6765
|
+
if (activeTelemetry) activeTelemetry.performance(input);
|
|
6766
|
+
else standaloneTelemetry?.performance(input);
|
|
6767
|
+
},
|
|
6768
|
+
now: () => activeTelemetry?.now() ?? standaloneTelemetry?.now() ?? 0,
|
|
6769
|
+
elapsed: (startedAt) => activeTelemetry?.elapsed(startedAt) ?? Math.max(0, (standaloneTelemetry?.now() ?? startedAt) - startedAt)
|
|
6770
|
+
};
|
|
6771
|
+
const processingStartedAt = telemetrySource.now();
|
|
6772
|
+
let recoveryFlow = Boolean(
|
|
6773
|
+
options?.initialAutoProcessingError || options?.initialAutoProcessingPending
|
|
6774
|
+
);
|
|
6775
|
+
let recoveryStarted = false;
|
|
6776
|
+
let recoveryStartedAt;
|
|
6777
|
+
const startRecovery = () => {
|
|
6778
|
+
if (recoveryStarted) return;
|
|
6779
|
+
recoveryStarted = true;
|
|
6780
|
+
recoveryFlow = true;
|
|
6781
|
+
recoveryStartedAt = telemetrySource.now();
|
|
6782
|
+
telemetrySource.log({
|
|
6783
|
+
name: "checkout.recovery.started",
|
|
6784
|
+
stage: "recovery",
|
|
6785
|
+
paymentMethodCategory: "saved"
|
|
6786
|
+
});
|
|
6787
|
+
telemetrySource.log({
|
|
6788
|
+
name: "operation.recovery.started",
|
|
6789
|
+
stage: "recovery",
|
|
6790
|
+
paymentMethodCategory: "saved"
|
|
6791
|
+
});
|
|
6792
|
+
};
|
|
6793
|
+
let processingFinished = false;
|
|
6794
|
+
const finishProcessing = () => {
|
|
6795
|
+
if (processingFinished) return;
|
|
6796
|
+
processingFinished = true;
|
|
6797
|
+
telemetrySource.log({
|
|
6798
|
+
name: "payment.processing.completed",
|
|
6799
|
+
stage: "processing",
|
|
6800
|
+
paymentMethodCategory: "saved"
|
|
6801
|
+
});
|
|
6802
|
+
telemetrySource.performance({
|
|
6803
|
+
stage: "processing",
|
|
6804
|
+
durationMs: telemetrySource.elapsed(processingStartedAt),
|
|
6805
|
+
durationMode: "machine",
|
|
6806
|
+
paymentMethodCategory: "saved"
|
|
6807
|
+
});
|
|
6808
|
+
if (recoveryStartedAt !== void 0) {
|
|
6809
|
+
telemetrySource.performance({
|
|
6810
|
+
stage: "recovery",
|
|
6811
|
+
durationMs: telemetrySource.elapsed(recoveryStartedAt),
|
|
6812
|
+
durationMode: "machine",
|
|
6813
|
+
paymentMethodCategory: "saved"
|
|
6814
|
+
});
|
|
6815
|
+
}
|
|
6816
|
+
};
|
|
6817
|
+
telemetrySource.log({
|
|
6818
|
+
name: "payment.method.selected",
|
|
6819
|
+
stage: "processing",
|
|
6820
|
+
paymentMethodCategory: "saved"
|
|
6821
|
+
});
|
|
6822
|
+
telemetrySource.log({
|
|
6823
|
+
name: "payment.processing.started",
|
|
6824
|
+
stage: "processing",
|
|
6825
|
+
paymentMethodCategory: "saved"
|
|
6826
|
+
});
|
|
6827
|
+
if (recoveryFlow) {
|
|
6828
|
+
startRecovery();
|
|
6829
|
+
}
|
|
6830
|
+
let completionCallback;
|
|
5745
6831
|
try {
|
|
5746
6832
|
const redirectResult = getRedirectResultFromCheckoutProcessError(options?.initialAutoProcessingError);
|
|
5747
6833
|
let paymentResult;
|
|
5748
6834
|
if (redirectResult) {
|
|
6835
|
+
startRecovery();
|
|
5749
6836
|
if (redirectResult.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
|
|
5750
6837
|
persistPayPalResumeState({
|
|
5751
6838
|
sessionId: activeSessionId2,
|
|
@@ -5759,13 +6846,14 @@ function FloPayCheckout({
|
|
|
5759
6846
|
attempt3DS: options?.attempt3DS,
|
|
5760
6847
|
billingApiUrl: resolvedBillingUrl,
|
|
5761
6848
|
sessionId: activeSessionId2,
|
|
5762
|
-
session: sess
|
|
6849
|
+
session: sess,
|
|
6850
|
+
telemetry: false
|
|
5763
6851
|
});
|
|
5764
6852
|
if (redirectResult.type === "paypal_redirect_required") {
|
|
5765
6853
|
clearPayPalResumeState();
|
|
5766
6854
|
}
|
|
5767
6855
|
} else if (options?.initialAutoProcessingPending) {
|
|
5768
|
-
const api = new import_js4.PaymentAPI(resolvedBillingUrl);
|
|
6856
|
+
const api = new import_js4.PaymentAPI(resolvedBillingUrl, { telemetry: false });
|
|
5769
6857
|
const completed = await api.waitForCheckoutSessionCompletion(options.initialAutoProcessingPending.sessionId, {
|
|
5770
6858
|
initialDelayMs: options.initialAutoProcessingPending.retryAfterMs
|
|
5771
6859
|
});
|
|
@@ -5793,11 +6881,13 @@ function FloPayCheckout({
|
|
|
5793
6881
|
const result = await processSavedPaymentForMode({
|
|
5794
6882
|
billingApiUrl: resolvedBillingUrl,
|
|
5795
6883
|
sessionId: activeSessionId2,
|
|
5796
|
-
session: sess
|
|
6884
|
+
session: sess,
|
|
6885
|
+
telemetry: false
|
|
5797
6886
|
});
|
|
5798
6887
|
if (result.type === "success") {
|
|
5799
6888
|
paymentResult = result.result;
|
|
5800
6889
|
} else {
|
|
6890
|
+
startRecovery();
|
|
5801
6891
|
if (result.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
|
|
5802
6892
|
persistPayPalResumeState({
|
|
5803
6893
|
sessionId: activeSessionId2,
|
|
@@ -5811,7 +6901,8 @@ function FloPayCheckout({
|
|
|
5811
6901
|
attempt3DS: options?.attempt3DS,
|
|
5812
6902
|
billingApiUrl: resolvedBillingUrl,
|
|
5813
6903
|
sessionId: activeSessionId2,
|
|
5814
|
-
session: sess
|
|
6904
|
+
session: sess,
|
|
6905
|
+
telemetry: false
|
|
5815
6906
|
});
|
|
5816
6907
|
if (result.type === "paypal_redirect_required") {
|
|
5817
6908
|
clearPayPalResumeState();
|
|
@@ -5819,20 +6910,57 @@ function FloPayCheckout({
|
|
|
5819
6910
|
}
|
|
5820
6911
|
}
|
|
5821
6912
|
if (activeSessionId2) markSessionRecentlyCompleted(activeSessionId2);
|
|
6913
|
+
finishProcessing();
|
|
6914
|
+
if (recoveryFlow) {
|
|
6915
|
+
telemetrySource.log({
|
|
6916
|
+
name: "checkout.recovery.completed",
|
|
6917
|
+
stage: "recovery",
|
|
6918
|
+
paymentMethodCategory: "saved"
|
|
6919
|
+
});
|
|
6920
|
+
telemetrySource.log({
|
|
6921
|
+
name: "operation.recovery.completed",
|
|
6922
|
+
stage: "recovery",
|
|
6923
|
+
paymentMethodCategory: "saved"
|
|
6924
|
+
});
|
|
6925
|
+
}
|
|
6926
|
+
telemetrySource.terminal({
|
|
6927
|
+
outcome: "payment_succeeded",
|
|
6928
|
+
paymentMethodCategory: "saved"
|
|
6929
|
+
});
|
|
5822
6930
|
setModeOverlayStatus("success");
|
|
5823
6931
|
await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
|
|
5824
|
-
onCompleteRef.current?.(paymentResult);
|
|
5825
|
-
return true;
|
|
6932
|
+
completionCallback = () => onCompleteRef.current?.(paymentResult);
|
|
5826
6933
|
} catch (err) {
|
|
5827
6934
|
const floPayErr = normalizeSavedPaymentError(err);
|
|
5828
6935
|
const method = floPayErr.checkoutMethod ?? DEFAULT_SAVED_PAYMENT_DECLINE_METHOD;
|
|
5829
6936
|
setModeError(floPayErr.message);
|
|
5830
6937
|
setModeOverlayError(floPayErr.message);
|
|
5831
6938
|
if (options?.fallbackToFull) {
|
|
6939
|
+
telemetrySource.log({
|
|
6940
|
+
name: "operation.fallback",
|
|
6941
|
+
stage: "recovery",
|
|
6942
|
+
paymentMethodCategory: "saved"
|
|
6943
|
+
});
|
|
5832
6944
|
setCurrentMode("full");
|
|
5833
6945
|
replaceCheckoutModeQueryParam("full");
|
|
5834
6946
|
}
|
|
5835
|
-
|
|
6947
|
+
finishProcessing();
|
|
6948
|
+
const expectedDecline = Boolean(
|
|
6949
|
+
floPayErr.declineCode || floPayErr.code?.toLowerCase().includes("declin")
|
|
6950
|
+
);
|
|
6951
|
+
if (expectedDecline) {
|
|
6952
|
+
telemetrySource.terminal({
|
|
6953
|
+
outcome: "payment_declined",
|
|
6954
|
+
paymentMethodCategory: "saved"
|
|
6955
|
+
});
|
|
6956
|
+
} else {
|
|
6957
|
+
telemetrySource.error({
|
|
6958
|
+
errorCode: recoveryFlow ? "RECOVERY_FAILED" : "PAYMENT_PROCESSING_FAILED",
|
|
6959
|
+
stage: recoveryFlow ? "recovery" : "processing",
|
|
6960
|
+
paymentMethodCategory: "saved"
|
|
6961
|
+
});
|
|
6962
|
+
}
|
|
6963
|
+
invokeMerchantCallback(() => onErrorRef.current?.(floPayErr));
|
|
5836
6964
|
emitDecline(method, floPayErr, {
|
|
5837
6965
|
code: floPayErr.code,
|
|
5838
6966
|
declineCode: floPayErr.declineCode
|
|
@@ -5842,16 +6970,35 @@ function FloPayCheckout({
|
|
|
5842
6970
|
sleep(PROCESSING_OVERLAY_ERROR_DELAY_MS),
|
|
5843
6971
|
options?.ensureProvidersReady ? options.ensureProvidersReady() : Promise.resolve()
|
|
5844
6972
|
]);
|
|
6973
|
+
if (recoveryFlow) {
|
|
6974
|
+
telemetrySource.log({
|
|
6975
|
+
name: "checkout.recovery.completed",
|
|
6976
|
+
stage: "recovery",
|
|
6977
|
+
paymentMethodCategory: "saved"
|
|
6978
|
+
});
|
|
6979
|
+
telemetrySource.log({
|
|
6980
|
+
name: "operation.recovery.completed",
|
|
6981
|
+
stage: "recovery",
|
|
6982
|
+
paymentMethodCategory: "saved"
|
|
6983
|
+
});
|
|
6984
|
+
}
|
|
5845
6985
|
return false;
|
|
5846
6986
|
} finally {
|
|
6987
|
+
finishProcessing();
|
|
6988
|
+
if (standaloneTelemetry) finishStandaloneTelemetry(standaloneTelemetry);
|
|
5847
6989
|
setModeOverlayStatus(null);
|
|
5848
6990
|
setModeOverlayError(null);
|
|
5849
6991
|
}
|
|
6992
|
+
invokeMerchantCallback(completionCallback);
|
|
6993
|
+
return true;
|
|
5850
6994
|
},
|
|
5851
6995
|
[
|
|
5852
6996
|
emitDecline,
|
|
6997
|
+
invokeMerchantCallback,
|
|
5853
6998
|
normalizeSavedPaymentError,
|
|
5854
|
-
resolvedBillingUrl
|
|
6999
|
+
resolvedBillingUrl,
|
|
7000
|
+
telemetry,
|
|
7001
|
+
telemetryCheckoutContext
|
|
5855
7002
|
]
|
|
5856
7003
|
);
|
|
5857
7004
|
(0, import_react10.useEffect)(() => {
|
|
@@ -5869,6 +7016,8 @@ function FloPayCheckout({
|
|
|
5869
7016
|
}
|
|
5870
7017
|
paypalResumeAttempted.current = true;
|
|
5871
7018
|
void (async () => {
|
|
7019
|
+
let resumeTelemetry;
|
|
7020
|
+
let redirectResumeStartedAt = 0;
|
|
5872
7021
|
setModeError(null);
|
|
5873
7022
|
setModeOverlayError(null);
|
|
5874
7023
|
setModeOverlayStatus("processing");
|
|
@@ -5876,7 +7025,11 @@ function FloPayCheckout({
|
|
|
5876
7025
|
try {
|
|
5877
7026
|
if (params.get("redirect_status") === "failed") {
|
|
5878
7027
|
throw Object.assign(
|
|
5879
|
-
new import_shared8.FloPayError(
|
|
7028
|
+
new import_shared8.FloPayError(
|
|
7029
|
+
"PayPal payment was declined. Please try again.",
|
|
7030
|
+
"api_error",
|
|
7031
|
+
{ declineCode: "paypal_redirect_failed" }
|
|
7032
|
+
),
|
|
5880
7033
|
{ checkoutMethod: "paypal" }
|
|
5881
7034
|
);
|
|
5882
7035
|
}
|
|
@@ -5887,7 +7040,22 @@ function FloPayCheckout({
|
|
|
5887
7040
|
publishableKey: resumeState.publishableKey,
|
|
5888
7041
|
paypalPublishableKey: resumeState.paypalPublishableKey,
|
|
5889
7042
|
billingApiUrl: resolvedBillingUrl,
|
|
5890
|
-
locale
|
|
7043
|
+
locale,
|
|
7044
|
+
telemetry
|
|
7045
|
+
});
|
|
7046
|
+
resumeTelemetry = getFloPayTelemetryBridge(resumePaypalFlopay ?? resumeFlopay);
|
|
7047
|
+
redirectResumeStartedAt = resumeTelemetry?.now() ?? 0;
|
|
7048
|
+
resumeTelemetry?.log({
|
|
7049
|
+
name: "provider.redirect.resumed",
|
|
7050
|
+
stage: "redirect_resume",
|
|
7051
|
+
provider: "paypal",
|
|
7052
|
+
paymentMethodCategory: "paypal"
|
|
7053
|
+
});
|
|
7054
|
+
resumeTelemetry?.log({
|
|
7055
|
+
name: "operation.recovery.started",
|
|
7056
|
+
stage: "recovery",
|
|
7057
|
+
provider: "paypal",
|
|
7058
|
+
paymentMethodCategory: "paypal"
|
|
5891
7059
|
});
|
|
5892
7060
|
const paypalStripe = (resumePaypalFlopay ?? resumeFlopay)?.getRawProvider();
|
|
5893
7061
|
if (!paypalStripe) {
|
|
@@ -5917,7 +7085,7 @@ function FloPayCheckout({
|
|
|
5917
7085
|
const paymentMethodId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
|
|
5918
7086
|
let finalResultStatus = resultStatus;
|
|
5919
7087
|
if (resumeState.sessionId) {
|
|
5920
|
-
const resumeApi = new import_js4.PaymentAPI(resolvedBillingUrl);
|
|
7088
|
+
const resumeApi = new import_js4.PaymentAPI(resolvedBillingUrl, { telemetry: false });
|
|
5921
7089
|
const resumeSessionResult = await resumeApi.getUnifiedCheckoutSession(resumeState.sessionId);
|
|
5922
7090
|
const resumeSession = resumeSessionResult.data.session;
|
|
5923
7091
|
if (resumeSession && resumeSession.status !== "complete") {
|
|
@@ -5925,6 +7093,7 @@ function FloPayCheckout({
|
|
|
5925
7093
|
billingApiUrl: resolvedBillingUrl,
|
|
5926
7094
|
sessionId: resumeState.sessionId,
|
|
5927
7095
|
session: resumeSession,
|
|
7096
|
+
telemetry: false,
|
|
5928
7097
|
tokenizedData: {
|
|
5929
7098
|
id: paymentMethodId ?? paymentIntent.id,
|
|
5930
7099
|
type: "card",
|
|
@@ -5942,20 +7111,84 @@ function FloPayCheckout({
|
|
|
5942
7111
|
}
|
|
5943
7112
|
}
|
|
5944
7113
|
if (resumeState.sessionId) markSessionRecentlyCompleted(resumeState.sessionId);
|
|
7114
|
+
resumeTelemetry?.log({
|
|
7115
|
+
name: "operation.recovery.completed",
|
|
7116
|
+
stage: "recovery",
|
|
7117
|
+
provider: "paypal",
|
|
7118
|
+
paymentMethodCategory: "paypal"
|
|
7119
|
+
});
|
|
7120
|
+
resumeTelemetry?.performance({
|
|
7121
|
+
stage: "redirect_resume",
|
|
7122
|
+
durationMs: resumeTelemetry.elapsed(redirectResumeStartedAt),
|
|
7123
|
+
durationMode: "machine",
|
|
7124
|
+
provider: "paypal",
|
|
7125
|
+
paymentMethodCategory: "paypal"
|
|
7126
|
+
});
|
|
7127
|
+
resumeTelemetry?.terminal({
|
|
7128
|
+
outcome: "payment_succeeded",
|
|
7129
|
+
provider: "paypal",
|
|
7130
|
+
paymentMethodCategory: "paypal"
|
|
7131
|
+
});
|
|
5945
7132
|
setModeOverlayStatus("success");
|
|
5946
7133
|
await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
|
|
5947
|
-
onCompleteRef.current?.({
|
|
7134
|
+
invokeMerchantCallback(() => onCompleteRef.current?.({
|
|
5948
7135
|
status: finalResultStatus,
|
|
5949
7136
|
paymentIntentId: paymentIntent.id,
|
|
5950
7137
|
paymentMethodId,
|
|
5951
7138
|
checkoutMethod: "paypal"
|
|
5952
|
-
});
|
|
7139
|
+
}));
|
|
5953
7140
|
} catch (err) {
|
|
5954
7141
|
const floPayErr = normalizeSavedPaymentError(err);
|
|
5955
7142
|
const method = floPayErr.checkoutMethod ?? "paypal";
|
|
7143
|
+
const expectedDecline = Boolean(
|
|
7144
|
+
floPayErr.declineCode || floPayErr.code?.toLowerCase().includes("declin")
|
|
7145
|
+
);
|
|
7146
|
+
if (resumeTelemetry) {
|
|
7147
|
+
resumeTelemetry.performance({
|
|
7148
|
+
stage: "redirect_resume",
|
|
7149
|
+
durationMs: resumeTelemetry.elapsed(redirectResumeStartedAt),
|
|
7150
|
+
durationMode: "machine",
|
|
7151
|
+
provider: "paypal",
|
|
7152
|
+
paymentMethodCategory: "paypal"
|
|
7153
|
+
});
|
|
7154
|
+
if (expectedDecline) {
|
|
7155
|
+
resumeTelemetry.terminal({
|
|
7156
|
+
outcome: "payment_declined",
|
|
7157
|
+
provider: "paypal",
|
|
7158
|
+
paymentMethodCategory: "paypal"
|
|
7159
|
+
});
|
|
7160
|
+
} else {
|
|
7161
|
+
resumeTelemetry.error({
|
|
7162
|
+
errorCode: "REDIRECT_RESUME_FAILED",
|
|
7163
|
+
stage: "redirect_resume",
|
|
7164
|
+
provider: "paypal",
|
|
7165
|
+
paymentMethodCategory: "paypal"
|
|
7166
|
+
});
|
|
7167
|
+
}
|
|
7168
|
+
} else if (expectedDecline) {
|
|
7169
|
+
const reporter = createStandaloneTelemetryReporter(
|
|
7170
|
+
resolvedBillingUrl,
|
|
7171
|
+
telemetry,
|
|
7172
|
+
telemetryCheckoutContext
|
|
7173
|
+
);
|
|
7174
|
+
reporter.terminal({
|
|
7175
|
+
outcome: "payment_declined",
|
|
7176
|
+
provider: "paypal",
|
|
7177
|
+
paymentMethodCategory: "paypal"
|
|
7178
|
+
});
|
|
7179
|
+
finishStandaloneTelemetry(reporter);
|
|
7180
|
+
} else {
|
|
7181
|
+
reportStandaloneTelemetryError(
|
|
7182
|
+
resolvedBillingUrl,
|
|
7183
|
+
telemetry,
|
|
7184
|
+
telemetryCheckoutContext,
|
|
7185
|
+
"REDIRECT_RESUME_FAILED",
|
|
7186
|
+
"redirect_resume"
|
|
7187
|
+
);
|
|
7188
|
+
}
|
|
5956
7189
|
setModeError(floPayErr.message);
|
|
5957
7190
|
setModeOverlayError(floPayErr.message);
|
|
5958
|
-
onErrorRef.current?.(floPayErr);
|
|
7191
|
+
invokeMerchantCallback(() => onErrorRef.current?.(floPayErr));
|
|
5959
7192
|
emitDecline(method, floPayErr, {
|
|
5960
7193
|
code: floPayErr.code,
|
|
5961
7194
|
declineCode: floPayErr.declineCode
|
|
@@ -5970,7 +7203,7 @@ function FloPayCheckout({
|
|
|
5970
7203
|
setConfirmProcessing(false);
|
|
5971
7204
|
}
|
|
5972
7205
|
})();
|
|
5973
|
-
}, [emitDecline, locale, normalizeSavedPaymentError, resolvedBillingUrl]);
|
|
7206
|
+
}, [emitDecline, invokeMerchantCallback, locale, normalizeSavedPaymentError, resolvedBillingUrl, telemetry]);
|
|
5974
7207
|
const initializedHashRef = (0, import_react10.useRef)(null);
|
|
5975
7208
|
function hashCreateParams(params) {
|
|
5976
7209
|
const key = JSON.stringify({
|
|
@@ -6020,42 +7253,105 @@ function FloPayCheckout({
|
|
|
6020
7253
|
setModeOverlayStatus(null);
|
|
6021
7254
|
}, [createSessionHash, initialErrorMessage, sessionIdProp]);
|
|
6022
7255
|
async function resolveInlineSession(params, cacheKey) {
|
|
6023
|
-
const
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
7256
|
+
const reporter = createStandaloneTelemetryReporter(
|
|
7257
|
+
resolvedBillingUrl,
|
|
7258
|
+
telemetry,
|
|
7259
|
+
telemetryCheckoutContext
|
|
7260
|
+
);
|
|
7261
|
+
const api = new import_js4.PaymentAPI(resolvedBillingUrl, { telemetry: false });
|
|
7262
|
+
try {
|
|
7263
|
+
const cached = readCachedInlineSession(cacheKey);
|
|
7264
|
+
reporter.log({
|
|
7265
|
+
name: cached ? "operation.cache.hit" : "operation.cache.miss",
|
|
7266
|
+
stage: "session_create",
|
|
7267
|
+
requestCategory: "session_create"
|
|
7268
|
+
});
|
|
7269
|
+
let sid = cached?.sid ?? null;
|
|
7270
|
+
let realResult = null;
|
|
7271
|
+
if (sid) {
|
|
7272
|
+
try {
|
|
7273
|
+
realResult = await api.getUnifiedCheckoutSession(sid, cached?.nonce);
|
|
7274
|
+
const status = realResult.data.session?.status;
|
|
7275
|
+
if (status === "complete") {
|
|
7276
|
+
if (wasSessionRecentlyCompleted(sid)) {
|
|
7277
|
+
return { sid, result: realResult };
|
|
7278
|
+
}
|
|
7279
|
+
clearCachedInlineSession(cacheKey);
|
|
7280
|
+
sid = null;
|
|
7281
|
+
realResult = null;
|
|
6034
7282
|
}
|
|
7283
|
+
} catch {
|
|
7284
|
+
reporter.log({
|
|
7285
|
+
name: "operation.fallback",
|
|
7286
|
+
stage: "session_read",
|
|
7287
|
+
requestCategory: "session_read"
|
|
7288
|
+
});
|
|
6035
7289
|
clearCachedInlineSession(cacheKey);
|
|
6036
7290
|
sid = null;
|
|
6037
|
-
realResult = null;
|
|
6038
7291
|
}
|
|
6039
|
-
} catch {
|
|
6040
|
-
clearCachedInlineSession(cacheKey);
|
|
6041
|
-
sid = null;
|
|
6042
7292
|
}
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
7293
|
+
if (!sid) {
|
|
7294
|
+
const sessionCreateStartedAt = reporter.now();
|
|
7295
|
+
reporter.log({
|
|
7296
|
+
name: "session.create.started",
|
|
7297
|
+
stage: "session_create",
|
|
7298
|
+
requestCategory: "session_create"
|
|
7299
|
+
});
|
|
7300
|
+
const paramsWithAnalytics = {
|
|
7301
|
+
...params,
|
|
7302
|
+
avsCheck: !!enableAVS,
|
|
7303
|
+
avsConfig: typeof enableAVS === "object" ? enableAVS : void 0,
|
|
7304
|
+
checkoutType: "embedded_checkout",
|
|
7305
|
+
checkoutLayout: children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout"
|
|
7306
|
+
};
|
|
7307
|
+
try {
|
|
7308
|
+
realResult = await api.createAndFetchSession(paramsWithAnalytics);
|
|
7309
|
+
reporter.log({
|
|
7310
|
+
name: "session.request.completed",
|
|
7311
|
+
stage: "session_complete",
|
|
7312
|
+
requestCategory: "session_create",
|
|
7313
|
+
statusClass: "2xx"
|
|
7314
|
+
});
|
|
7315
|
+
reporter.performance({
|
|
7316
|
+
stage: "session_create",
|
|
7317
|
+
durationMs: reporter.now() - sessionCreateStartedAt,
|
|
7318
|
+
durationMode: "machine",
|
|
7319
|
+
requestCategory: "session_create",
|
|
7320
|
+
statusClass: "2xx"
|
|
7321
|
+
});
|
|
7322
|
+
} catch (error) {
|
|
7323
|
+
reporter.performance({
|
|
7324
|
+
stage: "session_create",
|
|
7325
|
+
durationMs: reporter.now() - sessionCreateStartedAt,
|
|
7326
|
+
durationMode: "machine",
|
|
7327
|
+
requestCategory: "session_create",
|
|
7328
|
+
statusClass: "network_error"
|
|
7329
|
+
});
|
|
7330
|
+
if (error instanceof import_shared8.FloPayError && error.type === "validation_error") {
|
|
7331
|
+
reporter.terminal({
|
|
7332
|
+
outcome: "validation_rejected",
|
|
7333
|
+
stage: "session_create",
|
|
7334
|
+
paymentMethodCategory: "unknown"
|
|
7335
|
+
});
|
|
7336
|
+
} else {
|
|
7337
|
+
reporter.error({
|
|
7338
|
+
errorCode: "CHECKOUT_SESSION_CREATE_FAILED",
|
|
7339
|
+
stage: "session_create",
|
|
7340
|
+
paymentMethodCategory: "unknown",
|
|
7341
|
+
requestCategory: "session_create"
|
|
7342
|
+
});
|
|
7343
|
+
}
|
|
7344
|
+
throw error;
|
|
7345
|
+
}
|
|
7346
|
+
sid = realResult.data.session?.id ?? "";
|
|
7347
|
+
if (sid) {
|
|
7348
|
+
persistCachedInlineSession(cacheKey, sid, realResult.data.session?.clientSecret);
|
|
7349
|
+
}
|
|
6056
7350
|
}
|
|
7351
|
+
return { sid: sid ?? "", result: realResult };
|
|
7352
|
+
} finally {
|
|
7353
|
+
finishStandaloneTelemetry(reporter);
|
|
6057
7354
|
}
|
|
6058
|
-
return { sid: sid ?? "", result: realResult };
|
|
6059
7355
|
}
|
|
6060
7356
|
const bootstrapInlineSession = (0, import_react10.useCallback)(
|
|
6061
7357
|
async (patch) => {
|
|
@@ -6101,7 +7397,8 @@ function FloPayCheckout({
|
|
|
6101
7397
|
publishableKey,
|
|
6102
7398
|
paypalPublishableKey,
|
|
6103
7399
|
billingApiUrl: resolvedBillingUrl,
|
|
6104
|
-
locale
|
|
7400
|
+
locale,
|
|
7401
|
+
telemetry
|
|
6105
7402
|
});
|
|
6106
7403
|
flopayRef.current = instance;
|
|
6107
7404
|
setFloPay(instance);
|
|
@@ -6116,7 +7413,7 @@ function FloPayCheckout({
|
|
|
6116
7413
|
}
|
|
6117
7414
|
return resolved;
|
|
6118
7415
|
},
|
|
6119
|
-
[locale, resolvedBillingUrl]
|
|
7416
|
+
[locale, resolvedBillingUrl, telemetry, telemetryCheckoutContext]
|
|
6120
7417
|
);
|
|
6121
7418
|
const handleInlineSessionPatch = (0, import_react10.useCallback)(async (patch) => {
|
|
6122
7419
|
if (!hasInlineSessionPatchData(patch) || cardBootstrapPending) {
|
|
@@ -6195,7 +7492,9 @@ function FloPayCheckout({
|
|
|
6195
7492
|
setModeOverlayStatus("success");
|
|
6196
7493
|
await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
|
|
6197
7494
|
if (!cancelled) {
|
|
6198
|
-
|
|
7495
|
+
invokeMerchantCallback(() => {
|
|
7496
|
+
onCompleteRef.current?.({ status: "succeeded" });
|
|
7497
|
+
});
|
|
6199
7498
|
setModeOverlayStatus(null);
|
|
6200
7499
|
setModeOverlayError(null);
|
|
6201
7500
|
}
|
|
@@ -6215,8 +7514,9 @@ function FloPayCheckout({
|
|
|
6215
7514
|
}
|
|
6216
7515
|
setIsLoading(true);
|
|
6217
7516
|
async function init() {
|
|
7517
|
+
let sessionReadCompleted = false;
|
|
6218
7518
|
try {
|
|
6219
|
-
const api = new import_js4.PaymentAPI(resolvedBillingUrl);
|
|
7519
|
+
const api = new import_js4.PaymentAPI(resolvedBillingUrl, { telemetry: false });
|
|
6220
7520
|
const result = await api.getUnifiedCheckoutSession(activeSessionId, nonceProp);
|
|
6221
7521
|
if (cancelled) return;
|
|
6222
7522
|
setUnified(result);
|
|
@@ -6227,7 +7527,9 @@ function FloPayCheckout({
|
|
|
6227
7527
|
}
|
|
6228
7528
|
if (sess.status === "complete") {
|
|
6229
7529
|
setIsLoading(false);
|
|
6230
|
-
|
|
7530
|
+
invokeMerchantCallback(() => {
|
|
7531
|
+
onSessionCompletedRef.current?.(sess.successUrl ?? "");
|
|
7532
|
+
});
|
|
6231
7533
|
return;
|
|
6232
7534
|
}
|
|
6233
7535
|
if (sess.status === "expired") {
|
|
@@ -6235,6 +7537,7 @@ function FloPayCheckout({
|
|
|
6235
7537
|
code: "checkout_session_expired"
|
|
6236
7538
|
});
|
|
6237
7539
|
}
|
|
7540
|
+
sessionReadCompleted = true;
|
|
6238
7541
|
const effectiveMode = checkoutModeProp ?? sess.checkoutMode ?? "full";
|
|
6239
7542
|
setCurrentMode(effectiveMode);
|
|
6240
7543
|
const hasPayPalRedirectParams = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("payment_intent");
|
|
@@ -6266,6 +7569,23 @@ function FloPayCheckout({
|
|
|
6266
7569
|
if (!cancelled) setIsLoading(false);
|
|
6267
7570
|
} catch (err) {
|
|
6268
7571
|
if (cancelled) return;
|
|
7572
|
+
if (!(err instanceof import_shared8.FloPayError)) {
|
|
7573
|
+
reportStandaloneTelemetryError(
|
|
7574
|
+
resolvedBillingUrl,
|
|
7575
|
+
telemetry,
|
|
7576
|
+
telemetryCheckoutContext,
|
|
7577
|
+
"INTERNAL_SDK_ERROR",
|
|
7578
|
+
"checkout_mount"
|
|
7579
|
+
);
|
|
7580
|
+
} else if (!sessionReadCompleted && !isExpectedExistingSessionError(err)) {
|
|
7581
|
+
reportStandaloneTelemetryError(
|
|
7582
|
+
resolvedBillingUrl,
|
|
7583
|
+
telemetry,
|
|
7584
|
+
telemetryCheckoutContext,
|
|
7585
|
+
"NETWORK_REQUEST_FAILED",
|
|
7586
|
+
"session_read"
|
|
7587
|
+
);
|
|
7588
|
+
}
|
|
6269
7589
|
const floPayErr = err instanceof import_shared8.FloPayError ? err : new import_shared8.FloPayError(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
|
|
6270
7590
|
setLoadError(floPayErr);
|
|
6271
7591
|
setIsLoading(false);
|
|
@@ -6287,7 +7607,8 @@ function FloPayCheckout({
|
|
|
6287
7607
|
publishableKey,
|
|
6288
7608
|
paypalPublishableKey,
|
|
6289
7609
|
billingApiUrl: resolvedBillingUrl,
|
|
6290
|
-
locale
|
|
7610
|
+
locale,
|
|
7611
|
+
telemetry
|
|
6291
7612
|
});
|
|
6292
7613
|
flopayRef.current = instance;
|
|
6293
7614
|
setFloPay(instance);
|
|
@@ -6304,6 +7625,7 @@ function FloPayCheckout({
|
|
|
6304
7625
|
createSessionHash,
|
|
6305
7626
|
effectiveCreateSessionMode,
|
|
6306
7627
|
initSessionDependency,
|
|
7628
|
+
invokeMerchantCallback,
|
|
6307
7629
|
nonceProp,
|
|
6308
7630
|
runSavedPaymentFlow
|
|
6309
7631
|
]);
|
|
@@ -6471,7 +7793,7 @@ function FloPayCheckout({
|
|
|
6471
7793
|
}
|
|
6472
7794
|
),
|
|
6473
7795
|
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
6474
|
-
|
|
7796
|
+
InstrumentedDirectPayPalButton,
|
|
6475
7797
|
{
|
|
6476
7798
|
sessionId: activeSessionId,
|
|
6477
7799
|
nonce: session.clientSecret || void 0,
|
|
@@ -6486,6 +7808,8 @@ function FloPayCheckout({
|
|
|
6486
7808
|
onDecline,
|
|
6487
7809
|
onButtonClick,
|
|
6488
7810
|
session,
|
|
7811
|
+
telemetry,
|
|
7812
|
+
telemetryContext: telemetryCheckoutContext,
|
|
6489
7813
|
debug
|
|
6490
7814
|
}
|
|
6491
7815
|
)
|
|
@@ -7553,7 +8877,7 @@ function FloPayAutomaticPaymentButton({
|
|
|
7553
8877
|
};
|
|
7554
8878
|
}, []);
|
|
7555
8879
|
(0, import_react13.useEffect)(() => {
|
|
7556
|
-
if (
|
|
8880
|
+
if (typeof window === "undefined") {
|
|
7557
8881
|
return;
|
|
7558
8882
|
}
|
|
7559
8883
|
const handleKeyDown = (event) => {
|
|
@@ -7565,7 +8889,7 @@ function FloPayAutomaticPaymentButton({
|
|
|
7565
8889
|
return () => {
|
|
7566
8890
|
window.removeEventListener("keydown", handleKeyDown);
|
|
7567
8891
|
};
|
|
7568
|
-
}, [
|
|
8892
|
+
}, []);
|
|
7569
8893
|
const emitDecline = (0, import_react13.useCallback)((error, method = DEFAULT_SAVED_PAYMENT_DECLINE_METHOD) => {
|
|
7570
8894
|
onDeclineRef.current?.(buildDeclineEvent(method, error, {
|
|
7571
8895
|
code: error.code,
|