@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/provider.tsx
|
|
2
|
-
import { useEffect, useState, useMemo } from "react";
|
|
2
|
+
import { useCallback, useEffect, useState, useMemo, useRef } from "react";
|
|
3
3
|
import { resolveBillingApiUrl } from "@flopay/shared";
|
|
4
4
|
|
|
5
5
|
// src/context.ts
|
|
@@ -16,6 +16,59 @@ var CheckoutContext = createContext({
|
|
|
16
16
|
error: null
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
// src/telemetry-bridge.ts
|
|
20
|
+
var FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
|
|
21
|
+
var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
|
|
22
|
+
function noopTelemetryBridge() {
|
|
23
|
+
return {
|
|
24
|
+
error: () => {
|
|
25
|
+
},
|
|
26
|
+
log: () => {
|
|
27
|
+
},
|
|
28
|
+
performance: () => {
|
|
29
|
+
},
|
|
30
|
+
terminal: () => {
|
|
31
|
+
},
|
|
32
|
+
now: () => globalThis.performance?.now() ?? 0,
|
|
33
|
+
elapsed: (startedAt) => Math.max(0, (globalThis.performance?.now() ?? startedAt) - startedAt),
|
|
34
|
+
setCheckoutContext: () => {
|
|
35
|
+
},
|
|
36
|
+
beginCheckout: () => globalThis.performance?.now() ?? 0,
|
|
37
|
+
disable: () => {
|
|
38
|
+
},
|
|
39
|
+
flush: async () => {
|
|
40
|
+
},
|
|
41
|
+
destroy: () => {
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function normalizeTelemetryBridge(source) {
|
|
46
|
+
const fallback = noopTelemetryBridge();
|
|
47
|
+
const bind = (candidate, defaultValue) => candidate ? candidate.bind(source) : defaultValue;
|
|
48
|
+
const now = bind(source.now, fallback.now);
|
|
49
|
+
return {
|
|
50
|
+
error: bind(source.error, fallback.error),
|
|
51
|
+
log: bind(source.log, fallback.log),
|
|
52
|
+
performance: bind(source.performance, fallback.performance),
|
|
53
|
+
terminal: bind(source.terminal, fallback.terminal),
|
|
54
|
+
now,
|
|
55
|
+
elapsed: source.elapsed ? source.elapsed.bind(source) : (startedAt) => Math.max(0, now() - startedAt),
|
|
56
|
+
setCheckoutContext: bind(source.setCheckoutContext, fallback.setCheckoutContext),
|
|
57
|
+
beginCheckout: bind(source.beginCheckout, fallback.beginCheckout),
|
|
58
|
+
disable: bind(source.disable, fallback.disable),
|
|
59
|
+
flush: bind(source.flush, fallback.flush),
|
|
60
|
+
destroy: bind(source.destroy, fallback.destroy)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function createTelemetryBridge(options) {
|
|
64
|
+
const factory = globalThis[TELEMETRY_REPORTER_FACTORY];
|
|
65
|
+
return normalizeTelemetryBridge(factory?.(options) ?? {});
|
|
66
|
+
}
|
|
67
|
+
function getFloPayTelemetryBridge(floPay) {
|
|
68
|
+
if (!floPay) return void 0;
|
|
69
|
+
return floPay[FLOPAY_TELEMETRY_BRIDGE];
|
|
70
|
+
}
|
|
71
|
+
|
|
19
72
|
// src/provider.tsx
|
|
20
73
|
import { jsx } from "react/jsx-runtime";
|
|
21
74
|
function FloPayProvider({
|
|
@@ -31,6 +84,18 @@ function FloPayProvider({
|
|
|
31
84
|
paypalFloPayProp instanceof Promise || !paypalFloPayProp ? null : paypalFloPayProp
|
|
32
85
|
);
|
|
33
86
|
const [elements, setElements] = useState(null);
|
|
87
|
+
const mountedAt = useRef(null);
|
|
88
|
+
const renderedElements = useRef(null);
|
|
89
|
+
const interactiveElements = useRef(null);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!flopay) return;
|
|
92
|
+
const telemetry = getFloPayTelemetryBridge(flopay);
|
|
93
|
+
mountedAt.current = telemetry?.beginCheckout() ?? telemetry?.now() ?? 0;
|
|
94
|
+
telemetry?.log({ name: "checkout.mount", stage: "checkout_mount" });
|
|
95
|
+
return () => {
|
|
96
|
+
telemetry?.log({ name: "checkout.unmount", stage: "unmount" });
|
|
97
|
+
};
|
|
98
|
+
}, [flopay]);
|
|
34
99
|
useEffect(() => {
|
|
35
100
|
let cancelled = false;
|
|
36
101
|
if (floPayProp instanceof Promise) {
|
|
@@ -91,18 +156,46 @@ function FloPayProvider({
|
|
|
91
156
|
options?.paymentMethodCreation,
|
|
92
157
|
options?.setupFutureUsage
|
|
93
158
|
]);
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!flopay || !elements || renderedElements.current === elements) return;
|
|
161
|
+
renderedElements.current = elements;
|
|
162
|
+
const telemetry = getFloPayTelemetryBridge(flopay);
|
|
163
|
+
telemetry?.log({ name: "checkout.rendered", stage: "checkout_render" });
|
|
164
|
+
telemetry?.performance({
|
|
165
|
+
stage: "checkout_render",
|
|
166
|
+
durationMs: telemetry.elapsed(mountedAt.current ?? 0),
|
|
167
|
+
durationMode: "machine"
|
|
168
|
+
});
|
|
169
|
+
}, [elements, flopay]);
|
|
170
|
+
const reportInteractive = useCallback(() => {
|
|
171
|
+
if (!flopay || !elements || interactiveElements.current === elements) return;
|
|
172
|
+
interactiveElements.current = elements;
|
|
173
|
+
const telemetry = getFloPayTelemetryBridge(flopay);
|
|
174
|
+
telemetry?.log({ name: "checkout.interactive", stage: "checkout_interactive" });
|
|
175
|
+
telemetry?.performance({
|
|
176
|
+
stage: "checkout_interactive",
|
|
177
|
+
durationMs: telemetry.elapsed(mountedAt.current ?? 0),
|
|
178
|
+
durationMode: "machine"
|
|
179
|
+
});
|
|
180
|
+
}, [elements, flopay]);
|
|
94
181
|
const resolvedBillingApiUrl = resolveBillingApiUrl(options?.billingApiUrl);
|
|
95
182
|
const value = useMemo(
|
|
96
|
-
() => ({
|
|
97
|
-
|
|
183
|
+
() => ({
|
|
184
|
+
flopay,
|
|
185
|
+
paypalFlopay,
|
|
186
|
+
elements,
|
|
187
|
+
billingApiUrl: resolvedBillingApiUrl,
|
|
188
|
+
reportInteractive
|
|
189
|
+
}),
|
|
190
|
+
[flopay, paypalFlopay, elements, resolvedBillingApiUrl, reportInteractive]
|
|
98
191
|
);
|
|
99
192
|
return /* @__PURE__ */ jsx(FloPayContext.Provider, { value, children });
|
|
100
193
|
}
|
|
101
194
|
|
|
102
195
|
// src/flopay-checkout.tsx
|
|
103
|
-
import React8, { useCallback as
|
|
196
|
+
import React8, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef6, useState as useState4 } from "react";
|
|
104
197
|
import { PaymentAPI as PaymentAPI4 } from "@flopay/js";
|
|
105
|
-
import { SDK_VERSION, FloPayError as FloPayError5, resolveBillingApiUrl as resolveBillingApiUrl3, buildCheckoutDisplayData, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme2, resolveTheme as resolveTheme2 } from "@flopay/shared";
|
|
198
|
+
import { SDK_VERSION as SDK_VERSION2, FloPayError as FloPayError5, resolveBillingApiUrl as resolveBillingApiUrl3, buildCheckoutDisplayData, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme2, resolveTheme as resolveTheme2 } from "@flopay/shared";
|
|
106
199
|
|
|
107
200
|
// src/card-button-content.tsx
|
|
108
201
|
import "react";
|
|
@@ -172,7 +265,7 @@ function TitleContentSlot({
|
|
|
172
265
|
}
|
|
173
266
|
|
|
174
267
|
// src/elements.tsx
|
|
175
|
-
import { useEffect as useEffect2, useRef, useContext } from "react";
|
|
268
|
+
import { useEffect as useEffect2, useRef as useRef2, useContext } from "react";
|
|
176
269
|
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
177
270
|
function createElementComponent(elementType, displayName) {
|
|
178
271
|
function ElementComponent({
|
|
@@ -186,9 +279,9 @@ function createElementComponent(elementType, displayName) {
|
|
|
186
279
|
onBlur,
|
|
187
280
|
onEscape
|
|
188
281
|
}) {
|
|
189
|
-
const containerRef =
|
|
190
|
-
const elementRef =
|
|
191
|
-
const { elements } = useContext(FloPayContext);
|
|
282
|
+
const containerRef = useRef2(null);
|
|
283
|
+
const elementRef = useRef2(null);
|
|
284
|
+
const { elements, reportInteractive } = useContext(FloPayContext);
|
|
192
285
|
useEffect2(() => {
|
|
193
286
|
if (!elements || !containerRef.current) return;
|
|
194
287
|
let mounted = true;
|
|
@@ -203,7 +296,10 @@ function createElementComponent(elementType, displayName) {
|
|
|
203
296
|
element.mount(containerRef.current);
|
|
204
297
|
elementRef.current = element;
|
|
205
298
|
if (onChange) element.on("change", onChange);
|
|
206
|
-
|
|
299
|
+
element.on("ready", () => {
|
|
300
|
+
reportInteractive?.();
|
|
301
|
+
onReady?.();
|
|
302
|
+
});
|
|
207
303
|
if (onFocus) element.on("focus", onFocus);
|
|
208
304
|
if (onBlur) element.on("blur", onBlur);
|
|
209
305
|
if (onEscape) element.on("escape", onEscape);
|
|
@@ -218,7 +314,7 @@ function createElementComponent(elementType, displayName) {
|
|
|
218
314
|
elementRef.current = null;
|
|
219
315
|
}
|
|
220
316
|
};
|
|
221
|
-
}, [elements]);
|
|
317
|
+
}, [elements, reportInteractive]);
|
|
222
318
|
return /* @__PURE__ */ jsx3("div", { ref: containerRef, className, id, style });
|
|
223
319
|
}
|
|
224
320
|
ElementComponent.displayName = displayName;
|
|
@@ -262,7 +358,7 @@ import {
|
|
|
262
358
|
import { PaymentAPI as PaymentAPI2 } from "@flopay/js";
|
|
263
359
|
|
|
264
360
|
// src/vault-card-fields.tsx
|
|
265
|
-
import { useEffect as useEffect3, useRef as
|
|
361
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
266
362
|
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
267
363
|
function VaultCardFields({
|
|
268
364
|
capture,
|
|
@@ -275,14 +371,14 @@ function VaultCardFields({
|
|
|
275
371
|
onError,
|
|
276
372
|
onValidation
|
|
277
373
|
}) {
|
|
278
|
-
const containerRef =
|
|
279
|
-
const onReadyRef =
|
|
280
|
-
const onErrorRef =
|
|
281
|
-
const onValidationRef =
|
|
374
|
+
const containerRef = useRef3(null);
|
|
375
|
+
const onReadyRef = useRef3(onReady);
|
|
376
|
+
const onErrorRef = useRef3(onError);
|
|
377
|
+
const onValidationRef = useRef3(onValidation);
|
|
282
378
|
onReadyRef.current = onReady;
|
|
283
379
|
onErrorRef.current = onError;
|
|
284
380
|
onValidationRef.current = onValidation;
|
|
285
|
-
const themeRef =
|
|
381
|
+
const themeRef = useRef3(theme);
|
|
286
382
|
themeRef.current = theme;
|
|
287
383
|
useEffect3(() => {
|
|
288
384
|
const el = containerRef.current;
|
|
@@ -333,7 +429,7 @@ function VaultCardFields({
|
|
|
333
429
|
}
|
|
334
430
|
|
|
335
431
|
// src/split-card-form.tsx
|
|
336
|
-
import React7, { forwardRef, useCallback, useContext as useContext3, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as
|
|
432
|
+
import React7, { forwardRef, useCallback as useCallback3, useContext as useContext3, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef5, useState as useState3 } from "react";
|
|
337
433
|
|
|
338
434
|
// src/hooks.ts
|
|
339
435
|
import { useContext as useContext2 } from "react";
|
|
@@ -770,13 +866,81 @@ function isInAppBrowser(userAgent) {
|
|
|
770
866
|
}
|
|
771
867
|
|
|
772
868
|
// src/direct-paypal-button.tsx
|
|
773
|
-
import { useEffect as useEffect4, useMemo as useMemo2, useRef as
|
|
869
|
+
import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState2 } from "react";
|
|
774
870
|
import { loadScript } from "@paypal/paypal-js";
|
|
775
871
|
import { PaymentAPI } from "@flopay/js";
|
|
776
|
-
import { FloPayError as FloPayError2, normalizeGatewayEnvironment } from "@flopay/shared";
|
|
872
|
+
import { FloPayError as FloPayError2, SDK_VERSION, normalizeGatewayEnvironment } from "@flopay/shared";
|
|
873
|
+
|
|
874
|
+
// src/merchant-callback.ts
|
|
875
|
+
function invokeMerchantCallback(callback) {
|
|
876
|
+
if (!callback) return;
|
|
877
|
+
const reportFailure = (error) => {
|
|
878
|
+
console.error("[FloPay] Merchant callback failed; checkout continued.", error);
|
|
879
|
+
};
|
|
880
|
+
try {
|
|
881
|
+
void Promise.resolve(callback()).catch(reportFailure);
|
|
882
|
+
} catch (error) {
|
|
883
|
+
reportFailure(error);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/external-method-recovery.ts
|
|
888
|
+
var EXTERNAL_METHOD_CALLBACK_GRACE_MS = 600;
|
|
889
|
+
function getProviderErrorMessage(err) {
|
|
890
|
+
if (err instanceof Error) return err.message;
|
|
891
|
+
if (typeof err === "object" && err !== null) {
|
|
892
|
+
const message = err.message;
|
|
893
|
+
return typeof message === "string" ? message : void 0;
|
|
894
|
+
}
|
|
895
|
+
return typeof err === "string" ? err : void 0;
|
|
896
|
+
}
|
|
897
|
+
function sanitizeExternalFailureCode(value) {
|
|
898
|
+
if (typeof value !== "string") return void 0;
|
|
899
|
+
const trimmed = value.trim();
|
|
900
|
+
return /^[a-z0-9_.-]{1,64}$/i.test(trimmed) ? trimmed : void 0;
|
|
901
|
+
}
|
|
902
|
+
function getProviderErrorCode(err) {
|
|
903
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
904
|
+
const record = err;
|
|
905
|
+
return sanitizeExternalFailureCode(record.code) ?? sanitizeExternalFailureCode(record.type) ?? sanitizeExternalFailureCode(record.name);
|
|
906
|
+
}
|
|
907
|
+
function getProviderDeclineCode(err) {
|
|
908
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
909
|
+
return sanitizeExternalFailureCode(err.decline_code);
|
|
910
|
+
}
|
|
911
|
+
function isProviderDecline(err) {
|
|
912
|
+
if (typeof err !== "object" || err === null) return false;
|
|
913
|
+
const type = sanitizeExternalFailureCode(err.type)?.toLowerCase();
|
|
914
|
+
return type === "card_error" || getProviderDeclineCode(err) !== void 0;
|
|
915
|
+
}
|
|
916
|
+
function isPopupBlockedError(err) {
|
|
917
|
+
const message = getProviderErrorMessage(err)?.toLowerCase() ?? "";
|
|
918
|
+
const code = getProviderErrorCode(err)?.toLowerCase() ?? "";
|
|
919
|
+
const signal = `${code} ${message}`;
|
|
920
|
+
return signal.includes("popup") && signal.includes("block");
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/direct-paypal-button.tsx
|
|
777
924
|
import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
778
925
|
var DEFAULT_BUTTON_HEIGHT = 45;
|
|
779
|
-
|
|
926
|
+
var DIRECT_PAYPAL_RECOVERY_MESSAGE = "We couldn't open PayPal. Try again or choose another payment method.";
|
|
927
|
+
var DIRECT_PAYPAL_RECOVERY_ACTION_STYLE = {
|
|
928
|
+
width: "100%",
|
|
929
|
+
minHeight: DEFAULT_BUTTON_HEIGHT,
|
|
930
|
+
margin: "0 0 0.5rem",
|
|
931
|
+
padding: "0.75rem 0.875rem",
|
|
932
|
+
border: "1px solid #2563eb",
|
|
933
|
+
borderRadius: 8,
|
|
934
|
+
background: "#eff6ff",
|
|
935
|
+
color: "#1d4ed8",
|
|
936
|
+
fontSize: "0.95rem",
|
|
937
|
+
fontWeight: 700,
|
|
938
|
+
cursor: "pointer"
|
|
939
|
+
};
|
|
940
|
+
function buildDirectPayPalRecoveryMessage(popupBlocked) {
|
|
941
|
+
return popupBlocked ? `${DIRECT_PAYPAL_RECOVERY_MESSAGE} Allow pop-ups for this site, then try again.` : DIRECT_PAYPAL_RECOVERY_MESSAGE;
|
|
942
|
+
}
|
|
943
|
+
function DirectPayPalButtonImplementation({
|
|
780
944
|
sessionId,
|
|
781
945
|
nonce,
|
|
782
946
|
billingApiUrl,
|
|
@@ -789,16 +953,70 @@ function DirectPayPalButton({
|
|
|
789
953
|
onComplete,
|
|
790
954
|
onErrorChange,
|
|
791
955
|
onDecline,
|
|
956
|
+
onTechnicalFailure,
|
|
792
957
|
isProcessing = false,
|
|
793
958
|
onLoadStateChange,
|
|
794
959
|
onButtonClick,
|
|
795
960
|
runBeforeButtonClick,
|
|
796
961
|
session,
|
|
797
962
|
existingOrderId,
|
|
963
|
+
telemetry,
|
|
964
|
+
telemetryContext,
|
|
798
965
|
debug = false
|
|
799
966
|
}) {
|
|
800
|
-
const
|
|
967
|
+
const flopay = useFloPay();
|
|
968
|
+
const standaloneTelemetry = useMemo2(() => {
|
|
969
|
+
if (flopay) return null;
|
|
970
|
+
const reporter = createTelemetryBridge({
|
|
971
|
+
billingApiUrl,
|
|
972
|
+
sdkPackage: "@flopay/react",
|
|
973
|
+
sdkVersion: SDK_VERSION,
|
|
974
|
+
enabled: telemetry !== false
|
|
975
|
+
});
|
|
976
|
+
reporter.setCheckoutContext(telemetryContext ?? {});
|
|
977
|
+
reporter.beginCheckout(telemetryContext ?? {});
|
|
978
|
+
return reporter;
|
|
979
|
+
}, [
|
|
980
|
+
billingApiUrl,
|
|
981
|
+
flopay,
|
|
982
|
+
telemetry,
|
|
983
|
+
telemetryContext?.checkoutMode,
|
|
984
|
+
telemetryContext?.layout
|
|
985
|
+
]);
|
|
986
|
+
const floPayTelemetry = useMemo2(() => getFloPayTelemetryBridge(flopay), [flopay]);
|
|
987
|
+
useEffect4(() => () => {
|
|
988
|
+
if (!standaloneTelemetry) return;
|
|
989
|
+
void standaloneTelemetry.flush().catch(() => {
|
|
990
|
+
}).finally(() => standaloneTelemetry.destroy());
|
|
991
|
+
}, [standaloneTelemetry]);
|
|
992
|
+
const telemetrySource = useMemo2(() => ({
|
|
993
|
+
error: (input) => {
|
|
994
|
+
if (floPayTelemetry) floPayTelemetry.error(input);
|
|
995
|
+
else standaloneTelemetry?.error(input);
|
|
996
|
+
},
|
|
997
|
+
log: (input) => {
|
|
998
|
+
if (floPayTelemetry) floPayTelemetry.log(input);
|
|
999
|
+
else standaloneTelemetry?.log(input);
|
|
1000
|
+
},
|
|
1001
|
+
performance: (input) => {
|
|
1002
|
+
if (floPayTelemetry) floPayTelemetry.performance(input);
|
|
1003
|
+
else standaloneTelemetry?.performance(input);
|
|
1004
|
+
},
|
|
1005
|
+
terminal: (input) => {
|
|
1006
|
+
if (floPayTelemetry) floPayTelemetry.terminal(input);
|
|
1007
|
+
else standaloneTelemetry?.terminal(input);
|
|
1008
|
+
},
|
|
1009
|
+
startTiming: () => floPayTelemetry?.now() ?? standaloneTelemetry?.now() ?? 0,
|
|
1010
|
+
elapsed: (startedAt) => floPayTelemetry?.elapsed(startedAt) ?? Math.max(0, (standaloneTelemetry?.now() ?? startedAt) - startedAt)
|
|
1011
|
+
}), [floPayTelemetry, standaloneTelemetry]);
|
|
1012
|
+
const containerRef = useRef4(null);
|
|
1013
|
+
const providerStartedAt = useRef4(0);
|
|
1014
|
+
const focusTargetRef = useRef4(null);
|
|
1015
|
+
const pendingProviderFocusRef = useRef4(false);
|
|
801
1016
|
const [ready, setReady] = useState2(false);
|
|
1017
|
+
const [renderGeneration, setRenderGeneration] = useState2(0);
|
|
1018
|
+
const activeRenderGenerationRef = useRef4(0);
|
|
1019
|
+
const [showRetryFocusTarget, setShowRetryFocusTarget] = useState2(false);
|
|
802
1020
|
const [failed, setFailed] = useState2(false);
|
|
803
1021
|
const [submitting, setSubmitting] = useState2(false);
|
|
804
1022
|
const baseUrl = useMemo2(() => billingApiUrl.replace(/\/+$/, ""), [billingApiUrl]);
|
|
@@ -807,17 +1025,23 @@ function DirectPayPalButton({
|
|
|
807
1025
|
if (!debug) return;
|
|
808
1026
|
setDebugLines((prev) => [...prev, `${(/* @__PURE__ */ new Date()).toISOString().slice(11, 23)} ${line}`]);
|
|
809
1027
|
};
|
|
810
|
-
const onTokenizedBodyRef =
|
|
811
|
-
const onCompleteRef =
|
|
812
|
-
const onErrorChangeRef =
|
|
813
|
-
const onDeclineRef =
|
|
814
|
-
const
|
|
815
|
-
const
|
|
816
|
-
const
|
|
817
|
-
const
|
|
818
|
-
const
|
|
819
|
-
const
|
|
820
|
-
const
|
|
1028
|
+
const onTokenizedBodyRef = useRef4(onTokenizedBody);
|
|
1029
|
+
const onCompleteRef = useRef4(onComplete);
|
|
1030
|
+
const onErrorChangeRef = useRef4(onErrorChange);
|
|
1031
|
+
const onDeclineRef = useRef4(onDecline);
|
|
1032
|
+
const onTechnicalFailureRef = useRef4(onTechnicalFailure);
|
|
1033
|
+
const onButtonClickRef = useRef4(onButtonClick);
|
|
1034
|
+
const onLoadStateChangeRef = useRef4(onLoadStateChange);
|
|
1035
|
+
const runBeforeButtonClickRef = useRef4(runBeforeButtonClick);
|
|
1036
|
+
const sessionRef = useRef4(session);
|
|
1037
|
+
const emailRef = useRef4(email);
|
|
1038
|
+
const nonceRef = useRef4(nonce);
|
|
1039
|
+
const beforeClickRef = useRef4(null);
|
|
1040
|
+
const attemptGenerationRef = useRef4(0);
|
|
1041
|
+
const attemptRef = useRef4(null);
|
|
1042
|
+
const invalidatedAttemptGenerationRef = useRef4(null);
|
|
1043
|
+
const attemptContextBySurfaceRef = useRef4(/* @__PURE__ */ new Map());
|
|
1044
|
+
const approvalContextByTokenRef = useRef4(/* @__PURE__ */ new Map());
|
|
821
1045
|
useEffect4(() => {
|
|
822
1046
|
onTokenizedBodyRef.current = onTokenizedBody;
|
|
823
1047
|
}, [onTokenizedBody]);
|
|
@@ -830,6 +1054,9 @@ function DirectPayPalButton({
|
|
|
830
1054
|
useEffect4(() => {
|
|
831
1055
|
onDeclineRef.current = onDecline;
|
|
832
1056
|
}, [onDecline]);
|
|
1057
|
+
useEffect4(() => {
|
|
1058
|
+
onTechnicalFailureRef.current = onTechnicalFailure;
|
|
1059
|
+
}, [onTechnicalFailure]);
|
|
833
1060
|
useEffect4(() => {
|
|
834
1061
|
onButtonClickRef.current = onButtonClick;
|
|
835
1062
|
}, [onButtonClick]);
|
|
@@ -849,7 +1076,130 @@ function DirectPayPalButton({
|
|
|
849
1076
|
nonceRef.current = nonce;
|
|
850
1077
|
}, [nonce]);
|
|
851
1078
|
useEffect4(() => {
|
|
852
|
-
|
|
1079
|
+
activeRenderGenerationRef.current = renderGeneration;
|
|
1080
|
+
}, [renderGeneration]);
|
|
1081
|
+
useEffect4(() => {
|
|
1082
|
+
if (showRetryFocusTarget) focusTargetRef.current?.focus();
|
|
1083
|
+
}, [showRetryFocusTarget, renderGeneration]);
|
|
1084
|
+
const focusRetryTarget = useCallback2(() => {
|
|
1085
|
+
window.setTimeout(() => {
|
|
1086
|
+
focusTargetRef.current?.focus();
|
|
1087
|
+
}, 0);
|
|
1088
|
+
}, []);
|
|
1089
|
+
const remountPayPalButtons = useCallback2(() => {
|
|
1090
|
+
setReady(false);
|
|
1091
|
+
setRenderGeneration((current) => current + 1);
|
|
1092
|
+
}, []);
|
|
1093
|
+
const focusPayPalSurface = useCallback2(() => {
|
|
1094
|
+
setShowRetryFocusTarget(false);
|
|
1095
|
+
const target = containerRef.current?.querySelector(
|
|
1096
|
+
'iframe, button, [tabindex]:not([tabindex="-1"])'
|
|
1097
|
+
);
|
|
1098
|
+
(target ?? containerRef.current)?.focus?.();
|
|
1099
|
+
}, []);
|
|
1100
|
+
useEffect4(() => {
|
|
1101
|
+
if (!ready || !pendingProviderFocusRef.current) return;
|
|
1102
|
+
pendingProviderFocusRef.current = false;
|
|
1103
|
+
focusPayPalSurface();
|
|
1104
|
+
}, [focusPayPalSurface, ready, renderGeneration]);
|
|
1105
|
+
const notifyTechnicalFailure = useCallback2((err, options) => {
|
|
1106
|
+
const handler = onTechnicalFailureRef.current;
|
|
1107
|
+
if (handler) {
|
|
1108
|
+
handler("paypal", err, options);
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
onErrorChangeRef.current?.(
|
|
1112
|
+
buildDirectPayPalRecoveryMessage(options?.popupBlocked ?? isPopupBlockedError(err))
|
|
1113
|
+
);
|
|
1114
|
+
}, []);
|
|
1115
|
+
const clearAttemptTimer = () => {
|
|
1116
|
+
if (attemptRef.current?.timer) {
|
|
1117
|
+
clearTimeout(attemptRef.current.timer);
|
|
1118
|
+
attemptRef.current.timer = null;
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
const startAttempt = () => {
|
|
1122
|
+
clearAttemptTimer();
|
|
1123
|
+
const generation = attemptGenerationRef.current + 1;
|
|
1124
|
+
attemptGenerationRef.current = generation;
|
|
1125
|
+
invalidatedAttemptGenerationRef.current = null;
|
|
1126
|
+
attemptRef.current = { generation, timer: null, yieldedControl: false };
|
|
1127
|
+
setShowRetryFocusTarget(false);
|
|
1128
|
+
return generation;
|
|
1129
|
+
};
|
|
1130
|
+
const finishAttempt = (generation) => {
|
|
1131
|
+
if (generation !== void 0 && attemptRef.current?.generation !== generation) return false;
|
|
1132
|
+
clearAttemptTimer();
|
|
1133
|
+
attemptRef.current = null;
|
|
1134
|
+
invalidatedAttemptGenerationRef.current = null;
|
|
1135
|
+
return true;
|
|
1136
|
+
};
|
|
1137
|
+
const invalidateAttempt = (options) => {
|
|
1138
|
+
const generation = options?.generation ?? attemptRef.current?.generation ?? attemptGenerationRef.current;
|
|
1139
|
+
if (!options?.generation || attemptRef.current?.generation === generation) {
|
|
1140
|
+
clearAttemptTimer();
|
|
1141
|
+
attemptRef.current = null;
|
|
1142
|
+
}
|
|
1143
|
+
invalidatedAttemptGenerationRef.current = generation;
|
|
1144
|
+
if (options?.showRetry) setShowRetryFocusTarget(true);
|
|
1145
|
+
if (options?.remount) remountPayPalButtons();
|
|
1146
|
+
if (options?.focus !== false) focusRetryTarget();
|
|
1147
|
+
};
|
|
1148
|
+
const isAttemptActive = (generation) => attemptRef.current?.generation === generation && invalidatedAttemptGenerationRef.current !== generation;
|
|
1149
|
+
const armMissingCallbackRecovery = (attempt) => {
|
|
1150
|
+
if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
|
|
1151
|
+
clearAttemptTimer();
|
|
1152
|
+
attempt.timer = setTimeout(() => {
|
|
1153
|
+
if (attemptRef.current?.generation !== attempt.generation) return;
|
|
1154
|
+
attemptRef.current = null;
|
|
1155
|
+
invalidatedAttemptGenerationRef.current = attempt.generation;
|
|
1156
|
+
notifyTechnicalFailure(
|
|
1157
|
+
new Error("External payment method returned without a terminal callback."),
|
|
1158
|
+
{ code: "external_method_missing_terminal_callback" }
|
|
1159
|
+
);
|
|
1160
|
+
setShowRetryFocusTarget(true);
|
|
1161
|
+
remountPayPalButtons();
|
|
1162
|
+
focusRetryTarget();
|
|
1163
|
+
}, EXTERNAL_METHOD_CALLBACK_GRACE_MS);
|
|
1164
|
+
};
|
|
1165
|
+
const scheduleMissingCallbackRecovery = () => {
|
|
1166
|
+
const attempt = attemptRef.current;
|
|
1167
|
+
if (!attempt?.yieldedControl) return;
|
|
1168
|
+
armMissingCallbackRecovery(attempt);
|
|
1169
|
+
};
|
|
1170
|
+
const markAttemptYieldedControl = () => {
|
|
1171
|
+
const attempt = attemptRef.current;
|
|
1172
|
+
if (!attempt) return;
|
|
1173
|
+
attempt.yieldedControl = true;
|
|
1174
|
+
clearAttemptTimer();
|
|
1175
|
+
};
|
|
1176
|
+
const registerApprovalContext = (token) => {
|
|
1177
|
+
const context = beforeClickRef.current;
|
|
1178
|
+
if (context) {
|
|
1179
|
+
approvalContextByTokenRef.current.set(token, context);
|
|
1180
|
+
}
|
|
1181
|
+
return token;
|
|
1182
|
+
};
|
|
1183
|
+
useEffect4(() => {
|
|
1184
|
+
const handleVisibilityChange = () => {
|
|
1185
|
+
if (document.visibilityState === "visible") {
|
|
1186
|
+
scheduleMissingCallbackRecovery();
|
|
1187
|
+
} else {
|
|
1188
|
+
markAttemptYieldedControl();
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
1192
|
+
window.addEventListener("focus", scheduleMissingCallbackRecovery);
|
|
1193
|
+
window.addEventListener("blur", markAttemptYieldedControl);
|
|
1194
|
+
return () => {
|
|
1195
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
1196
|
+
window.removeEventListener("focus", scheduleMissingCallbackRecovery);
|
|
1197
|
+
window.removeEventListener("blur", markAttemptYieldedControl);
|
|
1198
|
+
clearAttemptTimer();
|
|
1199
|
+
};
|
|
1200
|
+
}, []);
|
|
1201
|
+
useEffect4(() => {
|
|
1202
|
+
invokeMerchantCallback(() => onLoadStateChangeRef.current?.(ready && !failed));
|
|
853
1203
|
}, [ready, failed]);
|
|
854
1204
|
const normalizedEnv = normalizeGatewayEnvironment(environment);
|
|
855
1205
|
useEffect4(() => {
|
|
@@ -860,6 +1210,13 @@ function DirectPayPalButton({
|
|
|
860
1210
|
if (!clientId) {
|
|
861
1211
|
appendDebug("FAIL: clientId empty \u2014 gateway misconfigured");
|
|
862
1212
|
setFailed(true);
|
|
1213
|
+
telemetrySource.error({
|
|
1214
|
+
errorCode: "CONFIGURATION_INVALID",
|
|
1215
|
+
stage: "provider_load",
|
|
1216
|
+
provider: "paypal",
|
|
1217
|
+
paymentMethodCategory: "paypal",
|
|
1218
|
+
requestCategory: "provider_sdk"
|
|
1219
|
+
});
|
|
863
1220
|
console.error("[FloPay] DirectPayPal: clientId empty \u2014 gateway misconfigured");
|
|
864
1221
|
return;
|
|
865
1222
|
}
|
|
@@ -867,8 +1224,33 @@ function DirectPayPalButton({
|
|
|
867
1224
|
appendDebug("FAIL: containerRef not attached");
|
|
868
1225
|
return;
|
|
869
1226
|
}
|
|
1227
|
+
providerStartedAt.current = telemetrySource.startTiming();
|
|
1228
|
+
telemetrySource.log({
|
|
1229
|
+
name: "provider.load.started",
|
|
1230
|
+
stage: "provider_load",
|
|
1231
|
+
provider: "paypal",
|
|
1232
|
+
paymentMethodCategory: "paypal"
|
|
1233
|
+
});
|
|
870
1234
|
let cancelled = false;
|
|
871
1235
|
let activeButtons = null;
|
|
1236
|
+
let overlayStartedAt = null;
|
|
1237
|
+
const finishOverlay = () => {
|
|
1238
|
+
if (overlayStartedAt === null) return;
|
|
1239
|
+
telemetrySource.log({
|
|
1240
|
+
name: "provider.overlay.returned",
|
|
1241
|
+
stage: "overlay_return",
|
|
1242
|
+
provider: "paypal",
|
|
1243
|
+
paymentMethodCategory: "paypal"
|
|
1244
|
+
});
|
|
1245
|
+
telemetrySource.performance({
|
|
1246
|
+
stage: "overlay_return",
|
|
1247
|
+
durationMs: telemetrySource.elapsed(overlayStartedAt),
|
|
1248
|
+
durationMode: "buyer",
|
|
1249
|
+
provider: "paypal",
|
|
1250
|
+
paymentMethodCategory: "paypal"
|
|
1251
|
+
});
|
|
1252
|
+
overlayStartedAt = null;
|
|
1253
|
+
};
|
|
872
1254
|
let rendered = false;
|
|
873
1255
|
const container = containerRef.current;
|
|
874
1256
|
let containerObserver = null;
|
|
@@ -972,7 +1354,7 @@ function DirectPayPalButton({
|
|
|
972
1354
|
if (cancelled) return;
|
|
973
1355
|
if (isZoidLifecycleMessage(message)) return;
|
|
974
1356
|
const friendly = applyFriendlyMessageOverride(message) ?? message;
|
|
975
|
-
onErrorChangeRef.current?.(friendly);
|
|
1357
|
+
invokeMerchantCallback(() => onErrorChangeRef.current?.(friendly));
|
|
976
1358
|
};
|
|
977
1359
|
const markRenderFailed = (message) => {
|
|
978
1360
|
if (cancelled) return;
|
|
@@ -981,29 +1363,60 @@ function DirectPayPalButton({
|
|
|
981
1363
|
return;
|
|
982
1364
|
}
|
|
983
1365
|
setFailed(true);
|
|
1366
|
+
if (!message.includes("paypal_ineligible")) {
|
|
1367
|
+
telemetrySource.error({
|
|
1368
|
+
errorCode: "PROVIDER_LOAD_FAILED",
|
|
1369
|
+
stage: "provider_load",
|
|
1370
|
+
provider: "paypal",
|
|
1371
|
+
paymentMethodCategory: "paypal",
|
|
1372
|
+
requestCategory: "provider_sdk"
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
984
1375
|
console.error("[FloPay] DirectPayPal load/render failure:", message);
|
|
985
1376
|
};
|
|
986
1377
|
setFailed(false);
|
|
987
|
-
const dispatchTokenizedBody = async (body) => {
|
|
988
|
-
const prepared = beforeClickRef.current;
|
|
1378
|
+
const dispatchTokenizedBody = async (body, prepared = beforeClickRef.current) => {
|
|
989
1379
|
const effectiveSessionId = prepared?.sessionId ?? sessionId;
|
|
990
1380
|
if (onTokenizedBodyRef.current) {
|
|
991
|
-
onTokenizedBodyRef.current(body, {
|
|
1381
|
+
await onTokenizedBodyRef.current(body, {
|
|
992
1382
|
sessionId: effectiveSessionId,
|
|
993
|
-
accountPatch: prepared?.accountPatch
|
|
1383
|
+
accountPatch: prepared?.accountPatch,
|
|
1384
|
+
nonce: prepared?.nonce
|
|
994
1385
|
});
|
|
995
1386
|
return;
|
|
996
1387
|
}
|
|
1388
|
+
const processingStartedAt = telemetrySource.startTiming();
|
|
1389
|
+
telemetrySource.log({
|
|
1390
|
+
name: "payment.processing.started",
|
|
1391
|
+
stage: "processing",
|
|
1392
|
+
provider: "paypal",
|
|
1393
|
+
paymentMethodCategory: "paypal"
|
|
1394
|
+
});
|
|
1395
|
+
const finishProcessing = () => {
|
|
1396
|
+
telemetrySource.log({
|
|
1397
|
+
name: "payment.processing.completed",
|
|
1398
|
+
stage: "processing",
|
|
1399
|
+
provider: "paypal",
|
|
1400
|
+
paymentMethodCategory: "paypal"
|
|
1401
|
+
});
|
|
1402
|
+
telemetrySource.performance({
|
|
1403
|
+
stage: "processing",
|
|
1404
|
+
durationMs: telemetrySource.elapsed(processingStartedAt),
|
|
1405
|
+
durationMode: "machine",
|
|
1406
|
+
provider: "paypal",
|
|
1407
|
+
paymentMethodCategory: "paypal"
|
|
1408
|
+
});
|
|
1409
|
+
};
|
|
997
1410
|
try {
|
|
998
1411
|
const currentSession = sessionRef.current;
|
|
999
1412
|
const currentEmail = prepared?.accountPatch?.email ?? emailRef.current;
|
|
1000
1413
|
const effectiveUserId = prepared?.accountPatch?.userId ?? currentSession?.customer?.id ?? currentSession?.accountData?.userId ?? "";
|
|
1001
|
-
const api = new PaymentAPI(baseUrl);
|
|
1414
|
+
const api = new PaymentAPI(baseUrl, { telemetry: false });
|
|
1002
1415
|
const response = await api.processPayment(
|
|
1003
1416
|
effectiveUserId,
|
|
1004
1417
|
{
|
|
1005
1418
|
sessionId: effectiveSessionId,
|
|
1006
|
-
nonce: nonceRef.current,
|
|
1419
|
+
nonce: prepared?.nonce ?? nonceRef.current,
|
|
1007
1420
|
tokenizedData: body,
|
|
1008
1421
|
accountData: {
|
|
1009
1422
|
userId: effectiveUserId,
|
|
@@ -1016,31 +1429,72 @@ function DirectPayPalButton({
|
|
|
1016
1429
|
}
|
|
1017
1430
|
);
|
|
1018
1431
|
if (response.ok) {
|
|
1019
|
-
|
|
1432
|
+
finishProcessing();
|
|
1433
|
+
telemetrySource.terminal({
|
|
1434
|
+
outcome: "payment_succeeded",
|
|
1435
|
+
provider: "paypal",
|
|
1436
|
+
paymentMethodCategory: "paypal"
|
|
1437
|
+
});
|
|
1438
|
+
} else {
|
|
1439
|
+
const json = await response.json().catch(() => null);
|
|
1440
|
+
const rawMessage = json?.["message"] ?? "PayPal payment failed.";
|
|
1441
|
+
const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
|
|
1442
|
+
finishProcessing();
|
|
1443
|
+
if (typeof json?.["declineCode"] === "string") {
|
|
1444
|
+
telemetrySource.terminal({
|
|
1445
|
+
outcome: "payment_declined",
|
|
1446
|
+
provider: "paypal",
|
|
1447
|
+
paymentMethodCategory: "paypal"
|
|
1448
|
+
});
|
|
1449
|
+
} else {
|
|
1450
|
+
telemetrySource.error({
|
|
1451
|
+
errorCode: "PAYMENT_PROCESSING_FAILED",
|
|
1452
|
+
stage: "processing",
|
|
1453
|
+
provider: "paypal",
|
|
1454
|
+
paymentMethodCategory: "paypal",
|
|
1455
|
+
requestCategory: "process_payment",
|
|
1456
|
+
statusClass: response.status >= 500 ? "5xx" : response.status >= 400 ? "4xx" : response.status >= 300 ? "3xx" : "unknown"
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
forwardError(message);
|
|
1460
|
+
invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
|
|
1461
|
+
code: json?.["code"],
|
|
1462
|
+
declineCode: json?.["declineCode"]
|
|
1463
|
+
})));
|
|
1020
1464
|
return;
|
|
1021
1465
|
}
|
|
1022
|
-
const json = await response.json().catch(() => null);
|
|
1023
|
-
const rawMessage = json?.["message"] ?? "PayPal payment failed.";
|
|
1024
|
-
const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
|
|
1025
|
-
forwardError(message);
|
|
1026
|
-
onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
|
|
1027
|
-
code: json?.["code"],
|
|
1028
|
-
declineCode: json?.["declineCode"]
|
|
1029
|
-
}));
|
|
1030
1466
|
} catch (err) {
|
|
1467
|
+
finishProcessing();
|
|
1468
|
+
telemetrySource.error({
|
|
1469
|
+
errorCode: "NETWORK_REQUEST_FAILED",
|
|
1470
|
+
stage: "processing",
|
|
1471
|
+
provider: "paypal",
|
|
1472
|
+
paymentMethodCategory: "paypal",
|
|
1473
|
+
requestCategory: "process_payment",
|
|
1474
|
+
statusClass: "network_error"
|
|
1475
|
+
});
|
|
1031
1476
|
const rawMessage = err instanceof Error ? err.message : "PayPal payment failed.";
|
|
1032
1477
|
const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
|
|
1033
1478
|
forwardError(message);
|
|
1034
|
-
onDeclineRef.current?.(buildDeclineEvent("paypal", message));
|
|
1479
|
+
invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message)));
|
|
1480
|
+
return;
|
|
1035
1481
|
}
|
|
1482
|
+
invokeMerchantCallback(() => onCompleteRef.current?.({ status: "succeeded", checkoutMethod: "paypal" }));
|
|
1036
1483
|
};
|
|
1037
1484
|
const createPaypalIntent = async (fallbackMessage) => {
|
|
1038
1485
|
const prepared = beforeClickRef.current;
|
|
1039
1486
|
const effectiveSessionId = prepared?.sessionId ?? sessionId;
|
|
1040
1487
|
const effectiveEmail = prepared?.accountPatch?.email ?? emailRef.current;
|
|
1041
1488
|
const intentHeaders = { "Content-Type": "application/json" };
|
|
1042
|
-
const currentNonce = nonceRef.current;
|
|
1489
|
+
const currentNonce = prepared?.nonce ?? nonceRef.current;
|
|
1043
1490
|
if (currentNonce) intentHeaders["x-checkout-session-token"] = currentNonce;
|
|
1491
|
+
telemetrySource.log({
|
|
1492
|
+
name: "payment.intent.started",
|
|
1493
|
+
stage: "processing",
|
|
1494
|
+
provider: "paypal",
|
|
1495
|
+
paymentMethodCategory: "paypal",
|
|
1496
|
+
requestCategory: "intent_create"
|
|
1497
|
+
});
|
|
1044
1498
|
const response = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
|
|
1045
1499
|
method: "POST",
|
|
1046
1500
|
headers: intentHeaders,
|
|
@@ -1059,8 +1513,17 @@ function DirectPayPalButton({
|
|
|
1059
1513
|
if (!id) {
|
|
1060
1514
|
throw new Error(fallbackMessage);
|
|
1061
1515
|
}
|
|
1516
|
+
telemetrySource.log({
|
|
1517
|
+
name: "payment.intent.completed",
|
|
1518
|
+
stage: "processing",
|
|
1519
|
+
provider: "paypal",
|
|
1520
|
+
paymentMethodCategory: "paypal",
|
|
1521
|
+
requestCategory: "intent_create",
|
|
1522
|
+
statusClass: "2xx"
|
|
1523
|
+
});
|
|
1062
1524
|
return id;
|
|
1063
1525
|
};
|
|
1526
|
+
const effectRenderGeneration = renderGeneration;
|
|
1064
1527
|
appendDebug("loadScript:scheduled (deferred 1 tick)");
|
|
1065
1528
|
let loadPromise = null;
|
|
1066
1529
|
const startTimer = setTimeout(() => {
|
|
@@ -1092,11 +1555,24 @@ function DirectPayPalButton({
|
|
|
1092
1555
|
if (!cancelled && !paypal?.Buttons) appendDebug("FAIL: namespace missing Buttons factory");
|
|
1093
1556
|
return;
|
|
1094
1557
|
}
|
|
1558
|
+
telemetrySource.log({
|
|
1559
|
+
name: "provider.availability.checked",
|
|
1560
|
+
stage: "provider_ready",
|
|
1561
|
+
provider: "paypal",
|
|
1562
|
+
paymentMethodCategory: "paypal"
|
|
1563
|
+
});
|
|
1095
1564
|
const handleApprove = async (data) => {
|
|
1565
|
+
if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
|
|
1566
|
+
const token = data.subscriptionID ?? data.orderID ?? "";
|
|
1567
|
+
const context = token ? approvalContextByTokenRef.current.get(token) : attemptContextBySurfaceRef.current.get(effectRenderGeneration);
|
|
1568
|
+
if (!context || !isAttemptActive(context.generation)) return;
|
|
1569
|
+
if (!finishAttempt(context.generation)) return;
|
|
1570
|
+
approvalContextByTokenRef.current.delete(token);
|
|
1571
|
+
attemptContextBySurfaceRef.current.delete(context.surfaceKey);
|
|
1572
|
+
finishOverlay();
|
|
1096
1573
|
try {
|
|
1097
1574
|
setSubmitting(true);
|
|
1098
|
-
onErrorChangeRef.current?.(null);
|
|
1099
|
-
const token = data.subscriptionID ?? data.orderID ?? "";
|
|
1575
|
+
invokeMerchantCallback(() => onErrorChangeRef.current?.(null));
|
|
1100
1576
|
if (!token) {
|
|
1101
1577
|
throw new FloPayError2(
|
|
1102
1578
|
"PayPal did not return an approval token.",
|
|
@@ -1107,7 +1583,7 @@ function DirectPayPalButton({
|
|
|
1107
1583
|
await dispatchTokenizedBody({
|
|
1108
1584
|
id: token,
|
|
1109
1585
|
isPaypal: true
|
|
1110
|
-
});
|
|
1586
|
+
}, context);
|
|
1111
1587
|
} catch (err) {
|
|
1112
1588
|
forwardError(err instanceof Error ? err.message : "PayPal capture failed.");
|
|
1113
1589
|
} finally {
|
|
@@ -1123,47 +1599,104 @@ function DirectPayPalButton({
|
|
|
1123
1599
|
// matching the Stripe-rendered PayPal flow.
|
|
1124
1600
|
onClick: async (_data, actions) => {
|
|
1125
1601
|
const runner = runBeforeButtonClickRef.current;
|
|
1602
|
+
let prepared = {};
|
|
1126
1603
|
if (runner) {
|
|
1127
1604
|
try {
|
|
1128
1605
|
const beforeClick = await runner("paypal");
|
|
1129
1606
|
if (!beforeClick.proceed) {
|
|
1130
1607
|
beforeClickRef.current = null;
|
|
1608
|
+
attemptContextBySurfaceRef.current.delete(effectRenderGeneration);
|
|
1131
1609
|
await actions.reject();
|
|
1132
1610
|
return;
|
|
1133
1611
|
}
|
|
1134
|
-
|
|
1612
|
+
prepared = {
|
|
1135
1613
|
sessionId: beforeClick.sessionId,
|
|
1136
|
-
accountPatch: beforeClick.accountPatch
|
|
1614
|
+
accountPatch: beforeClick.accountPatch,
|
|
1615
|
+
nonce: beforeClick.nonce
|
|
1137
1616
|
};
|
|
1138
1617
|
} catch (err) {
|
|
1139
1618
|
appendDebug(`onClick:runBeforeButtonClick rejected msg=${(err instanceof Error ? err.message : String(err)).slice(0, 120)}`);
|
|
1140
1619
|
beforeClickRef.current = null;
|
|
1620
|
+
attemptContextBySurfaceRef.current.delete(effectRenderGeneration);
|
|
1141
1621
|
await actions.reject();
|
|
1142
1622
|
return;
|
|
1143
1623
|
}
|
|
1144
|
-
} else {
|
|
1145
|
-
beforeClickRef.current = null;
|
|
1146
1624
|
}
|
|
1147
|
-
onButtonClickRef.current?.("paypal");
|
|
1625
|
+
invokeMerchantCallback(() => onButtonClickRef.current?.("paypal"));
|
|
1626
|
+
const generation = startAttempt();
|
|
1627
|
+
const context = {
|
|
1628
|
+
generation,
|
|
1629
|
+
surfaceKey: effectRenderGeneration,
|
|
1630
|
+
...prepared
|
|
1631
|
+
};
|
|
1632
|
+
beforeClickRef.current = context;
|
|
1633
|
+
attemptContextBySurfaceRef.current.set(effectRenderGeneration, context);
|
|
1148
1634
|
await actions.resolve();
|
|
1635
|
+
telemetrySource.log({
|
|
1636
|
+
name: "payment.method.selected",
|
|
1637
|
+
stage: "processing",
|
|
1638
|
+
provider: "paypal",
|
|
1639
|
+
paymentMethodCategory: "paypal"
|
|
1640
|
+
});
|
|
1641
|
+
telemetrySource.log({
|
|
1642
|
+
name: "provider.popup.opened",
|
|
1643
|
+
stage: "overlay_open",
|
|
1644
|
+
provider: "paypal",
|
|
1645
|
+
paymentMethodCategory: "paypal"
|
|
1646
|
+
});
|
|
1647
|
+
telemetrySource.log({
|
|
1648
|
+
name: "provider.overlay.opened",
|
|
1649
|
+
stage: "overlay_open",
|
|
1650
|
+
provider: "paypal",
|
|
1651
|
+
paymentMethodCategory: "paypal"
|
|
1652
|
+
});
|
|
1653
|
+
overlayStartedAt = telemetrySource.startTiming();
|
|
1149
1654
|
},
|
|
1150
1655
|
// When the SDK is already holding an order/subscription id from a
|
|
1151
1656
|
// prior backend round-trip (the `paypal_direct_required` retry
|
|
1152
1657
|
// path), feed it straight to PayPal instead of creating a new one.
|
|
1153
1658
|
// Otherwise fall back to the normal create-intent call.
|
|
1154
|
-
createOrder: isSubscription ? void 0 : existingOrderId ? () => Promise.resolve(existingOrderId) : () => createPaypalIntent("Failed to create PayPal order."),
|
|
1155
|
-
createSubscription: isSubscription ? existingOrderId ? () => Promise.resolve(existingOrderId) : () => createPaypalIntent("Failed to create PayPal subscription.") : void 0,
|
|
1659
|
+
createOrder: isSubscription ? void 0 : existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal order.").then(registerApprovalContext),
|
|
1660
|
+
createSubscription: isSubscription ? existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal subscription.").then(registerApprovalContext) : void 0,
|
|
1156
1661
|
onApprove: handleApprove,
|
|
1157
1662
|
onCancel: () => {
|
|
1663
|
+
const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
|
|
1664
|
+
if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
|
|
1158
1665
|
beforeClickRef.current = null;
|
|
1159
|
-
|
|
1666
|
+
finishOverlay();
|
|
1667
|
+
telemetrySource.terminal({
|
|
1668
|
+
outcome: "payment_cancelled",
|
|
1669
|
+
provider: "paypal",
|
|
1670
|
+
paymentMethodCategory: "paypal"
|
|
1671
|
+
});
|
|
1672
|
+
pendingProviderFocusRef.current = true;
|
|
1673
|
+
invalidateAttempt({ generation: context?.generation, remount: true, focus: false });
|
|
1160
1674
|
},
|
|
1161
1675
|
onError: (err) => {
|
|
1676
|
+
if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
|
|
1162
1677
|
const message = err instanceof Error ? err.message : "PayPal failed to render.";
|
|
1163
1678
|
appendDebug(`onError rendered=${rendered} msg=${message.slice(0, 120)}`);
|
|
1164
1679
|
if (rendered) {
|
|
1165
|
-
|
|
1166
|
-
|
|
1680
|
+
const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
|
|
1681
|
+
if (context && !isAttemptActive(context.generation)) return;
|
|
1682
|
+
if (!context && invalidatedAttemptGenerationRef.current === attemptGenerationRef.current) return;
|
|
1683
|
+
if (isZoidLifecycleMessage(message)) {
|
|
1684
|
+
finishAttempt(context?.generation);
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
finishOverlay();
|
|
1688
|
+
const lower = message.toLowerCase();
|
|
1689
|
+
telemetrySource.error({
|
|
1690
|
+
errorCode: lower.includes("popup") && lower.includes("block") ? "POPUP_BLOCKED" : "PROVIDER_RUNTIME_FAILED",
|
|
1691
|
+
stage: "provider_ready",
|
|
1692
|
+
provider: "paypal",
|
|
1693
|
+
paymentMethodCategory: "paypal",
|
|
1694
|
+
requestCategory: "provider_sdk"
|
|
1695
|
+
});
|
|
1696
|
+
if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
|
|
1697
|
+
beforeClickRef.current = null;
|
|
1698
|
+
invalidateAttempt({ generation: context?.generation, remount: true, showRetry: true, focus: true });
|
|
1699
|
+
notifyTechnicalFailure(err, { code: "paypal_runtime_failed" });
|
|
1167
1700
|
return;
|
|
1168
1701
|
}
|
|
1169
1702
|
markRenderFailed(message);
|
|
@@ -1171,6 +1704,12 @@ function DirectPayPalButton({
|
|
|
1171
1704
|
});
|
|
1172
1705
|
const eligible = buttons.isEligible();
|
|
1173
1706
|
appendDebug(`isEligible=${eligible}`);
|
|
1707
|
+
telemetrySource.log({
|
|
1708
|
+
name: "provider.eligibility.checked",
|
|
1709
|
+
stage: "provider_ready",
|
|
1710
|
+
provider: "paypal",
|
|
1711
|
+
paymentMethodCategory: "paypal"
|
|
1712
|
+
});
|
|
1174
1713
|
if (!eligible) {
|
|
1175
1714
|
setReady(false);
|
|
1176
1715
|
markRenderFailed(
|
|
@@ -1242,6 +1781,19 @@ function DirectPayPalButton({
|
|
|
1242
1781
|
activeButtons = typedButtons;
|
|
1243
1782
|
rendered = true;
|
|
1244
1783
|
setReady(true);
|
|
1784
|
+
telemetrySource.log({
|
|
1785
|
+
name: "provider.ready",
|
|
1786
|
+
stage: "provider_ready",
|
|
1787
|
+
provider: "paypal",
|
|
1788
|
+
paymentMethodCategory: "paypal"
|
|
1789
|
+
});
|
|
1790
|
+
telemetrySource.performance({
|
|
1791
|
+
stage: "provider_ready",
|
|
1792
|
+
durationMs: telemetrySource.elapsed(providerStartedAt.current),
|
|
1793
|
+
durationMode: "machine",
|
|
1794
|
+
provider: "paypal",
|
|
1795
|
+
paymentMethodCategory: "paypal"
|
|
1796
|
+
});
|
|
1245
1797
|
}).catch((err) => {
|
|
1246
1798
|
const message = err instanceof Error ? err.message : "PayPal failed to render.";
|
|
1247
1799
|
appendDebug(`render:rejected msg=${message.slice(0, 120)}`);
|
|
@@ -1263,7 +1815,7 @@ function DirectPayPalButton({
|
|
|
1263
1815
|
});
|
|
1264
1816
|
}
|
|
1265
1817
|
};
|
|
1266
|
-
}, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId]);
|
|
1818
|
+
}, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration, telemetrySource]);
|
|
1267
1819
|
const debugPanel = debug ? /* @__PURE__ */ jsxs3(
|
|
1268
1820
|
"pre",
|
|
1269
1821
|
{
|
|
@@ -1318,18 +1870,38 @@ ${debugLines.join("\n")}`
|
|
|
1318
1870
|
{
|
|
1319
1871
|
ref: containerRef,
|
|
1320
1872
|
"data-testid": "flopay-direct-paypal-container",
|
|
1873
|
+
tabIndex: -1,
|
|
1874
|
+
"aria-label": "PayPal payment method",
|
|
1321
1875
|
style: {
|
|
1322
1876
|
minHeight: DEFAULT_BUTTON_HEIGHT,
|
|
1323
1877
|
display: "flex",
|
|
1324
1878
|
opacity: ready ? 1 : 0
|
|
1325
1879
|
},
|
|
1326
1880
|
"aria-busy": submitting || isProcessing
|
|
1327
|
-
}
|
|
1881
|
+
},
|
|
1882
|
+
renderGeneration
|
|
1328
1883
|
)
|
|
1329
|
-
] })
|
|
1884
|
+
] }),
|
|
1885
|
+
showRetryFocusTarget && /* @__PURE__ */ jsx6(
|
|
1886
|
+
"button",
|
|
1887
|
+
{
|
|
1888
|
+
ref: focusTargetRef,
|
|
1889
|
+
type: "button",
|
|
1890
|
+
"data-testid": "flopay-direct-paypal-focus-target",
|
|
1891
|
+
onClick: focusPayPalSurface,
|
|
1892
|
+
style: DIRECT_PAYPAL_RECOVERY_ACTION_STYLE,
|
|
1893
|
+
children: "Try PayPal again"
|
|
1894
|
+
}
|
|
1895
|
+
)
|
|
1330
1896
|
] })
|
|
1331
1897
|
);
|
|
1332
1898
|
}
|
|
1899
|
+
function DirectPayPalButton(props) {
|
|
1900
|
+
return /* @__PURE__ */ jsx6(DirectPayPalButtonImplementation, { ...props });
|
|
1901
|
+
}
|
|
1902
|
+
function InstrumentedDirectPayPalButton(props) {
|
|
1903
|
+
return /* @__PURE__ */ jsx6(DirectPayPalButtonImplementation, { ...props });
|
|
1904
|
+
}
|
|
1333
1905
|
|
|
1334
1906
|
// src/split-card-form.tsx
|
|
1335
1907
|
import { FloPayError as FloPayError3, isSetupIntentClientSecret as isSetupIntentClientSecret2, resolveTheme } from "@flopay/shared";
|
|
@@ -1410,6 +1982,19 @@ var BUTTONS_PANEL_HIDDEN_STYLE = {
|
|
|
1410
1982
|
height: 0,
|
|
1411
1983
|
overflow: "hidden"
|
|
1412
1984
|
};
|
|
1985
|
+
var EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE = {
|
|
1986
|
+
width: "100%",
|
|
1987
|
+
minHeight: DEFAULT_BUTTONS_LAYOUT_BUTTON_HEIGHT,
|
|
1988
|
+
margin: "0 0 0.5rem",
|
|
1989
|
+
padding: "0.75rem 0.875rem",
|
|
1990
|
+
border: "1px solid #2563eb",
|
|
1991
|
+
borderRadius: 8,
|
|
1992
|
+
background: "#eff6ff",
|
|
1993
|
+
color: "#1d4ed8",
|
|
1994
|
+
fontSize: "0.95rem",
|
|
1995
|
+
fontWeight: 700,
|
|
1996
|
+
cursor: "pointer"
|
|
1997
|
+
};
|
|
1413
1998
|
function getButtonMethodLabel(method) {
|
|
1414
1999
|
switch (method) {
|
|
1415
2000
|
case "paypal":
|
|
@@ -1422,6 +2007,118 @@ function getButtonMethodLabel(method) {
|
|
|
1422
2007
|
return "Card";
|
|
1423
2008
|
}
|
|
1424
2009
|
}
|
|
2010
|
+
function checkoutButtonMethodFromProviderMethod(method) {
|
|
2011
|
+
if (method === "paypal") return "paypal";
|
|
2012
|
+
if (method === "apple_pay") return "apple_pay";
|
|
2013
|
+
return "google_pay";
|
|
2014
|
+
}
|
|
2015
|
+
function getExternalMethodDisplayName(method) {
|
|
2016
|
+
return method === "paypal" ? "PayPal" : getStripeMethodDisplayName(method);
|
|
2017
|
+
}
|
|
2018
|
+
function buildExternalMethodRecoveryMessage(method, popupBlocked) {
|
|
2019
|
+
const base = `We couldn't open ${getExternalMethodDisplayName(method)}. Try again or choose another payment method.`;
|
|
2020
|
+
return popupBlocked ? `${base} Allow pop-ups for this site, then try again.` : base;
|
|
2021
|
+
}
|
|
2022
|
+
function useExternalAttemptReconciliation(onMissingTerminal) {
|
|
2023
|
+
const generationRef = useRef5(0);
|
|
2024
|
+
const invalidatedGenerationRef = useRef5(null);
|
|
2025
|
+
const attemptRef = useRef5(null);
|
|
2026
|
+
const clearAttemptTimer = useCallback3((targetAttempt = attemptRef.current) => {
|
|
2027
|
+
const attempt = targetAttempt;
|
|
2028
|
+
if (attempt?.timer) {
|
|
2029
|
+
clearTimeout(attempt.timer);
|
|
2030
|
+
attempt.timer = null;
|
|
2031
|
+
}
|
|
2032
|
+
}, []);
|
|
2033
|
+
const armAttemptTimer = useCallback3((attempt, delayMs) => {
|
|
2034
|
+
if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
|
|
2035
|
+
clearAttemptTimer(attempt);
|
|
2036
|
+
attempt.timer = setTimeout(() => {
|
|
2037
|
+
attempt.timer = null;
|
|
2038
|
+
if (attemptRef.current?.generation !== attempt.generation) return;
|
|
2039
|
+
attemptRef.current = null;
|
|
2040
|
+
invalidatedGenerationRef.current = attempt.generation;
|
|
2041
|
+
onMissingTerminal(
|
|
2042
|
+
attempt.method,
|
|
2043
|
+
new Error("External payment method returned without a terminal callback."),
|
|
2044
|
+
{ code: "external_method_missing_terminal_callback" }
|
|
2045
|
+
);
|
|
2046
|
+
}, delayMs);
|
|
2047
|
+
}, [clearAttemptTimer, onMissingTerminal]);
|
|
2048
|
+
const armRecoveryTimer = useCallback3((attempt) => {
|
|
2049
|
+
if (!attempt.yieldedControl) return;
|
|
2050
|
+
armAttemptTimer(attempt, EXTERNAL_METHOD_CALLBACK_GRACE_MS);
|
|
2051
|
+
}, [armAttemptTimer]);
|
|
2052
|
+
const startAttempt = useCallback3((method) => {
|
|
2053
|
+
clearAttemptTimer();
|
|
2054
|
+
const generation = generationRef.current + 1;
|
|
2055
|
+
generationRef.current = generation;
|
|
2056
|
+
invalidatedGenerationRef.current = null;
|
|
2057
|
+
const attempt = { generation, method, timer: null, yieldedControl: false };
|
|
2058
|
+
attemptRef.current = attempt;
|
|
2059
|
+
return generation;
|
|
2060
|
+
}, [clearAttemptTimer]);
|
|
2061
|
+
const finishAttempt = useCallback3((generation) => {
|
|
2062
|
+
const attempt = attemptRef.current;
|
|
2063
|
+
if (typeof generation === "number" && attempt?.generation !== generation) return false;
|
|
2064
|
+
clearAttemptTimer(attempt);
|
|
2065
|
+
attemptRef.current = null;
|
|
2066
|
+
if (typeof generation !== "number" || invalidatedGenerationRef.current === generation) {
|
|
2067
|
+
invalidatedGenerationRef.current = null;
|
|
2068
|
+
}
|
|
2069
|
+
return true;
|
|
2070
|
+
}, [clearAttemptTimer]);
|
|
2071
|
+
const invalidateAttempt = useCallback3((generation) => {
|
|
2072
|
+
const attempt = attemptRef.current;
|
|
2073
|
+
const targetGeneration = generation ?? attempt?.generation ?? generationRef.current;
|
|
2074
|
+
if (!generation || attempt?.generation === generation) {
|
|
2075
|
+
clearAttemptTimer(attempt);
|
|
2076
|
+
attemptRef.current = null;
|
|
2077
|
+
}
|
|
2078
|
+
invalidatedGenerationRef.current = targetGeneration;
|
|
2079
|
+
}, [clearAttemptTimer]);
|
|
2080
|
+
const isAttemptInvalidated = useCallback3(
|
|
2081
|
+
(generation) => invalidatedGenerationRef.current === (generation ?? generationRef.current),
|
|
2082
|
+
[]
|
|
2083
|
+
);
|
|
2084
|
+
const isAttemptCurrent = useCallback3(
|
|
2085
|
+
(generation) => attemptRef.current?.generation === generation && invalidatedGenerationRef.current !== generation,
|
|
2086
|
+
[]
|
|
2087
|
+
);
|
|
2088
|
+
const scheduleRecoveryIfReturned = useCallback3(() => {
|
|
2089
|
+
const attempt = attemptRef.current;
|
|
2090
|
+
if (!attempt) return;
|
|
2091
|
+
armRecoveryTimer(attempt);
|
|
2092
|
+
}, [armRecoveryTimer]);
|
|
2093
|
+
const markAttemptYieldedControl = useCallback3(() => {
|
|
2094
|
+
const attempt = attemptRef.current;
|
|
2095
|
+
if (!attempt) return;
|
|
2096
|
+
attempt.yieldedControl = true;
|
|
2097
|
+
clearAttemptTimer(attempt);
|
|
2098
|
+
}, [clearAttemptTimer]);
|
|
2099
|
+
useEffect5(() => {
|
|
2100
|
+
const handleVisibilityChange = () => {
|
|
2101
|
+
if (document.visibilityState === "visible") {
|
|
2102
|
+
scheduleRecoveryIfReturned();
|
|
2103
|
+
} else {
|
|
2104
|
+
markAttemptYieldedControl();
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
2107
|
+
const handleBlur = () => {
|
|
2108
|
+
markAttemptYieldedControl();
|
|
2109
|
+
};
|
|
2110
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
2111
|
+
window.addEventListener("blur", handleBlur);
|
|
2112
|
+
window.addEventListener("focus", scheduleRecoveryIfReturned);
|
|
2113
|
+
return () => {
|
|
2114
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
2115
|
+
window.removeEventListener("blur", handleBlur);
|
|
2116
|
+
window.removeEventListener("focus", scheduleRecoveryIfReturned);
|
|
2117
|
+
clearAttemptTimer();
|
|
2118
|
+
};
|
|
2119
|
+
}, [clearAttemptTimer, markAttemptYieldedControl, scheduleRecoveryIfReturned]);
|
|
2120
|
+
return { startAttempt, finishAttempt, invalidateAttempt, isAttemptInvalidated, isAttemptCurrent };
|
|
2121
|
+
}
|
|
1425
2122
|
function malformedPostcodeMessage(country) {
|
|
1426
2123
|
const example = getPostalCodeExample(country);
|
|
1427
2124
|
return `Enter a valid ${getPostalCodeLabel(country)}${example ? ` (e.g. ${example})` : ""}`;
|
|
@@ -1516,10 +2213,12 @@ function PayPalButtonInner({
|
|
|
1516
2213
|
isProcessing = false,
|
|
1517
2214
|
onButtonClick,
|
|
1518
2215
|
onDecline,
|
|
2216
|
+
onTechnicalFailure,
|
|
1519
2217
|
runBeforeButtonClick,
|
|
1520
2218
|
onLoadStateChange,
|
|
1521
2219
|
placeholderBorderRadius
|
|
1522
2220
|
}) {
|
|
2221
|
+
const flopay = useFloPay();
|
|
1523
2222
|
const stripe = useStripeRaw();
|
|
1524
2223
|
const elements = useStripeElements();
|
|
1525
2224
|
const [loadState, setLoadState] = useState3("loading");
|
|
@@ -1527,9 +2226,56 @@ function PayPalButtonInner({
|
|
|
1527
2226
|
onLoadStateChange?.(loadState);
|
|
1528
2227
|
}, [loadState, onLoadStateChange]);
|
|
1529
2228
|
const [submitting, setSubmitting] = useState3(false);
|
|
1530
|
-
const paypalResumeAttempted =
|
|
1531
|
-
const
|
|
2229
|
+
const paypalResumeAttempted = useRef5(false);
|
|
2230
|
+
const focusTargetRef = useRef5(null);
|
|
2231
|
+
const recoveryActionRef = useRef5(null);
|
|
2232
|
+
const [surfaceKey, setSurfaceKey] = useState3(0);
|
|
2233
|
+
const [showRecoveryAction, setShowRecoveryAction] = useState3(false);
|
|
2234
|
+
const pendingProviderFocusRef = useRef5(false);
|
|
2235
|
+
const attemptContextBySurfaceRef = useRef5(/* @__PURE__ */ new Map());
|
|
1532
2236
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
2237
|
+
const {
|
|
2238
|
+
startAttempt,
|
|
2239
|
+
finishAttempt,
|
|
2240
|
+
invalidateAttempt,
|
|
2241
|
+
isAttemptInvalidated,
|
|
2242
|
+
isAttemptCurrent
|
|
2243
|
+
} = useExternalAttemptReconciliation((method, err, options) => {
|
|
2244
|
+
onTechnicalFailure?.(method, err, {
|
|
2245
|
+
...options,
|
|
2246
|
+
popupBlocked: options?.popupBlocked ?? isPopupBlockedError(err)
|
|
2247
|
+
});
|
|
2248
|
+
setShowRecoveryAction(true);
|
|
2249
|
+
setSurfaceKey((key) => key + 1);
|
|
2250
|
+
});
|
|
2251
|
+
useEffect5(() => {
|
|
2252
|
+
if (showRecoveryAction) recoveryActionRef.current?.focus();
|
|
2253
|
+
}, [showRecoveryAction, surfaceKey]);
|
|
2254
|
+
const resetSurface = useCallback3(() => {
|
|
2255
|
+
setSurfaceKey((key) => key + 1);
|
|
2256
|
+
}, []);
|
|
2257
|
+
const recoverTechnicalFailure = useCallback3((err, code, generation) => {
|
|
2258
|
+
invalidateAttempt(generation);
|
|
2259
|
+
onTechnicalFailure?.("paypal", err, { code, popupBlocked: isPopupBlockedError(err) });
|
|
2260
|
+
setShowRecoveryAction(true);
|
|
2261
|
+
resetSurface();
|
|
2262
|
+
}, [invalidateAttempt, onTechnicalFailure, resetSurface]);
|
|
2263
|
+
const focusProviderSurface = useCallback3(() => {
|
|
2264
|
+
window.setTimeout(() => {
|
|
2265
|
+
const target = focusTargetRef.current?.querySelector("iframe");
|
|
2266
|
+
(target ?? focusTargetRef.current)?.focus();
|
|
2267
|
+
}, 0);
|
|
2268
|
+
}, []);
|
|
2269
|
+
useEffect5(() => {
|
|
2270
|
+
if (!pendingProviderFocusRef.current) return;
|
|
2271
|
+
pendingProviderFocusRef.current = false;
|
|
2272
|
+
focusProviderSurface();
|
|
2273
|
+
}, [focusProviderSurface, surfaceKey]);
|
|
2274
|
+
const handleRecoveryActionClick = useCallback3(() => {
|
|
2275
|
+
setShowRecoveryAction(false);
|
|
2276
|
+
onErrorChange?.(null);
|
|
2277
|
+
focusProviderSurface();
|
|
2278
|
+
}, [focusProviderSurface, onErrorChange]);
|
|
1533
2279
|
useEffect5(() => {
|
|
1534
2280
|
if (!stripe || paypalResumeAttempted.current) return;
|
|
1535
2281
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -1596,29 +2342,40 @@ function PayPalButtonInner({
|
|
|
1596
2342
|
}
|
|
1597
2343
|
})();
|
|
1598
2344
|
}, [stripe, onTokenizedBody, onErrorChange, onDecline]);
|
|
1599
|
-
const handlePayPalClick =
|
|
2345
|
+
const handlePayPalClick = useCallback3(async (event) => {
|
|
1600
2346
|
if (isProcessing || submitting) {
|
|
1601
2347
|
event.reject();
|
|
1602
2348
|
return;
|
|
1603
2349
|
}
|
|
1604
2350
|
const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick("paypal") : { proceed: true };
|
|
1605
2351
|
if (!beforeClick.proceed) {
|
|
1606
|
-
|
|
2352
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
1607
2353
|
event.reject();
|
|
1608
2354
|
return;
|
|
1609
2355
|
}
|
|
1610
|
-
|
|
2356
|
+
const generation = startAttempt("paypal");
|
|
2357
|
+
attemptContextBySurfaceRef.current.set(surfaceKey, {
|
|
2358
|
+
generation,
|
|
1611
2359
|
accountPatch: beforeClick.accountPatch,
|
|
1612
2360
|
sessionId: beforeClick.sessionId,
|
|
1613
2361
|
nonce: beforeClick.nonce
|
|
1614
|
-
};
|
|
2362
|
+
});
|
|
2363
|
+
setShowRecoveryAction(false);
|
|
1615
2364
|
onButtonClick?.("paypal");
|
|
1616
2365
|
event.resolve();
|
|
1617
|
-
}, [isProcessing, onButtonClick, runBeforeButtonClick, submitting]);
|
|
1618
|
-
const handlePayPalConfirm =
|
|
2366
|
+
}, [attemptContextBySurfaceRef, isProcessing, onButtonClick, runBeforeButtonClick, startAttempt, submitting, surfaceKey]);
|
|
2367
|
+
const handlePayPalConfirm = useCallback3(async (event) => {
|
|
1619
2368
|
if (!stripe || !elements) return;
|
|
1620
|
-
|
|
1621
|
-
|
|
2369
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2370
|
+
if (attemptContext && !isAttemptCurrent(attemptContext.generation)) {
|
|
2371
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2372
|
+
return;
|
|
2373
|
+
}
|
|
2374
|
+
if (!attemptContext && isAttemptInvalidated()) return;
|
|
2375
|
+
if (attemptContext && !finishAttempt(attemptContext.generation)) return;
|
|
2376
|
+
if (!attemptContext) finishAttempt();
|
|
2377
|
+
let prepared = attemptContext ?? null;
|
|
2378
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
1622
2379
|
if (!prepared && runBeforeButtonClick) {
|
|
1623
2380
|
const beforeClick = await runBeforeButtonClick("paypal");
|
|
1624
2381
|
if (!beforeClick.proceed) {
|
|
@@ -1643,9 +2400,8 @@ function PayPalButtonInner({
|
|
|
1643
2400
|
const createPM = stripe.createPaymentMethod;
|
|
1644
2401
|
const { error: pmError, paymentMethod } = await createPM({ type: "paypal" });
|
|
1645
2402
|
if (pmError) {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
event.paymentFailed({ reason: "fail", message });
|
|
2403
|
+
recoverTechnicalFailure(pmError, "stripe_paypal_create_payment_method_failed", attemptContext?.generation);
|
|
2404
|
+
event.paymentFailed({ reason: "fail" });
|
|
1649
2405
|
return;
|
|
1650
2406
|
}
|
|
1651
2407
|
const intentHeaders = { "Content-Type": "application/json" };
|
|
@@ -1704,11 +2460,16 @@ function PayPalButtonInner({
|
|
|
1704
2460
|
} catch {
|
|
1705
2461
|
}
|
|
1706
2462
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2463
|
+
if (isProviderDecline(confirmError)) {
|
|
2464
|
+
const message = confirmError.message ?? "Your payment was declined.";
|
|
2465
|
+
onErrorChange?.(message);
|
|
2466
|
+
onDecline?.(buildDeclineEvent("paypal", message, {
|
|
2467
|
+
code: getProviderErrorCode(confirmError),
|
|
2468
|
+
declineCode: getProviderDeclineCode(confirmError)
|
|
2469
|
+
}));
|
|
2470
|
+
} else {
|
|
2471
|
+
recoverTechnicalFailure(confirmError, "stripe_paypal_confirm_failed", attemptContext?.generation);
|
|
2472
|
+
}
|
|
1712
2473
|
return;
|
|
1713
2474
|
}
|
|
1714
2475
|
if (typeof window !== "undefined") {
|
|
@@ -1730,11 +2491,11 @@ function PayPalButtonInner({
|
|
|
1730
2491
|
});
|
|
1731
2492
|
return;
|
|
1732
2493
|
} catch (err) {
|
|
1733
|
-
|
|
2494
|
+
recoverTechnicalFailure(err, "stripe_paypal_failed", attemptContext?.generation);
|
|
1734
2495
|
} finally {
|
|
1735
2496
|
setSubmitting(false);
|
|
1736
2497
|
}
|
|
1737
|
-
}, [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]);
|
|
2498
|
+
}, [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]);
|
|
1738
2499
|
return /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
1739
2500
|
/* @__PURE__ */ jsx7(
|
|
1740
2501
|
ExpressCheckoutReadySwap,
|
|
@@ -1742,29 +2503,62 @@ function PayPalButtonInner({
|
|
|
1742
2503
|
state: loadState,
|
|
1743
2504
|
placeholderTestId: "flopay-paypal-placeholder",
|
|
1744
2505
|
borderRadius: placeholderBorderRadius,
|
|
1745
|
-
children: /* @__PURE__ */
|
|
1746
|
-
|
|
2506
|
+
children: /* @__PURE__ */ jsxs4(
|
|
2507
|
+
"div",
|
|
1747
2508
|
{
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2509
|
+
ref: focusTargetRef,
|
|
2510
|
+
tabIndex: -1,
|
|
2511
|
+
"data-testid": "flopay-paypal-focus-target",
|
|
2512
|
+
"aria-label": "PayPal payment method",
|
|
2513
|
+
style: { borderRadius: 8, outlineOffset: 4 },
|
|
2514
|
+
children: [
|
|
2515
|
+
showRecoveryAction && /* @__PURE__ */ jsx7(
|
|
2516
|
+
"button",
|
|
2517
|
+
{
|
|
2518
|
+
ref: recoveryActionRef,
|
|
2519
|
+
type: "button",
|
|
2520
|
+
"data-testid": "flopay-paypal-retry-button",
|
|
2521
|
+
onClick: handleRecoveryActionClick,
|
|
2522
|
+
style: EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE,
|
|
2523
|
+
children: "Try PayPal again"
|
|
2524
|
+
}
|
|
2525
|
+
),
|
|
2526
|
+
/* @__PURE__ */ jsx7(
|
|
2527
|
+
ExpressCheckoutElement,
|
|
2528
|
+
{
|
|
2529
|
+
onReady: (event) => setLoadState(resolveExpressCheckoutLoadState(event, ["paypal"])),
|
|
2530
|
+
onLoadError: () => setLoadState("load_error"),
|
|
2531
|
+
onClick: handlePayPalClick,
|
|
2532
|
+
onConfirm: handlePayPalConfirm,
|
|
2533
|
+
onCancel: () => {
|
|
2534
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2535
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2536
|
+
getFloPayTelemetryBridge(flopay)?.terminal({
|
|
2537
|
+
outcome: "payment_cancelled",
|
|
2538
|
+
provider: "paypal",
|
|
2539
|
+
paymentMethodCategory: "paypal"
|
|
2540
|
+
});
|
|
2541
|
+
invalidateAttempt(attemptContext?.generation);
|
|
2542
|
+
setShowRecoveryAction(false);
|
|
2543
|
+
pendingProviderFocusRef.current = true;
|
|
2544
|
+
resetSurface();
|
|
2545
|
+
},
|
|
2546
|
+
options: {
|
|
2547
|
+
buttonType: { paypal: "paypal" },
|
|
2548
|
+
billingAddressRequired: false,
|
|
2549
|
+
phoneNumberRequired: false,
|
|
2550
|
+
shippingAddressRequired: false,
|
|
2551
|
+
paymentMethods: {
|
|
2552
|
+
applePay: "never",
|
|
2553
|
+
googlePay: "never",
|
|
2554
|
+
paypal: "auto",
|
|
2555
|
+
link: "never"
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
},
|
|
2559
|
+
surfaceKey
|
|
2560
|
+
)
|
|
2561
|
+
]
|
|
1768
2562
|
}
|
|
1769
2563
|
)
|
|
1770
2564
|
}
|
|
@@ -1782,10 +2576,12 @@ function WalletButtonInner({
|
|
|
1782
2576
|
onErrorChange,
|
|
1783
2577
|
onButtonClick,
|
|
1784
2578
|
onDecline,
|
|
2579
|
+
onTechnicalFailure,
|
|
1785
2580
|
runBeforeButtonClick,
|
|
1786
2581
|
onLoadStateChange,
|
|
1787
2582
|
placeholderBorderRadius
|
|
1788
2583
|
}) {
|
|
2584
|
+
const flopay = useFloPay();
|
|
1789
2585
|
const stripe = useStripeRaw();
|
|
1790
2586
|
const elements = useStripeElements();
|
|
1791
2587
|
const [loadState, setLoadState] = useState3("loading");
|
|
@@ -1794,15 +2590,74 @@ function WalletButtonInner({
|
|
|
1794
2590
|
}, [loadState, onLoadStateChange]);
|
|
1795
2591
|
const [submitting, setSubmitting] = useState3(false);
|
|
1796
2592
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
1797
|
-
const
|
|
1798
|
-
const
|
|
1799
|
-
const
|
|
2593
|
+
const focusTargetRef = useRef5(null);
|
|
2594
|
+
const recoveryActionRef = useRef5(null);
|
|
2595
|
+
const [surfaceKey, setSurfaceKey] = useState3(0);
|
|
2596
|
+
const [showRecoveryAction, setShowRecoveryAction] = useState3(false);
|
|
2597
|
+
const [recoveryActionMethod, setRecoveryActionMethod] = useState3("google_pay");
|
|
2598
|
+
const pendingProviderFocusRef = useRef5(false);
|
|
2599
|
+
const lastWalletProviderMethodRef = useRef5("google_pay");
|
|
2600
|
+
const lastWalletMethodRef = useRef5("google_pay");
|
|
2601
|
+
const attemptContextBySurfaceRef = useRef5(/* @__PURE__ */ new Map());
|
|
2602
|
+
const {
|
|
2603
|
+
startAttempt,
|
|
2604
|
+
finishAttempt,
|
|
2605
|
+
invalidateAttempt,
|
|
2606
|
+
isAttemptInvalidated,
|
|
2607
|
+
isAttemptCurrent
|
|
2608
|
+
} = useExternalAttemptReconciliation((method, err, options) => {
|
|
2609
|
+
onTechnicalFailure?.(method, err, {
|
|
2610
|
+
...options,
|
|
2611
|
+
popupBlocked: options?.popupBlocked ?? isPopupBlockedError(err)
|
|
2612
|
+
});
|
|
2613
|
+
setRecoveryActionMethod(method);
|
|
2614
|
+
setShowRecoveryAction(true);
|
|
2615
|
+
setSurfaceKey((key) => key + 1);
|
|
2616
|
+
});
|
|
2617
|
+
useEffect5(() => {
|
|
2618
|
+
if (showRecoveryAction) recoveryActionRef.current?.focus();
|
|
2619
|
+
}, [showRecoveryAction, surfaceKey]);
|
|
2620
|
+
const resetSurface = useCallback3(() => {
|
|
2621
|
+
setSurfaceKey((key) => key + 1);
|
|
2622
|
+
}, []);
|
|
2623
|
+
const recoverTechnicalFailure = useCallback3((method, err, code, generation) => {
|
|
2624
|
+
invalidateAttempt(generation);
|
|
2625
|
+
onTechnicalFailure?.(method, err, { code, popupBlocked: isPopupBlockedError(err) });
|
|
2626
|
+
setRecoveryActionMethod(method);
|
|
2627
|
+
setShowRecoveryAction(true);
|
|
2628
|
+
resetSurface();
|
|
2629
|
+
}, [invalidateAttempt, onTechnicalFailure, resetSurface]);
|
|
2630
|
+
const focusProviderSurface = useCallback3(() => {
|
|
2631
|
+
window.setTimeout(() => {
|
|
2632
|
+
const target = focusTargetRef.current?.querySelector("iframe");
|
|
2633
|
+
(target ?? focusTargetRef.current)?.focus();
|
|
2634
|
+
}, 0);
|
|
2635
|
+
}, []);
|
|
2636
|
+
useEffect5(() => {
|
|
2637
|
+
if (!pendingProviderFocusRef.current) return;
|
|
2638
|
+
pendingProviderFocusRef.current = false;
|
|
2639
|
+
focusProviderSurface();
|
|
2640
|
+
}, [focusProviderSurface, surfaceKey]);
|
|
2641
|
+
const handleRecoveryActionClick = useCallback3(() => {
|
|
2642
|
+
setShowRecoveryAction(false);
|
|
2643
|
+
onErrorChange?.(null);
|
|
2644
|
+
focusProviderSurface();
|
|
2645
|
+
}, [focusProviderSurface, onErrorChange]);
|
|
2646
|
+
const handleWalletConfirm = useCallback3(
|
|
1800
2647
|
async (event) => {
|
|
1801
2648
|
if (!stripe || !elements) return;
|
|
1802
|
-
const
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
2649
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2650
|
+
if (attemptContext && !isAttemptCurrent(attemptContext.generation)) {
|
|
2651
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
if (!attemptContext && isAttemptInvalidated()) return;
|
|
2655
|
+
if (attemptContext && !finishAttempt(attemptContext.generation)) return;
|
|
2656
|
+
if (!attemptContext) finishAttempt();
|
|
2657
|
+
const walletType = event.expressPaymentType ?? attemptContext?.method ?? lastWalletProviderMethodRef.current;
|
|
2658
|
+
let prepared = attemptContext ?? null;
|
|
2659
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2660
|
+
const buttonMethod = checkoutButtonMethodFromProviderMethod(walletType);
|
|
1806
2661
|
if (!prepared && runBeforeButtonClick) {
|
|
1807
2662
|
const beforeClick = await runBeforeButtonClick(buttonMethod);
|
|
1808
2663
|
if (!beforeClick.proceed) {
|
|
@@ -1824,12 +2679,12 @@ function WalletButtonInner({
|
|
|
1824
2679
|
onErrorChange?.(null);
|
|
1825
2680
|
const { error: submitError } = await elements.submit();
|
|
1826
2681
|
if (submitError) {
|
|
1827
|
-
|
|
2682
|
+
recoverTechnicalFailure(walletType, submitError, "stripe_wallet_submit_failed", attemptContext?.generation);
|
|
1828
2683
|
return;
|
|
1829
2684
|
}
|
|
1830
2685
|
const { error: pmError, paymentMethod } = await stripe.createPaymentMethod({ elements });
|
|
1831
2686
|
if (pmError || !paymentMethod) {
|
|
1832
|
-
|
|
2687
|
+
recoverTechnicalFailure(walletType, pmError ?? new Error("Failed to create payment method."), "stripe_wallet_create_payment_method_failed", attemptContext?.generation);
|
|
1833
2688
|
return;
|
|
1834
2689
|
}
|
|
1835
2690
|
if (!effectiveSessionId || !effectiveEmail) {
|
|
@@ -1862,11 +2717,16 @@ function WalletButtonInner({
|
|
|
1862
2717
|
if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
|
|
1863
2718
|
const { error: confirmError, intentId } = isSetupIntentClientSecret2(intentClientSecret) ? await stripe.confirmCardSetup(intentClientSecret, { payment_method: paymentMethod.id }).then((r) => ({ error: r.error, intentId: r.setupIntent?.id })) : await stripe.confirmCardPayment(intentClientSecret, { payment_method: paymentMethod.id }).then((r) => ({ error: r.error, intentId: r.paymentIntent?.id }));
|
|
1864
2719
|
if (confirmError) {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2720
|
+
if (isProviderDecline(confirmError)) {
|
|
2721
|
+
const message = confirmError.message ?? "Your payment was declined.";
|
|
2722
|
+
onErrorChange?.(message);
|
|
2723
|
+
onDecline?.(buildDeclineEvent(method, message, {
|
|
2724
|
+
code: getProviderErrorCode(confirmError),
|
|
2725
|
+
declineCode: getProviderDeclineCode(confirmError)
|
|
2726
|
+
}));
|
|
2727
|
+
} else {
|
|
2728
|
+
recoverTechnicalFailure(walletType, confirmError, "stripe_wallet_confirm_failed", attemptContext?.generation);
|
|
2729
|
+
}
|
|
1870
2730
|
return;
|
|
1871
2731
|
}
|
|
1872
2732
|
onTokenizedBody({
|
|
@@ -1879,12 +2739,12 @@ function WalletButtonInner({
|
|
|
1879
2739
|
nonce: effectiveNonce
|
|
1880
2740
|
});
|
|
1881
2741
|
} catch (err) {
|
|
1882
|
-
|
|
2742
|
+
recoverTechnicalFailure(walletType, err, "stripe_wallet_failed", attemptContext?.generation);
|
|
1883
2743
|
} finally {
|
|
1884
2744
|
setSubmitting(false);
|
|
1885
2745
|
}
|
|
1886
2746
|
},
|
|
1887
|
-
[stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick]
|
|
2747
|
+
[stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]
|
|
1888
2748
|
);
|
|
1889
2749
|
const expressMethodMap = useMemo3(() => {
|
|
1890
2750
|
const allKeys = ["applePay", "googlePay", "paypal", "link", "amazonPay", "klarna"];
|
|
@@ -1911,41 +2771,90 @@ function WalletButtonInner({
|
|
|
1911
2771
|
state: loadState,
|
|
1912
2772
|
placeholderTestId: "flopay-wallet-placeholder",
|
|
1913
2773
|
borderRadius: placeholderBorderRadius,
|
|
1914
|
-
children: /* @__PURE__ */
|
|
1915
|
-
|
|
2774
|
+
children: /* @__PURE__ */ jsxs4(
|
|
2775
|
+
"div",
|
|
1916
2776
|
{
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
2777
|
+
ref: focusTargetRef,
|
|
2778
|
+
tabIndex: -1,
|
|
2779
|
+
"data-testid": "flopay-wallet-focus-target",
|
|
2780
|
+
"aria-label": "Wallet payment methods",
|
|
2781
|
+
style: { borderRadius: 8, outlineOffset: 4 },
|
|
2782
|
+
children: [
|
|
2783
|
+
showRecoveryAction && /* @__PURE__ */ jsxs4(
|
|
2784
|
+
"button",
|
|
2785
|
+
{
|
|
2786
|
+
ref: recoveryActionRef,
|
|
2787
|
+
type: "button",
|
|
2788
|
+
"data-testid": "flopay-wallet-retry-button",
|
|
2789
|
+
onClick: handleRecoveryActionClick,
|
|
2790
|
+
style: EXTERNAL_METHOD_RECOVERY_BUTTON_STYLE,
|
|
2791
|
+
children: [
|
|
2792
|
+
"Try ",
|
|
2793
|
+
getExternalMethodDisplayName(recoveryActionMethod),
|
|
2794
|
+
" again"
|
|
2795
|
+
]
|
|
2796
|
+
}
|
|
2797
|
+
),
|
|
2798
|
+
/* @__PURE__ */ jsx7(
|
|
2799
|
+
ExpressCheckoutElement,
|
|
2800
|
+
{
|
|
2801
|
+
onReady: (event) => {
|
|
2802
|
+
setLoadState(resolveExpressCheckoutLoadState(event, availableMethodKeys));
|
|
2803
|
+
},
|
|
2804
|
+
onLoadError: (_event) => {
|
|
2805
|
+
setLoadState("load_error");
|
|
2806
|
+
getFloPayTelemetryBridge(flopay)?.error({
|
|
2807
|
+
errorCode: "PROVIDER_LOAD_FAILED",
|
|
2808
|
+
stage: "provider_load",
|
|
2809
|
+
provider: "stripe",
|
|
2810
|
+
paymentMethodCategory: "wallet",
|
|
2811
|
+
requestCategory: "provider_sdk"
|
|
2812
|
+
});
|
|
2813
|
+
},
|
|
2814
|
+
onClick: async (event) => {
|
|
2815
|
+
lastWalletProviderMethodRef.current = event.expressPaymentType;
|
|
2816
|
+
lastWalletMethodRef.current = checkoutButtonMethodFromProviderMethod(event.expressPaymentType);
|
|
2817
|
+
const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick(lastWalletMethodRef.current) : { proceed: true };
|
|
2818
|
+
if (!beforeClick.proceed) {
|
|
2819
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2820
|
+
event.reject();
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
const generation = startAttempt(event.expressPaymentType);
|
|
2824
|
+
attemptContextBySurfaceRef.current.set(surfaceKey, {
|
|
2825
|
+
generation,
|
|
2826
|
+
method: event.expressPaymentType,
|
|
2827
|
+
accountPatch: beforeClick.accountPatch,
|
|
2828
|
+
sessionId: beforeClick.sessionId,
|
|
2829
|
+
nonce: beforeClick.nonce
|
|
2830
|
+
});
|
|
2831
|
+
setShowRecoveryAction(false);
|
|
2832
|
+
onButtonClick?.(lastWalletMethodRef.current);
|
|
2833
|
+
event.resolve();
|
|
2834
|
+
},
|
|
2835
|
+
onConfirm: handleWalletConfirm,
|
|
2836
|
+
onCancel: () => {
|
|
2837
|
+
const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
|
|
2838
|
+
attemptContextBySurfaceRef.current.delete(surfaceKey);
|
|
2839
|
+
getFloPayTelemetryBridge(flopay)?.terminal({
|
|
2840
|
+
outcome: "payment_cancelled",
|
|
2841
|
+
provider: "stripe",
|
|
2842
|
+
paymentMethodCategory: "wallet"
|
|
2843
|
+
});
|
|
2844
|
+
invalidateAttempt(attemptContext?.generation);
|
|
2845
|
+
setShowRecoveryAction(false);
|
|
2846
|
+
pendingProviderFocusRef.current = true;
|
|
2847
|
+
resetSurface();
|
|
2848
|
+
},
|
|
2849
|
+
options: {
|
|
2850
|
+
buttonType: { applePay: "plain", googlePay: "plain" },
|
|
2851
|
+
paymentMethods: expressMethodMap,
|
|
2852
|
+
layout: { maxColumns: 1, overflow: "never" }
|
|
2853
|
+
}
|
|
2854
|
+
},
|
|
2855
|
+
surfaceKey
|
|
2856
|
+
)
|
|
2857
|
+
]
|
|
1949
2858
|
}
|
|
1950
2859
|
)
|
|
1951
2860
|
}
|
|
@@ -2102,14 +3011,15 @@ function StripeMethodInlineForm({
|
|
|
2102
3011
|
submitButtonStyle,
|
|
2103
3012
|
errorText
|
|
2104
3013
|
}) {
|
|
3014
|
+
const flopay = useFloPay();
|
|
2105
3015
|
const stripe = useStripeRaw();
|
|
2106
3016
|
const elements = useStripeElements();
|
|
2107
3017
|
const [submitting, setSubmitting] = useState3(false);
|
|
2108
|
-
const submittingRef =
|
|
3018
|
+
const submittingRef = useRef5(false);
|
|
2109
3019
|
const [isMethodComplete, setIsMethodComplete] = useState3(false);
|
|
2110
3020
|
const [loadState, setLoadState] = useState3("loading");
|
|
2111
3021
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
2112
|
-
const handlePay =
|
|
3022
|
+
const handlePay = useCallback3(async () => {
|
|
2113
3023
|
if (!stripe || !elements || isProcessing || submittingRef.current || !isMethodComplete) return;
|
|
2114
3024
|
submittingRef.current = true;
|
|
2115
3025
|
setSubmitting(true);
|
|
@@ -2254,7 +3164,16 @@ function StripeMethodInlineForm({
|
|
|
2254
3164
|
PaymentElement2,
|
|
2255
3165
|
{
|
|
2256
3166
|
onReady: () => setLoadState("ready"),
|
|
2257
|
-
onLoadError: () =>
|
|
3167
|
+
onLoadError: () => {
|
|
3168
|
+
setLoadState("load_error");
|
|
3169
|
+
getFloPayTelemetryBridge(flopay)?.error({
|
|
3170
|
+
errorCode: "PROVIDER_LOAD_FAILED",
|
|
3171
|
+
stage: "provider_load",
|
|
3172
|
+
provider: "stripe",
|
|
3173
|
+
paymentMethodCategory: "apm",
|
|
3174
|
+
requestCategory: "provider_sdk"
|
|
3175
|
+
});
|
|
3176
|
+
},
|
|
2258
3177
|
onChange: (event) => {
|
|
2259
3178
|
const evRecord = event;
|
|
2260
3179
|
setIsMethodComplete(!!evRecord.complete);
|
|
@@ -2342,7 +3261,7 @@ function StripePaymentElementInner({
|
|
|
2342
3261
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
2343
3262
|
const [expandedMethod, setExpandedMethod] = useState3(null);
|
|
2344
3263
|
const [submittingMethod, setSubmittingMethod] = useState3(null);
|
|
2345
|
-
const submittingRef =
|
|
3264
|
+
const submittingRef = useRef5(false);
|
|
2346
3265
|
useEffect5(() => {
|
|
2347
3266
|
if (paymentElementMethods.length > 0 && stripeInstance) {
|
|
2348
3267
|
onLoadStateChange?.("ready");
|
|
@@ -2350,7 +3269,7 @@ function StripePaymentElementInner({
|
|
|
2350
3269
|
onLoadStateChange?.("loading");
|
|
2351
3270
|
}
|
|
2352
3271
|
}, [paymentElementMethods.length, stripeInstance, onLoadStateChange]);
|
|
2353
|
-
const handleAutoConfirm =
|
|
3272
|
+
const handleAutoConfirm = useCallback3(async (method) => {
|
|
2354
3273
|
if (!stripeInstance) return;
|
|
2355
3274
|
if (submittingRef.current || isProcessing) return;
|
|
2356
3275
|
submittingRef.current = true;
|
|
@@ -2491,7 +3410,7 @@ function StripePaymentElementInner({
|
|
|
2491
3410
|
]);
|
|
2492
3411
|
const [localExpandedMethod, setLocalExpandedMethod] = useState3(null);
|
|
2493
3412
|
const activeExpandedMethod = onExpandApm ? expandedApmMethod ?? null : localExpandedMethod;
|
|
2494
|
-
const handleMethodClick =
|
|
3413
|
+
const handleMethodClick = useCallback3((method) => {
|
|
2495
3414
|
if (submittingRef.current || isProcessing) return;
|
|
2496
3415
|
if (activeExpandedMethod && activeExpandedMethod !== method) return;
|
|
2497
3416
|
if (needsStripeMethodExplicitConfirm(method)) {
|
|
@@ -2649,12 +3568,12 @@ function SplitCardFormInner({
|
|
|
2649
3568
|
const [city, setCity] = useState3(cityProp ?? "");
|
|
2650
3569
|
const [stateValue, setStateValue] = useState3(stateProp ?? "");
|
|
2651
3570
|
const [accountPatch, setAccountPatch] = useState3({});
|
|
2652
|
-
const zipCodeRef =
|
|
2653
|
-
const selectedCountryRef =
|
|
2654
|
-
const addressLine1Ref =
|
|
2655
|
-
const addressLine2Ref =
|
|
2656
|
-
const cityRef =
|
|
2657
|
-
const stateRef =
|
|
3571
|
+
const zipCodeRef = useRef5(zipProp ?? "");
|
|
3572
|
+
const selectedCountryRef = useRef5(countryProp ?? "US");
|
|
3573
|
+
const addressLine1Ref = useRef5(addressLine1Prop ?? "");
|
|
3574
|
+
const addressLine2Ref = useRef5(addressLine2Prop ?? "");
|
|
3575
|
+
const cityRef = useRef5(cityProp ?? "");
|
|
3576
|
+
const stateRef = useRef5(stateProp ?? "");
|
|
2658
3577
|
const avsConfig = useMemo3(() => resolveAVSConfig(enableAVSProp), [enableAVSProp]);
|
|
2659
3578
|
const enableAVS = avsConfig !== null;
|
|
2660
3579
|
const vaultBlockReady = Boolean(session?.vault?.html);
|
|
@@ -2696,20 +3615,20 @@ function SplitCardFormInner({
|
|
|
2696
3615
|
const [expandedApmMethod, setExpandedApmMethod] = useState3(null);
|
|
2697
3616
|
const showCardForm = viewState === "expanding" || viewState === "card";
|
|
2698
3617
|
const TRANSITION_MS = 280;
|
|
2699
|
-
const expandToCard =
|
|
3618
|
+
const expandToCard = useCallback3(() => {
|
|
2700
3619
|
setViewState("expanding");
|
|
2701
3620
|
setTimeout(() => setViewState("card"), TRANSITION_MS);
|
|
2702
3621
|
}, []);
|
|
2703
|
-
const collapseToButtons =
|
|
3622
|
+
const collapseToButtons = useCallback3(() => {
|
|
2704
3623
|
setViewState("collapsing");
|
|
2705
3624
|
setTimeout(() => setViewState("buttons"), TRANSITION_MS);
|
|
2706
3625
|
}, []);
|
|
2707
|
-
const expandToApm =
|
|
3626
|
+
const expandToApm = useCallback3((method) => {
|
|
2708
3627
|
setExpandedApmMethod(method);
|
|
2709
3628
|
setViewState("apm-expanding");
|
|
2710
3629
|
setTimeout(() => setViewState("apm-form"), TRANSITION_MS);
|
|
2711
3630
|
}, []);
|
|
2712
|
-
const collapseFromApm =
|
|
3631
|
+
const collapseFromApm = useCallback3(() => {
|
|
2713
3632
|
setViewState("apm-collapsing");
|
|
2714
3633
|
setTimeout(() => {
|
|
2715
3634
|
setViewState("buttons");
|
|
@@ -2724,9 +3643,9 @@ function SplitCardFormInner({
|
|
|
2724
3643
|
const [fullName, setFullName] = useState3("");
|
|
2725
3644
|
const [formReady, setFormReady] = useState3(false);
|
|
2726
3645
|
const [overlayStatus, setOverlayStatus] = useState3(null);
|
|
2727
|
-
const processingRef =
|
|
3646
|
+
const processingRef = useRef5(false);
|
|
2728
3647
|
const [paypalDirectRetry, setPaypalDirectRetry] = useState3(null);
|
|
2729
|
-
const paypalDirectRetryRef =
|
|
3648
|
+
const paypalDirectRetryRef = useRef5(paypalDirectRetry);
|
|
2730
3649
|
useEffect5(() => {
|
|
2731
3650
|
paypalDirectRetryRef.current = paypalDirectRetry;
|
|
2732
3651
|
}, [paypalDirectRetry]);
|
|
@@ -2834,7 +3753,7 @@ function SplitCardFormInner({
|
|
|
2834
3753
|
setupFutureUsage: "off_session",
|
|
2835
3754
|
...stripeAppearanceProp
|
|
2836
3755
|
}), [amountInCents, currency, stripeAppearanceProp]);
|
|
2837
|
-
const updateError =
|
|
3756
|
+
const updateError = useCallback3(
|
|
2838
3757
|
(err) => {
|
|
2839
3758
|
setError(err);
|
|
2840
3759
|
onErrorChange?.(err);
|
|
@@ -2850,12 +3769,36 @@ function SplitCardFormInner({
|
|
|
2850
3769
|
updateError(null);
|
|
2851
3770
|
}
|
|
2852
3771
|
}, [viewState, updateError]);
|
|
2853
|
-
const emitDecline =
|
|
3772
|
+
const emitDecline = useCallback3(
|
|
2854
3773
|
(method, input, overrides) => {
|
|
2855
3774
|
onDecline?.(buildDeclineEvent(method, input, overrides));
|
|
2856
3775
|
},
|
|
2857
3776
|
[onDecline]
|
|
2858
3777
|
);
|
|
3778
|
+
const recoverExternalMethodTechnicalFailure = useCallback3(
|
|
3779
|
+
(method, err, options) => {
|
|
3780
|
+
const popupBlocked = options?.popupBlocked ?? isPopupBlockedError(err);
|
|
3781
|
+
const message = buildExternalMethodRecoveryMessage(method, popupBlocked);
|
|
3782
|
+
const providerCode = sanitizeExternalFailureCode(options?.code) ?? getProviderErrorCode(err) ?? "external_payment_method_failed";
|
|
3783
|
+
const floPayError = new FloPayError3(message, "api_error", {
|
|
3784
|
+
code: "external_payment_method_failed",
|
|
3785
|
+
param: method
|
|
3786
|
+
});
|
|
3787
|
+
setOverlayStatus(null);
|
|
3788
|
+
if (layout === "buttons") {
|
|
3789
|
+
setViewState("buttons");
|
|
3790
|
+
setExpandedApmMethod(null);
|
|
3791
|
+
}
|
|
3792
|
+
updateError(message);
|
|
3793
|
+
onError?.(floPayError);
|
|
3794
|
+
console.error("[FloPay] External payment method failed:", {
|
|
3795
|
+
method,
|
|
3796
|
+
code: providerCode,
|
|
3797
|
+
popupBlocked
|
|
3798
|
+
});
|
|
3799
|
+
},
|
|
3800
|
+
[layout, onError, updateError]
|
|
3801
|
+
);
|
|
2859
3802
|
useEffect5(() => {
|
|
2860
3803
|
if (!vaultActive || !sessionId) {
|
|
2861
3804
|
setVaultMount(null);
|
|
@@ -2894,7 +3837,7 @@ function SplitCardFormInner({
|
|
|
2894
3837
|
nonce,
|
|
2895
3838
|
updateError
|
|
2896
3839
|
]);
|
|
2897
|
-
const vaultOutcomeRef =
|
|
3840
|
+
const vaultOutcomeRef = useRef5({
|
|
2898
3841
|
onComplete,
|
|
2899
3842
|
onError,
|
|
2900
3843
|
updateError,
|
|
@@ -2920,8 +3863,8 @@ function SplitCardFormInner({
|
|
|
2920
3863
|
nonce,
|
|
2921
3864
|
baseUrl
|
|
2922
3865
|
};
|
|
2923
|
-
const vaultCompletedRef =
|
|
2924
|
-
const buildVaultAccountSnapshot =
|
|
3866
|
+
const vaultCompletedRef = useRef5(false);
|
|
3867
|
+
const buildVaultAccountSnapshot = useCallback3(() => {
|
|
2925
3868
|
const { resolvedAccount: resolvedAccount2, avsConfig: avsConfig2, fullName: fullName2, avsCheckProp: avsCheckProp2 } = vaultOutcomeRef.current;
|
|
2926
3869
|
const cc = selectedCountryRef.current || resolvedAccount2.country || "US";
|
|
2927
3870
|
const stateVisible = avsConfig2 ? isAVSFieldVisible(avsConfig2.state, cc) : false;
|
|
@@ -3077,7 +4020,7 @@ function SplitCardFormInner({
|
|
|
3077
4020
|
const shouldDisplayPayPalRow = shouldRenderDirectPayPal ? directPaypalReady : shouldRenderStripePayPal && isExpressCheckoutRowVisible(paypalLoadState);
|
|
3078
4021
|
const shouldDisplayWalletRow = shouldRenderWallets && isExpressCheckoutRowVisible(walletLoadState);
|
|
3079
4022
|
const shouldDisplayPaymentElementRow = shouldRenderPaymentElement && paymentElementLoadState !== "load_error";
|
|
3080
|
-
const validationFiredRef =
|
|
4023
|
+
const validationFiredRef = useRef5(false);
|
|
3081
4024
|
useEffect5(() => {
|
|
3082
4025
|
if (validationFiredRef.current) return;
|
|
3083
4026
|
if (!showStripe && !showPayPal) {
|
|
@@ -3098,7 +4041,7 @@ function SplitCardFormInner({
|
|
|
3098
4041
|
updateError(err.message);
|
|
3099
4042
|
}
|
|
3100
4043
|
}, [showStripe, showPayPal, directPaypalConfigured, paypalStripeInstance, onError, updateError]);
|
|
3101
|
-
const deprecationLoggedRef =
|
|
4044
|
+
const deprecationLoggedRef = useRef5(false);
|
|
3102
4045
|
useEffect5(() => {
|
|
3103
4046
|
if (deprecationLoggedRef.current) return;
|
|
3104
4047
|
if (!hasEnabledMethods) return;
|
|
@@ -3111,14 +4054,14 @@ function SplitCardFormInner({
|
|
|
3111
4054
|
`[FloPay] ${stale.join(" / ")} ${stale.length === 1 ? "is" : "are"} deprecated: the Apple Pay / Google Pay surface is now driven by \`gateways.stripe.enabledPaymentMethods\` on the session response. Remove the legacy prop(s) to silence this warning.`
|
|
3112
4055
|
);
|
|
3113
4056
|
}, [hasEnabledMethods, showApplePay, showGooglePay]);
|
|
3114
|
-
const handleNameChange =
|
|
4057
|
+
const handleNameChange = useCallback3((value) => {
|
|
3115
4058
|
setFullName(value);
|
|
3116
4059
|
onFullNameChange?.(value);
|
|
3117
4060
|
const parts = value.trim().split(/\s+/);
|
|
3118
4061
|
onFirstNameChange?.(parts[0] ?? "");
|
|
3119
4062
|
onLastNameChange?.(parts.length > 1 ? parts.slice(1).join(" ") : "");
|
|
3120
4063
|
}, [onFullNameChange, onFirstNameChange, onLastNameChange]);
|
|
3121
|
-
const applyInlineSessionPatch =
|
|
4064
|
+
const applyInlineSessionPatch = useCallback3(
|
|
3122
4065
|
(patch, method) => {
|
|
3123
4066
|
if (!checkout.applyInlineSessionPatch) {
|
|
3124
4067
|
return Promise.resolve({ error: null, sessionId, nonce });
|
|
@@ -3140,7 +4083,7 @@ function SplitCardFormInner({
|
|
|
3140
4083
|
},
|
|
3141
4084
|
[checkout.applyInlineSessionPatch, nonce, onError, sessionId, updateError]
|
|
3142
4085
|
);
|
|
3143
|
-
const runBeforeButtonClick =
|
|
4086
|
+
const runBeforeButtonClick = useCallback3(async (method) => {
|
|
3144
4087
|
if (!onBeforeButtonClick) return { proceed: true };
|
|
3145
4088
|
try {
|
|
3146
4089
|
const result = await onBeforeButtonClick({
|
|
@@ -3177,7 +4120,7 @@ function SplitCardFormInner({
|
|
|
3177
4120
|
return { proceed: false };
|
|
3178
4121
|
}
|
|
3179
4122
|
}, [applyInlineSessionPatch, checkout.inlineSessionDraft, onBeforeButtonClick, onError, sessionId, updateError]);
|
|
3180
|
-
const processPaymentInternal =
|
|
4123
|
+
const processPaymentInternal = useCallback3(
|
|
3181
4124
|
async (tokenizedBody, overrides) => {
|
|
3182
4125
|
if (processingRef.current) return;
|
|
3183
4126
|
processingRef.current = true;
|
|
@@ -3388,7 +4331,7 @@ function SplitCardFormInner({
|
|
|
3388
4331
|
},
|
|
3389
4332
|
[baseUrl, sessionId, nonce, resolvedAccount, fullName, chv, flopay, paypalFlopay, onComplete, onError, updateError, emitDecline]
|
|
3390
4333
|
);
|
|
3391
|
-
const dispatchTokenizedBody =
|
|
4334
|
+
const dispatchTokenizedBody = useCallback3(
|
|
3392
4335
|
(tokenizedBody, overrides) => {
|
|
3393
4336
|
if (onTokenizedBody) {
|
|
3394
4337
|
onTokenizedBody(tokenizedBody);
|
|
@@ -3425,7 +4368,7 @@ function SplitCardFormInner({
|
|
|
3425
4368
|
}
|
|
3426
4369
|
}
|
|
3427
4370
|
}), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
|
|
3428
|
-
const stripeResumeAttemptedRef =
|
|
4371
|
+
const stripeResumeAttemptedRef = useRef5(false);
|
|
3429
4372
|
useEffect5(() => {
|
|
3430
4373
|
if (typeof window === "undefined" || stripeResumeAttemptedRef.current) return;
|
|
3431
4374
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -3504,7 +4447,7 @@ function SplitCardFormInner({
|
|
|
3504
4447
|
}, persistedOverrides);
|
|
3505
4448
|
})();
|
|
3506
4449
|
}, [sessionId, dispatchTokenizedBody, flopay, updateError, emitDecline]);
|
|
3507
|
-
const handleSubmit =
|
|
4450
|
+
const handleSubmit = useCallback3(
|
|
3508
4451
|
async (e) => {
|
|
3509
4452
|
e.preventDefault();
|
|
3510
4453
|
if (!flopay || !elements || isSubmitting || processingRef.current) return;
|
|
@@ -4312,6 +5255,7 @@ function SplitCardFormInner({
|
|
|
4312
5255
|
onComplete,
|
|
4313
5256
|
onErrorChange: updateError,
|
|
4314
5257
|
onDecline,
|
|
5258
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4315
5259
|
onButtonClick,
|
|
4316
5260
|
runBeforeButtonClick,
|
|
4317
5261
|
isProcessing: isSubmitting,
|
|
@@ -4335,6 +5279,7 @@ function SplitCardFormInner({
|
|
|
4335
5279
|
isProcessing: isSubmitting,
|
|
4336
5280
|
onButtonClick,
|
|
4337
5281
|
onDecline,
|
|
5282
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4338
5283
|
runBeforeButtonClick,
|
|
4339
5284
|
onLoadStateChange: setPaypalLoadState,
|
|
4340
5285
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -4352,6 +5297,7 @@ function SplitCardFormInner({
|
|
|
4352
5297
|
onErrorChange: updateError,
|
|
4353
5298
|
onButtonClick,
|
|
4354
5299
|
onDecline,
|
|
5300
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4355
5301
|
runBeforeButtonClick,
|
|
4356
5302
|
onLoadStateChange: setWalletLoadState,
|
|
4357
5303
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -4707,6 +5653,7 @@ function SplitCardFormInner({
|
|
|
4707
5653
|
onComplete,
|
|
4708
5654
|
onErrorChange: updateError,
|
|
4709
5655
|
onDecline,
|
|
5656
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4710
5657
|
onButtonClick,
|
|
4711
5658
|
runBeforeButtonClick,
|
|
4712
5659
|
isProcessing: isSubmitting,
|
|
@@ -4730,6 +5677,7 @@ function SplitCardFormInner({
|
|
|
4730
5677
|
isProcessing: isSubmitting,
|
|
4731
5678
|
onButtonClick,
|
|
4732
5679
|
onDecline,
|
|
5680
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4733
5681
|
runBeforeButtonClick,
|
|
4734
5682
|
onLoadStateChange: setPaypalLoadState,
|
|
4735
5683
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -4747,6 +5695,7 @@ function SplitCardFormInner({
|
|
|
4747
5695
|
onErrorChange: updateError,
|
|
4748
5696
|
onButtonClick,
|
|
4749
5697
|
onDecline,
|
|
5698
|
+
onTechnicalFailure: recoverExternalMethodTechnicalFailure,
|
|
4750
5699
|
runBeforeButtonClick,
|
|
4751
5700
|
onLoadStateChange: setWalletLoadState,
|
|
4752
5701
|
placeholderBorderRadius: buttonBorderRadius
|
|
@@ -5140,7 +6089,7 @@ async function recover3DSRedirectResult({
|
|
|
5140
6089
|
return null;
|
|
5141
6090
|
}
|
|
5142
6091
|
try {
|
|
5143
|
-
const api = new PaymentAPI3(billingApiUrl);
|
|
6092
|
+
const api = new PaymentAPI3(billingApiUrl, { telemetry: false });
|
|
5144
6093
|
const unified = await api.getUnifiedCheckoutSession(sessionId, nonce);
|
|
5145
6094
|
const refreshedToken = unified.data.stripe?.clientSecret;
|
|
5146
6095
|
if (isStripePaymentIntentClientSecret(refreshedToken)) {
|
|
@@ -5159,7 +6108,8 @@ async function processSavedPaymentForMode({
|
|
|
5159
6108
|
session,
|
|
5160
6109
|
nonce,
|
|
5161
6110
|
tokenizedData,
|
|
5162
|
-
returnUrl
|
|
6111
|
+
returnUrl,
|
|
6112
|
+
telemetry
|
|
5163
6113
|
}) {
|
|
5164
6114
|
const baseUrl = billingApiUrl.replace(/\/+$/, "");
|
|
5165
6115
|
const resolvedSessionId = sessionId ?? session.id;
|
|
@@ -5170,7 +6120,10 @@ async function processSavedPaymentForMode({
|
|
|
5170
6120
|
const lastName = session.customer?.lastName ?? session.accountData?.lastName ?? "";
|
|
5171
6121
|
const country = session.customer?.country ?? session.accountData?.country ?? void 0;
|
|
5172
6122
|
const zip = session.customer?.zip ?? session.accountData?.zip ?? void 0;
|
|
5173
|
-
const api = new PaymentAPI3(
|
|
6123
|
+
const api = new PaymentAPI3(
|
|
6124
|
+
baseUrl,
|
|
6125
|
+
telemetry === false ? { telemetry: false } : void 0
|
|
6126
|
+
);
|
|
5174
6127
|
const response = await retryOnceOnFetchFailure(() => api.processPayment(customerId, {
|
|
5175
6128
|
sessionId: resolvedSessionId,
|
|
5176
6129
|
nonce: resolvedNonce,
|
|
@@ -5247,7 +6200,8 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
|
|
|
5247
6200
|
billingApiUrl,
|
|
5248
6201
|
sessionId,
|
|
5249
6202
|
session,
|
|
5250
|
-
returnUrl
|
|
6203
|
+
returnUrl,
|
|
6204
|
+
telemetry
|
|
5251
6205
|
}) {
|
|
5252
6206
|
const stripe = flopay?.getRawProvider();
|
|
5253
6207
|
if (!stripe) {
|
|
@@ -5330,6 +6284,7 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
|
|
|
5330
6284
|
billingApiUrl,
|
|
5331
6285
|
sessionId,
|
|
5332
6286
|
session,
|
|
6287
|
+
telemetry,
|
|
5333
6288
|
tokenizedData: {
|
|
5334
6289
|
id: paymentIntent.id,
|
|
5335
6290
|
type: "card",
|
|
@@ -5351,7 +6306,8 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
|
|
|
5351
6306
|
billingApiUrl,
|
|
5352
6307
|
sessionId,
|
|
5353
6308
|
session,
|
|
5354
|
-
returnUrl
|
|
6309
|
+
returnUrl,
|
|
6310
|
+
telemetry
|
|
5355
6311
|
});
|
|
5356
6312
|
}
|
|
5357
6313
|
throw Object.assign(
|
|
@@ -5457,7 +6413,8 @@ async function loadSavedPaymentProviders({
|
|
|
5457
6413
|
publishableKey,
|
|
5458
6414
|
paypalPublishableKey,
|
|
5459
6415
|
billingApiUrl,
|
|
5460
|
-
locale
|
|
6416
|
+
locale,
|
|
6417
|
+
telemetry
|
|
5461
6418
|
}) {
|
|
5462
6419
|
if (!publishableKey) {
|
|
5463
6420
|
return { flopay: null, paypalFlopay: null };
|
|
@@ -5466,11 +6423,13 @@ async function loadSavedPaymentProviders({
|
|
|
5466
6423
|
const [instance, paypalInstanceOrError] = await Promise.all([
|
|
5467
6424
|
loadFloPay(publishableKey, {
|
|
5468
6425
|
billingApiUrl,
|
|
5469
|
-
locale
|
|
6426
|
+
locale,
|
|
6427
|
+
telemetry
|
|
5470
6428
|
}),
|
|
5471
6429
|
needsSeparatePaypal ? loadFloPay(paypalPublishableKey, {
|
|
5472
6430
|
billingApiUrl,
|
|
5473
|
-
locale
|
|
6431
|
+
locale,
|
|
6432
|
+
telemetry
|
|
5474
6433
|
}).catch((err) => {
|
|
5475
6434
|
console.warn("[FloPay] Failed to load PayPal Stripe instance:", err);
|
|
5476
6435
|
return null;
|
|
@@ -5490,6 +6449,33 @@ var sessionInflightMap = /* @__PURE__ */ new Map();
|
|
|
5490
6449
|
function sleep(ms) {
|
|
5491
6450
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
5492
6451
|
}
|
|
6452
|
+
function createStandaloneTelemetryReporter(billingApiUrl, enabled, context) {
|
|
6453
|
+
const reporter = createTelemetryBridge({
|
|
6454
|
+
billingApiUrl,
|
|
6455
|
+
sdkPackage: "@flopay/react",
|
|
6456
|
+
sdkVersion: SDK_VERSION2,
|
|
6457
|
+
enabled: enabled !== false
|
|
6458
|
+
});
|
|
6459
|
+
reporter.beginCheckout(context);
|
|
6460
|
+
return reporter;
|
|
6461
|
+
}
|
|
6462
|
+
function finishStandaloneTelemetry(reporter) {
|
|
6463
|
+
void reporter.flush().catch(() => {
|
|
6464
|
+
}).finally(() => reporter.destroy());
|
|
6465
|
+
}
|
|
6466
|
+
function reportStandaloneTelemetryError(billingApiUrl, enabled, context, errorCode, stage) {
|
|
6467
|
+
const reporter = createStandaloneTelemetryReporter(billingApiUrl, enabled, context);
|
|
6468
|
+
reporter.error({
|
|
6469
|
+
errorCode,
|
|
6470
|
+
stage,
|
|
6471
|
+
paymentMethodCategory: "unknown",
|
|
6472
|
+
...stage === "session_read" ? { requestCategory: "session_read" } : {}
|
|
6473
|
+
});
|
|
6474
|
+
finishStandaloneTelemetry(reporter);
|
|
6475
|
+
}
|
|
6476
|
+
function isExpectedExistingSessionError(error) {
|
|
6477
|
+
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";
|
|
6478
|
+
}
|
|
5493
6479
|
function resolveDirectPaypalConfig(unified) {
|
|
5494
6480
|
const clientId = unified?.data.paypal?.publishableKey;
|
|
5495
6481
|
if (!clientId) return void 0;
|
|
@@ -5594,6 +6580,7 @@ function FloPayCheckout({
|
|
|
5594
6580
|
nonce: nonceProp,
|
|
5595
6581
|
createSession: createSessionParams,
|
|
5596
6582
|
billingApiUrl,
|
|
6583
|
+
telemetry,
|
|
5597
6584
|
appearance: appearanceOverride,
|
|
5598
6585
|
locale,
|
|
5599
6586
|
loading: loadingNode,
|
|
@@ -5639,9 +6626,9 @@ function FloPayCheckout({
|
|
|
5639
6626
|
const checkoutLayout = children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout";
|
|
5640
6627
|
const [unified, setUnified] = useState4(null);
|
|
5641
6628
|
const [flopay, setFloPay] = useState4(null);
|
|
5642
|
-
const flopayRef =
|
|
6629
|
+
const flopayRef = useRef6(null);
|
|
5643
6630
|
const [paypalFlopay, setPaypalFloPay] = useState4(null);
|
|
5644
|
-
const paypalFlopayRef =
|
|
6631
|
+
const paypalFlopayRef = useRef6(null);
|
|
5645
6632
|
const [session, setSession] = useState4(null);
|
|
5646
6633
|
const [resolvedSessionId, setResolvedSessionId] = useState4(sessionIdProp ?? "");
|
|
5647
6634
|
const activeSessionId = sessionIdProp ?? resolvedSessionId;
|
|
@@ -5656,20 +6643,30 @@ function FloPayCheckout({
|
|
|
5656
6643
|
const [createSessionPatch, setCreateSessionPatch] = useState4(void 0);
|
|
5657
6644
|
const [createSessionPatchBaseHash, setCreateSessionPatchBaseHash] = useState4("");
|
|
5658
6645
|
const [cardBootstrapPending, setCardBootstrapPending] = useState4(false);
|
|
5659
|
-
const autoCheckoutAttempted =
|
|
5660
|
-
const paypalResumeAttempted =
|
|
5661
|
-
const savedPaymentKeysRef =
|
|
5662
|
-
const
|
|
6646
|
+
const autoCheckoutAttempted = useRef6(false);
|
|
6647
|
+
const paypalResumeAttempted = useRef6(false);
|
|
6648
|
+
const savedPaymentKeysRef = useRef6(null);
|
|
6649
|
+
const telemetryCheckoutContext = useMemo4(() => ({
|
|
6650
|
+
checkoutMode: checkoutModeProp ?? currentMode,
|
|
6651
|
+
layout: children ? "unknown" : layout === "buttons" ? "buttons" : "embedded"
|
|
6652
|
+
}), [checkoutModeProp, children, currentMode, layout]);
|
|
6653
|
+
const onCompleteRef = useRef6(onComplete);
|
|
5663
6654
|
onCompleteRef.current = onComplete;
|
|
5664
|
-
const onErrorRef =
|
|
6655
|
+
const onErrorRef = useRef6(onError);
|
|
5665
6656
|
onErrorRef.current = onError;
|
|
5666
|
-
const onDeclineRef =
|
|
6657
|
+
const onDeclineRef = useRef6(onDecline);
|
|
5667
6658
|
onDeclineRef.current = onDecline;
|
|
5668
|
-
|
|
6659
|
+
useEffect6(() => {
|
|
6660
|
+
getFloPayTelemetryBridge(flopay)?.setCheckoutContext(telemetryCheckoutContext);
|
|
6661
|
+
if (paypalFlopay && paypalFlopay !== flopay) {
|
|
6662
|
+
getFloPayTelemetryBridge(paypalFlopay)?.setCheckoutContext(telemetryCheckoutContext);
|
|
6663
|
+
}
|
|
6664
|
+
}, [flopay, paypalFlopay, telemetryCheckoutContext]);
|
|
6665
|
+
const onSessionCompletedRef = useRef6(onSessionCompleted);
|
|
5669
6666
|
onSessionCompletedRef.current = onSessionCompleted;
|
|
5670
6667
|
useEffect6(() => {
|
|
5671
6668
|
console.info("[FloPay] Checkout initialized", {
|
|
5672
|
-
sdk_version:
|
|
6669
|
+
sdk_version: SDK_VERSION2,
|
|
5673
6670
|
checkout_type: checkoutType,
|
|
5674
6671
|
checkout_layout: checkoutLayout,
|
|
5675
6672
|
billing_api_url: resolvedBillingUrl
|
|
@@ -5702,22 +6699,112 @@ function FloPayCheckout({
|
|
|
5702
6699
|
useEffect6(() => {
|
|
5703
6700
|
setModeError(initialErrorMessage);
|
|
5704
6701
|
}, [initialErrorMessage]);
|
|
5705
|
-
const emitDecline =
|
|
6702
|
+
const emitDecline = useCallback4(
|
|
5706
6703
|
(method, input, overrides) => {
|
|
5707
|
-
|
|
6704
|
+
invokeMerchantCallback(() => {
|
|
6705
|
+
onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
|
|
6706
|
+
});
|
|
5708
6707
|
},
|
|
5709
|
-
[]
|
|
6708
|
+
[invokeMerchantCallback]
|
|
5710
6709
|
);
|
|
5711
|
-
const runSavedPaymentFlow =
|
|
6710
|
+
const runSavedPaymentFlow = useCallback4(
|
|
5712
6711
|
async (sess, options) => {
|
|
5713
6712
|
setModeError(null);
|
|
5714
6713
|
setModeOverlayError(null);
|
|
5715
6714
|
setModeOverlayStatus("processing");
|
|
5716
6715
|
const activeSessionId2 = options?.sessionId ?? sess.id;
|
|
6716
|
+
const activeFloPay = flopayRef.current ?? paypalFlopayRef.current;
|
|
6717
|
+
const activeTelemetry = getFloPayTelemetryBridge(activeFloPay);
|
|
6718
|
+
const standaloneTelemetry = activeFloPay ? null : createStandaloneTelemetryReporter(
|
|
6719
|
+
resolvedBillingUrl,
|
|
6720
|
+
telemetry,
|
|
6721
|
+
telemetryCheckoutContext
|
|
6722
|
+
);
|
|
6723
|
+
const telemetrySource = {
|
|
6724
|
+
log: (input) => {
|
|
6725
|
+
if (activeTelemetry) activeTelemetry.log(input);
|
|
6726
|
+
else standaloneTelemetry?.log(input);
|
|
6727
|
+
},
|
|
6728
|
+
error: (input) => {
|
|
6729
|
+
if (activeTelemetry) activeTelemetry.error(input);
|
|
6730
|
+
else standaloneTelemetry?.error(input);
|
|
6731
|
+
},
|
|
6732
|
+
terminal: (input) => {
|
|
6733
|
+
if (activeTelemetry) activeTelemetry.terminal(input);
|
|
6734
|
+
else standaloneTelemetry?.terminal(input);
|
|
6735
|
+
},
|
|
6736
|
+
performance: (input) => {
|
|
6737
|
+
if (activeTelemetry) activeTelemetry.performance(input);
|
|
6738
|
+
else standaloneTelemetry?.performance(input);
|
|
6739
|
+
},
|
|
6740
|
+
now: () => activeTelemetry?.now() ?? standaloneTelemetry?.now() ?? 0,
|
|
6741
|
+
elapsed: (startedAt) => activeTelemetry?.elapsed(startedAt) ?? Math.max(0, (standaloneTelemetry?.now() ?? startedAt) - startedAt)
|
|
6742
|
+
};
|
|
6743
|
+
const processingStartedAt = telemetrySource.now();
|
|
6744
|
+
let recoveryFlow = Boolean(
|
|
6745
|
+
options?.initialAutoProcessingError || options?.initialAutoProcessingPending
|
|
6746
|
+
);
|
|
6747
|
+
let recoveryStarted = false;
|
|
6748
|
+
let recoveryStartedAt;
|
|
6749
|
+
const startRecovery = () => {
|
|
6750
|
+
if (recoveryStarted) return;
|
|
6751
|
+
recoveryStarted = true;
|
|
6752
|
+
recoveryFlow = true;
|
|
6753
|
+
recoveryStartedAt = telemetrySource.now();
|
|
6754
|
+
telemetrySource.log({
|
|
6755
|
+
name: "checkout.recovery.started",
|
|
6756
|
+
stage: "recovery",
|
|
6757
|
+
paymentMethodCategory: "saved"
|
|
6758
|
+
});
|
|
6759
|
+
telemetrySource.log({
|
|
6760
|
+
name: "operation.recovery.started",
|
|
6761
|
+
stage: "recovery",
|
|
6762
|
+
paymentMethodCategory: "saved"
|
|
6763
|
+
});
|
|
6764
|
+
};
|
|
6765
|
+
let processingFinished = false;
|
|
6766
|
+
const finishProcessing = () => {
|
|
6767
|
+
if (processingFinished) return;
|
|
6768
|
+
processingFinished = true;
|
|
6769
|
+
telemetrySource.log({
|
|
6770
|
+
name: "payment.processing.completed",
|
|
6771
|
+
stage: "processing",
|
|
6772
|
+
paymentMethodCategory: "saved"
|
|
6773
|
+
});
|
|
6774
|
+
telemetrySource.performance({
|
|
6775
|
+
stage: "processing",
|
|
6776
|
+
durationMs: telemetrySource.elapsed(processingStartedAt),
|
|
6777
|
+
durationMode: "machine",
|
|
6778
|
+
paymentMethodCategory: "saved"
|
|
6779
|
+
});
|
|
6780
|
+
if (recoveryStartedAt !== void 0) {
|
|
6781
|
+
telemetrySource.performance({
|
|
6782
|
+
stage: "recovery",
|
|
6783
|
+
durationMs: telemetrySource.elapsed(recoveryStartedAt),
|
|
6784
|
+
durationMode: "machine",
|
|
6785
|
+
paymentMethodCategory: "saved"
|
|
6786
|
+
});
|
|
6787
|
+
}
|
|
6788
|
+
};
|
|
6789
|
+
telemetrySource.log({
|
|
6790
|
+
name: "payment.method.selected",
|
|
6791
|
+
stage: "processing",
|
|
6792
|
+
paymentMethodCategory: "saved"
|
|
6793
|
+
});
|
|
6794
|
+
telemetrySource.log({
|
|
6795
|
+
name: "payment.processing.started",
|
|
6796
|
+
stage: "processing",
|
|
6797
|
+
paymentMethodCategory: "saved"
|
|
6798
|
+
});
|
|
6799
|
+
if (recoveryFlow) {
|
|
6800
|
+
startRecovery();
|
|
6801
|
+
}
|
|
6802
|
+
let completionCallback;
|
|
5717
6803
|
try {
|
|
5718
6804
|
const redirectResult = getRedirectResultFromCheckoutProcessError(options?.initialAutoProcessingError);
|
|
5719
6805
|
let paymentResult;
|
|
5720
6806
|
if (redirectResult) {
|
|
6807
|
+
startRecovery();
|
|
5721
6808
|
if (redirectResult.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
|
|
5722
6809
|
persistPayPalResumeState({
|
|
5723
6810
|
sessionId: activeSessionId2,
|
|
@@ -5731,13 +6818,14 @@ function FloPayCheckout({
|
|
|
5731
6818
|
attempt3DS: options?.attempt3DS,
|
|
5732
6819
|
billingApiUrl: resolvedBillingUrl,
|
|
5733
6820
|
sessionId: activeSessionId2,
|
|
5734
|
-
session: sess
|
|
6821
|
+
session: sess,
|
|
6822
|
+
telemetry: false
|
|
5735
6823
|
});
|
|
5736
6824
|
if (redirectResult.type === "paypal_redirect_required") {
|
|
5737
6825
|
clearPayPalResumeState();
|
|
5738
6826
|
}
|
|
5739
6827
|
} else if (options?.initialAutoProcessingPending) {
|
|
5740
|
-
const api = new PaymentAPI4(resolvedBillingUrl);
|
|
6828
|
+
const api = new PaymentAPI4(resolvedBillingUrl, { telemetry: false });
|
|
5741
6829
|
const completed = await api.waitForCheckoutSessionCompletion(options.initialAutoProcessingPending.sessionId, {
|
|
5742
6830
|
initialDelayMs: options.initialAutoProcessingPending.retryAfterMs
|
|
5743
6831
|
});
|
|
@@ -5765,11 +6853,13 @@ function FloPayCheckout({
|
|
|
5765
6853
|
const result = await processSavedPaymentForMode({
|
|
5766
6854
|
billingApiUrl: resolvedBillingUrl,
|
|
5767
6855
|
sessionId: activeSessionId2,
|
|
5768
|
-
session: sess
|
|
6856
|
+
session: sess,
|
|
6857
|
+
telemetry: false
|
|
5769
6858
|
});
|
|
5770
6859
|
if (result.type === "success") {
|
|
5771
6860
|
paymentResult = result.result;
|
|
5772
6861
|
} else {
|
|
6862
|
+
startRecovery();
|
|
5773
6863
|
if (result.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
|
|
5774
6864
|
persistPayPalResumeState({
|
|
5775
6865
|
sessionId: activeSessionId2,
|
|
@@ -5783,7 +6873,8 @@ function FloPayCheckout({
|
|
|
5783
6873
|
attempt3DS: options?.attempt3DS,
|
|
5784
6874
|
billingApiUrl: resolvedBillingUrl,
|
|
5785
6875
|
sessionId: activeSessionId2,
|
|
5786
|
-
session: sess
|
|
6876
|
+
session: sess,
|
|
6877
|
+
telemetry: false
|
|
5787
6878
|
});
|
|
5788
6879
|
if (result.type === "paypal_redirect_required") {
|
|
5789
6880
|
clearPayPalResumeState();
|
|
@@ -5791,20 +6882,57 @@ function FloPayCheckout({
|
|
|
5791
6882
|
}
|
|
5792
6883
|
}
|
|
5793
6884
|
if (activeSessionId2) markSessionRecentlyCompleted(activeSessionId2);
|
|
6885
|
+
finishProcessing();
|
|
6886
|
+
if (recoveryFlow) {
|
|
6887
|
+
telemetrySource.log({
|
|
6888
|
+
name: "checkout.recovery.completed",
|
|
6889
|
+
stage: "recovery",
|
|
6890
|
+
paymentMethodCategory: "saved"
|
|
6891
|
+
});
|
|
6892
|
+
telemetrySource.log({
|
|
6893
|
+
name: "operation.recovery.completed",
|
|
6894
|
+
stage: "recovery",
|
|
6895
|
+
paymentMethodCategory: "saved"
|
|
6896
|
+
});
|
|
6897
|
+
}
|
|
6898
|
+
telemetrySource.terminal({
|
|
6899
|
+
outcome: "payment_succeeded",
|
|
6900
|
+
paymentMethodCategory: "saved"
|
|
6901
|
+
});
|
|
5794
6902
|
setModeOverlayStatus("success");
|
|
5795
6903
|
await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
|
|
5796
|
-
onCompleteRef.current?.(paymentResult);
|
|
5797
|
-
return true;
|
|
6904
|
+
completionCallback = () => onCompleteRef.current?.(paymentResult);
|
|
5798
6905
|
} catch (err) {
|
|
5799
6906
|
const floPayErr = normalizeSavedPaymentError(err);
|
|
5800
6907
|
const method = floPayErr.checkoutMethod ?? DEFAULT_SAVED_PAYMENT_DECLINE_METHOD;
|
|
5801
6908
|
setModeError(floPayErr.message);
|
|
5802
6909
|
setModeOverlayError(floPayErr.message);
|
|
5803
6910
|
if (options?.fallbackToFull) {
|
|
6911
|
+
telemetrySource.log({
|
|
6912
|
+
name: "operation.fallback",
|
|
6913
|
+
stage: "recovery",
|
|
6914
|
+
paymentMethodCategory: "saved"
|
|
6915
|
+
});
|
|
5804
6916
|
setCurrentMode("full");
|
|
5805
6917
|
replaceCheckoutModeQueryParam("full");
|
|
5806
6918
|
}
|
|
5807
|
-
|
|
6919
|
+
finishProcessing();
|
|
6920
|
+
const expectedDecline = Boolean(
|
|
6921
|
+
floPayErr.declineCode || floPayErr.code?.toLowerCase().includes("declin")
|
|
6922
|
+
);
|
|
6923
|
+
if (expectedDecline) {
|
|
6924
|
+
telemetrySource.terminal({
|
|
6925
|
+
outcome: "payment_declined",
|
|
6926
|
+
paymentMethodCategory: "saved"
|
|
6927
|
+
});
|
|
6928
|
+
} else {
|
|
6929
|
+
telemetrySource.error({
|
|
6930
|
+
errorCode: recoveryFlow ? "RECOVERY_FAILED" : "PAYMENT_PROCESSING_FAILED",
|
|
6931
|
+
stage: recoveryFlow ? "recovery" : "processing",
|
|
6932
|
+
paymentMethodCategory: "saved"
|
|
6933
|
+
});
|
|
6934
|
+
}
|
|
6935
|
+
invokeMerchantCallback(() => onErrorRef.current?.(floPayErr));
|
|
5808
6936
|
emitDecline(method, floPayErr, {
|
|
5809
6937
|
code: floPayErr.code,
|
|
5810
6938
|
declineCode: floPayErr.declineCode
|
|
@@ -5814,16 +6942,35 @@ function FloPayCheckout({
|
|
|
5814
6942
|
sleep(PROCESSING_OVERLAY_ERROR_DELAY_MS),
|
|
5815
6943
|
options?.ensureProvidersReady ? options.ensureProvidersReady() : Promise.resolve()
|
|
5816
6944
|
]);
|
|
6945
|
+
if (recoveryFlow) {
|
|
6946
|
+
telemetrySource.log({
|
|
6947
|
+
name: "checkout.recovery.completed",
|
|
6948
|
+
stage: "recovery",
|
|
6949
|
+
paymentMethodCategory: "saved"
|
|
6950
|
+
});
|
|
6951
|
+
telemetrySource.log({
|
|
6952
|
+
name: "operation.recovery.completed",
|
|
6953
|
+
stage: "recovery",
|
|
6954
|
+
paymentMethodCategory: "saved"
|
|
6955
|
+
});
|
|
6956
|
+
}
|
|
5817
6957
|
return false;
|
|
5818
6958
|
} finally {
|
|
6959
|
+
finishProcessing();
|
|
6960
|
+
if (standaloneTelemetry) finishStandaloneTelemetry(standaloneTelemetry);
|
|
5819
6961
|
setModeOverlayStatus(null);
|
|
5820
6962
|
setModeOverlayError(null);
|
|
5821
6963
|
}
|
|
6964
|
+
invokeMerchantCallback(completionCallback);
|
|
6965
|
+
return true;
|
|
5822
6966
|
},
|
|
5823
6967
|
[
|
|
5824
6968
|
emitDecline,
|
|
6969
|
+
invokeMerchantCallback,
|
|
5825
6970
|
normalizeSavedPaymentError,
|
|
5826
|
-
resolvedBillingUrl
|
|
6971
|
+
resolvedBillingUrl,
|
|
6972
|
+
telemetry,
|
|
6973
|
+
telemetryCheckoutContext
|
|
5827
6974
|
]
|
|
5828
6975
|
);
|
|
5829
6976
|
useEffect6(() => {
|
|
@@ -5841,6 +6988,8 @@ function FloPayCheckout({
|
|
|
5841
6988
|
}
|
|
5842
6989
|
paypalResumeAttempted.current = true;
|
|
5843
6990
|
void (async () => {
|
|
6991
|
+
let resumeTelemetry;
|
|
6992
|
+
let redirectResumeStartedAt = 0;
|
|
5844
6993
|
setModeError(null);
|
|
5845
6994
|
setModeOverlayError(null);
|
|
5846
6995
|
setModeOverlayStatus("processing");
|
|
@@ -5848,7 +6997,11 @@ function FloPayCheckout({
|
|
|
5848
6997
|
try {
|
|
5849
6998
|
if (params.get("redirect_status") === "failed") {
|
|
5850
6999
|
throw Object.assign(
|
|
5851
|
-
new FloPayError5(
|
|
7000
|
+
new FloPayError5(
|
|
7001
|
+
"PayPal payment was declined. Please try again.",
|
|
7002
|
+
"api_error",
|
|
7003
|
+
{ declineCode: "paypal_redirect_failed" }
|
|
7004
|
+
),
|
|
5852
7005
|
{ checkoutMethod: "paypal" }
|
|
5853
7006
|
);
|
|
5854
7007
|
}
|
|
@@ -5859,7 +7012,22 @@ function FloPayCheckout({
|
|
|
5859
7012
|
publishableKey: resumeState.publishableKey,
|
|
5860
7013
|
paypalPublishableKey: resumeState.paypalPublishableKey,
|
|
5861
7014
|
billingApiUrl: resolvedBillingUrl,
|
|
5862
|
-
locale
|
|
7015
|
+
locale,
|
|
7016
|
+
telemetry
|
|
7017
|
+
});
|
|
7018
|
+
resumeTelemetry = getFloPayTelemetryBridge(resumePaypalFlopay ?? resumeFlopay);
|
|
7019
|
+
redirectResumeStartedAt = resumeTelemetry?.now() ?? 0;
|
|
7020
|
+
resumeTelemetry?.log({
|
|
7021
|
+
name: "provider.redirect.resumed",
|
|
7022
|
+
stage: "redirect_resume",
|
|
7023
|
+
provider: "paypal",
|
|
7024
|
+
paymentMethodCategory: "paypal"
|
|
7025
|
+
});
|
|
7026
|
+
resumeTelemetry?.log({
|
|
7027
|
+
name: "operation.recovery.started",
|
|
7028
|
+
stage: "recovery",
|
|
7029
|
+
provider: "paypal",
|
|
7030
|
+
paymentMethodCategory: "paypal"
|
|
5863
7031
|
});
|
|
5864
7032
|
const paypalStripe = (resumePaypalFlopay ?? resumeFlopay)?.getRawProvider();
|
|
5865
7033
|
if (!paypalStripe) {
|
|
@@ -5889,7 +7057,7 @@ function FloPayCheckout({
|
|
|
5889
7057
|
const paymentMethodId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
|
|
5890
7058
|
let finalResultStatus = resultStatus;
|
|
5891
7059
|
if (resumeState.sessionId) {
|
|
5892
|
-
const resumeApi = new PaymentAPI4(resolvedBillingUrl);
|
|
7060
|
+
const resumeApi = new PaymentAPI4(resolvedBillingUrl, { telemetry: false });
|
|
5893
7061
|
const resumeSessionResult = await resumeApi.getUnifiedCheckoutSession(resumeState.sessionId);
|
|
5894
7062
|
const resumeSession = resumeSessionResult.data.session;
|
|
5895
7063
|
if (resumeSession && resumeSession.status !== "complete") {
|
|
@@ -5897,6 +7065,7 @@ function FloPayCheckout({
|
|
|
5897
7065
|
billingApiUrl: resolvedBillingUrl,
|
|
5898
7066
|
sessionId: resumeState.sessionId,
|
|
5899
7067
|
session: resumeSession,
|
|
7068
|
+
telemetry: false,
|
|
5900
7069
|
tokenizedData: {
|
|
5901
7070
|
id: paymentMethodId ?? paymentIntent.id,
|
|
5902
7071
|
type: "card",
|
|
@@ -5914,20 +7083,84 @@ function FloPayCheckout({
|
|
|
5914
7083
|
}
|
|
5915
7084
|
}
|
|
5916
7085
|
if (resumeState.sessionId) markSessionRecentlyCompleted(resumeState.sessionId);
|
|
7086
|
+
resumeTelemetry?.log({
|
|
7087
|
+
name: "operation.recovery.completed",
|
|
7088
|
+
stage: "recovery",
|
|
7089
|
+
provider: "paypal",
|
|
7090
|
+
paymentMethodCategory: "paypal"
|
|
7091
|
+
});
|
|
7092
|
+
resumeTelemetry?.performance({
|
|
7093
|
+
stage: "redirect_resume",
|
|
7094
|
+
durationMs: resumeTelemetry.elapsed(redirectResumeStartedAt),
|
|
7095
|
+
durationMode: "machine",
|
|
7096
|
+
provider: "paypal",
|
|
7097
|
+
paymentMethodCategory: "paypal"
|
|
7098
|
+
});
|
|
7099
|
+
resumeTelemetry?.terminal({
|
|
7100
|
+
outcome: "payment_succeeded",
|
|
7101
|
+
provider: "paypal",
|
|
7102
|
+
paymentMethodCategory: "paypal"
|
|
7103
|
+
});
|
|
5917
7104
|
setModeOverlayStatus("success");
|
|
5918
7105
|
await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
|
|
5919
|
-
onCompleteRef.current?.({
|
|
7106
|
+
invokeMerchantCallback(() => onCompleteRef.current?.({
|
|
5920
7107
|
status: finalResultStatus,
|
|
5921
7108
|
paymentIntentId: paymentIntent.id,
|
|
5922
7109
|
paymentMethodId,
|
|
5923
7110
|
checkoutMethod: "paypal"
|
|
5924
|
-
});
|
|
7111
|
+
}));
|
|
5925
7112
|
} catch (err) {
|
|
5926
7113
|
const floPayErr = normalizeSavedPaymentError(err);
|
|
5927
7114
|
const method = floPayErr.checkoutMethod ?? "paypal";
|
|
7115
|
+
const expectedDecline = Boolean(
|
|
7116
|
+
floPayErr.declineCode || floPayErr.code?.toLowerCase().includes("declin")
|
|
7117
|
+
);
|
|
7118
|
+
if (resumeTelemetry) {
|
|
7119
|
+
resumeTelemetry.performance({
|
|
7120
|
+
stage: "redirect_resume",
|
|
7121
|
+
durationMs: resumeTelemetry.elapsed(redirectResumeStartedAt),
|
|
7122
|
+
durationMode: "machine",
|
|
7123
|
+
provider: "paypal",
|
|
7124
|
+
paymentMethodCategory: "paypal"
|
|
7125
|
+
});
|
|
7126
|
+
if (expectedDecline) {
|
|
7127
|
+
resumeTelemetry.terminal({
|
|
7128
|
+
outcome: "payment_declined",
|
|
7129
|
+
provider: "paypal",
|
|
7130
|
+
paymentMethodCategory: "paypal"
|
|
7131
|
+
});
|
|
7132
|
+
} else {
|
|
7133
|
+
resumeTelemetry.error({
|
|
7134
|
+
errorCode: "REDIRECT_RESUME_FAILED",
|
|
7135
|
+
stage: "redirect_resume",
|
|
7136
|
+
provider: "paypal",
|
|
7137
|
+
paymentMethodCategory: "paypal"
|
|
7138
|
+
});
|
|
7139
|
+
}
|
|
7140
|
+
} else if (expectedDecline) {
|
|
7141
|
+
const reporter = createStandaloneTelemetryReporter(
|
|
7142
|
+
resolvedBillingUrl,
|
|
7143
|
+
telemetry,
|
|
7144
|
+
telemetryCheckoutContext
|
|
7145
|
+
);
|
|
7146
|
+
reporter.terminal({
|
|
7147
|
+
outcome: "payment_declined",
|
|
7148
|
+
provider: "paypal",
|
|
7149
|
+
paymentMethodCategory: "paypal"
|
|
7150
|
+
});
|
|
7151
|
+
finishStandaloneTelemetry(reporter);
|
|
7152
|
+
} else {
|
|
7153
|
+
reportStandaloneTelemetryError(
|
|
7154
|
+
resolvedBillingUrl,
|
|
7155
|
+
telemetry,
|
|
7156
|
+
telemetryCheckoutContext,
|
|
7157
|
+
"REDIRECT_RESUME_FAILED",
|
|
7158
|
+
"redirect_resume"
|
|
7159
|
+
);
|
|
7160
|
+
}
|
|
5928
7161
|
setModeError(floPayErr.message);
|
|
5929
7162
|
setModeOverlayError(floPayErr.message);
|
|
5930
|
-
onErrorRef.current?.(floPayErr);
|
|
7163
|
+
invokeMerchantCallback(() => onErrorRef.current?.(floPayErr));
|
|
5931
7164
|
emitDecline(method, floPayErr, {
|
|
5932
7165
|
code: floPayErr.code,
|
|
5933
7166
|
declineCode: floPayErr.declineCode
|
|
@@ -5942,8 +7175,8 @@ function FloPayCheckout({
|
|
|
5942
7175
|
setConfirmProcessing(false);
|
|
5943
7176
|
}
|
|
5944
7177
|
})();
|
|
5945
|
-
}, [emitDecline, locale, normalizeSavedPaymentError, resolvedBillingUrl]);
|
|
5946
|
-
const initializedHashRef =
|
|
7178
|
+
}, [emitDecline, invokeMerchantCallback, locale, normalizeSavedPaymentError, resolvedBillingUrl, telemetry]);
|
|
7179
|
+
const initializedHashRef = useRef6(null);
|
|
5947
7180
|
function hashCreateParams(params) {
|
|
5948
7181
|
const key = JSON.stringify({
|
|
5949
7182
|
c: params?.clientId,
|
|
@@ -5980,7 +7213,7 @@ function FloPayCheckout({
|
|
|
5980
7213
|
() => effectiveCreateSession ? hashCreateParams(effectiveCreateSession) : "",
|
|
5981
7214
|
[effectiveCreateSession]
|
|
5982
7215
|
);
|
|
5983
|
-
const createSessionParamsRef =
|
|
7216
|
+
const createSessionParamsRef = useRef6(effectiveCreateSession);
|
|
5984
7217
|
createSessionParamsRef.current = effectiveCreateSession;
|
|
5985
7218
|
useEffect6(() => {
|
|
5986
7219
|
setResolvedSessionId(sessionIdProp ?? "");
|
|
@@ -5992,44 +7225,107 @@ function FloPayCheckout({
|
|
|
5992
7225
|
setModeOverlayStatus(null);
|
|
5993
7226
|
}, [createSessionHash, initialErrorMessage, sessionIdProp]);
|
|
5994
7227
|
async function resolveInlineSession(params, cacheKey) {
|
|
5995
|
-
const
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
7228
|
+
const reporter = createStandaloneTelemetryReporter(
|
|
7229
|
+
resolvedBillingUrl,
|
|
7230
|
+
telemetry,
|
|
7231
|
+
telemetryCheckoutContext
|
|
7232
|
+
);
|
|
7233
|
+
const api = new PaymentAPI4(resolvedBillingUrl, { telemetry: false });
|
|
7234
|
+
try {
|
|
7235
|
+
const cached = readCachedInlineSession(cacheKey);
|
|
7236
|
+
reporter.log({
|
|
7237
|
+
name: cached ? "operation.cache.hit" : "operation.cache.miss",
|
|
7238
|
+
stage: "session_create",
|
|
7239
|
+
requestCategory: "session_create"
|
|
7240
|
+
});
|
|
7241
|
+
let sid = cached?.sid ?? null;
|
|
7242
|
+
let realResult = null;
|
|
7243
|
+
if (sid) {
|
|
7244
|
+
try {
|
|
7245
|
+
realResult = await api.getUnifiedCheckoutSession(sid, cached?.nonce);
|
|
7246
|
+
const status = realResult.data.session?.status;
|
|
7247
|
+
if (status === "complete") {
|
|
7248
|
+
if (wasSessionRecentlyCompleted(sid)) {
|
|
7249
|
+
return { sid, result: realResult };
|
|
7250
|
+
}
|
|
7251
|
+
clearCachedInlineSession(cacheKey);
|
|
7252
|
+
sid = null;
|
|
7253
|
+
realResult = null;
|
|
6006
7254
|
}
|
|
7255
|
+
} catch {
|
|
7256
|
+
reporter.log({
|
|
7257
|
+
name: "operation.fallback",
|
|
7258
|
+
stage: "session_read",
|
|
7259
|
+
requestCategory: "session_read"
|
|
7260
|
+
});
|
|
6007
7261
|
clearCachedInlineSession(cacheKey);
|
|
6008
7262
|
sid = null;
|
|
6009
|
-
realResult = null;
|
|
6010
7263
|
}
|
|
6011
|
-
} catch {
|
|
6012
|
-
clearCachedInlineSession(cacheKey);
|
|
6013
|
-
sid = null;
|
|
6014
7264
|
}
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
7265
|
+
if (!sid) {
|
|
7266
|
+
const sessionCreateStartedAt = reporter.now();
|
|
7267
|
+
reporter.log({
|
|
7268
|
+
name: "session.create.started",
|
|
7269
|
+
stage: "session_create",
|
|
7270
|
+
requestCategory: "session_create"
|
|
7271
|
+
});
|
|
7272
|
+
const paramsWithAnalytics = {
|
|
7273
|
+
...params,
|
|
7274
|
+
avsCheck: !!enableAVS,
|
|
7275
|
+
avsConfig: typeof enableAVS === "object" ? enableAVS : void 0,
|
|
7276
|
+
checkoutType: "embedded_checkout",
|
|
7277
|
+
checkoutLayout: children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout"
|
|
7278
|
+
};
|
|
7279
|
+
try {
|
|
7280
|
+
realResult = await api.createAndFetchSession(paramsWithAnalytics);
|
|
7281
|
+
reporter.log({
|
|
7282
|
+
name: "session.request.completed",
|
|
7283
|
+
stage: "session_complete",
|
|
7284
|
+
requestCategory: "session_create",
|
|
7285
|
+
statusClass: "2xx"
|
|
7286
|
+
});
|
|
7287
|
+
reporter.performance({
|
|
7288
|
+
stage: "session_create",
|
|
7289
|
+
durationMs: reporter.now() - sessionCreateStartedAt,
|
|
7290
|
+
durationMode: "machine",
|
|
7291
|
+
requestCategory: "session_create",
|
|
7292
|
+
statusClass: "2xx"
|
|
7293
|
+
});
|
|
7294
|
+
} catch (error) {
|
|
7295
|
+
reporter.performance({
|
|
7296
|
+
stage: "session_create",
|
|
7297
|
+
durationMs: reporter.now() - sessionCreateStartedAt,
|
|
7298
|
+
durationMode: "machine",
|
|
7299
|
+
requestCategory: "session_create",
|
|
7300
|
+
statusClass: "network_error"
|
|
7301
|
+
});
|
|
7302
|
+
if (error instanceof FloPayError5 && error.type === "validation_error") {
|
|
7303
|
+
reporter.terminal({
|
|
7304
|
+
outcome: "validation_rejected",
|
|
7305
|
+
stage: "session_create",
|
|
7306
|
+
paymentMethodCategory: "unknown"
|
|
7307
|
+
});
|
|
7308
|
+
} else {
|
|
7309
|
+
reporter.error({
|
|
7310
|
+
errorCode: "CHECKOUT_SESSION_CREATE_FAILED",
|
|
7311
|
+
stage: "session_create",
|
|
7312
|
+
paymentMethodCategory: "unknown",
|
|
7313
|
+
requestCategory: "session_create"
|
|
7314
|
+
});
|
|
7315
|
+
}
|
|
7316
|
+
throw error;
|
|
7317
|
+
}
|
|
7318
|
+
sid = realResult.data.session?.id ?? "";
|
|
7319
|
+
if (sid) {
|
|
7320
|
+
persistCachedInlineSession(cacheKey, sid, realResult.data.session?.clientSecret);
|
|
7321
|
+
}
|
|
6028
7322
|
}
|
|
7323
|
+
return { sid: sid ?? "", result: realResult };
|
|
7324
|
+
} finally {
|
|
7325
|
+
finishStandaloneTelemetry(reporter);
|
|
6029
7326
|
}
|
|
6030
|
-
return { sid: sid ?? "", result: realResult };
|
|
6031
7327
|
}
|
|
6032
|
-
const bootstrapInlineSession =
|
|
7328
|
+
const bootstrapInlineSession = useCallback4(
|
|
6033
7329
|
async (patch) => {
|
|
6034
7330
|
const baseParams = createSessionParamsRef.current;
|
|
6035
7331
|
if (!baseParams) {
|
|
@@ -6073,7 +7369,8 @@ function FloPayCheckout({
|
|
|
6073
7369
|
publishableKey,
|
|
6074
7370
|
paypalPublishableKey,
|
|
6075
7371
|
billingApiUrl: resolvedBillingUrl,
|
|
6076
|
-
locale
|
|
7372
|
+
locale,
|
|
7373
|
+
telemetry
|
|
6077
7374
|
});
|
|
6078
7375
|
flopayRef.current = instance;
|
|
6079
7376
|
setFloPay(instance);
|
|
@@ -6088,9 +7385,9 @@ function FloPayCheckout({
|
|
|
6088
7385
|
}
|
|
6089
7386
|
return resolved;
|
|
6090
7387
|
},
|
|
6091
|
-
[locale, resolvedBillingUrl]
|
|
7388
|
+
[locale, resolvedBillingUrl, telemetry, telemetryCheckoutContext]
|
|
6092
7389
|
);
|
|
6093
|
-
const handleInlineSessionPatch =
|
|
7390
|
+
const handleInlineSessionPatch = useCallback4(async (patch) => {
|
|
6094
7391
|
if (!hasInlineSessionPatchData(patch) || cardBootstrapPending) {
|
|
6095
7392
|
return {
|
|
6096
7393
|
sessionId: resolvedSessionId,
|
|
@@ -6167,7 +7464,9 @@ function FloPayCheckout({
|
|
|
6167
7464
|
setModeOverlayStatus("success");
|
|
6168
7465
|
await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
|
|
6169
7466
|
if (!cancelled) {
|
|
6170
|
-
|
|
7467
|
+
invokeMerchantCallback(() => {
|
|
7468
|
+
onCompleteRef.current?.({ status: "succeeded" });
|
|
7469
|
+
});
|
|
6171
7470
|
setModeOverlayStatus(null);
|
|
6172
7471
|
setModeOverlayError(null);
|
|
6173
7472
|
}
|
|
@@ -6187,8 +7486,9 @@ function FloPayCheckout({
|
|
|
6187
7486
|
}
|
|
6188
7487
|
setIsLoading(true);
|
|
6189
7488
|
async function init() {
|
|
7489
|
+
let sessionReadCompleted = false;
|
|
6190
7490
|
try {
|
|
6191
|
-
const api = new PaymentAPI4(resolvedBillingUrl);
|
|
7491
|
+
const api = new PaymentAPI4(resolvedBillingUrl, { telemetry: false });
|
|
6192
7492
|
const result = await api.getUnifiedCheckoutSession(activeSessionId, nonceProp);
|
|
6193
7493
|
if (cancelled) return;
|
|
6194
7494
|
setUnified(result);
|
|
@@ -6199,7 +7499,9 @@ function FloPayCheckout({
|
|
|
6199
7499
|
}
|
|
6200
7500
|
if (sess.status === "complete") {
|
|
6201
7501
|
setIsLoading(false);
|
|
6202
|
-
|
|
7502
|
+
invokeMerchantCallback(() => {
|
|
7503
|
+
onSessionCompletedRef.current?.(sess.successUrl ?? "");
|
|
7504
|
+
});
|
|
6203
7505
|
return;
|
|
6204
7506
|
}
|
|
6205
7507
|
if (sess.status === "expired") {
|
|
@@ -6207,6 +7509,7 @@ function FloPayCheckout({
|
|
|
6207
7509
|
code: "checkout_session_expired"
|
|
6208
7510
|
});
|
|
6209
7511
|
}
|
|
7512
|
+
sessionReadCompleted = true;
|
|
6210
7513
|
const effectiveMode = checkoutModeProp ?? sess.checkoutMode ?? "full";
|
|
6211
7514
|
setCurrentMode(effectiveMode);
|
|
6212
7515
|
const hasPayPalRedirectParams = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("payment_intent");
|
|
@@ -6238,6 +7541,23 @@ function FloPayCheckout({
|
|
|
6238
7541
|
if (!cancelled) setIsLoading(false);
|
|
6239
7542
|
} catch (err) {
|
|
6240
7543
|
if (cancelled) return;
|
|
7544
|
+
if (!(err instanceof FloPayError5)) {
|
|
7545
|
+
reportStandaloneTelemetryError(
|
|
7546
|
+
resolvedBillingUrl,
|
|
7547
|
+
telemetry,
|
|
7548
|
+
telemetryCheckoutContext,
|
|
7549
|
+
"INTERNAL_SDK_ERROR",
|
|
7550
|
+
"checkout_mount"
|
|
7551
|
+
);
|
|
7552
|
+
} else if (!sessionReadCompleted && !isExpectedExistingSessionError(err)) {
|
|
7553
|
+
reportStandaloneTelemetryError(
|
|
7554
|
+
resolvedBillingUrl,
|
|
7555
|
+
telemetry,
|
|
7556
|
+
telemetryCheckoutContext,
|
|
7557
|
+
"NETWORK_REQUEST_FAILED",
|
|
7558
|
+
"session_read"
|
|
7559
|
+
);
|
|
7560
|
+
}
|
|
6241
7561
|
const floPayErr = err instanceof FloPayError5 ? err : new FloPayError5(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
|
|
6242
7562
|
setLoadError(floPayErr);
|
|
6243
7563
|
setIsLoading(false);
|
|
@@ -6259,7 +7579,8 @@ function FloPayCheckout({
|
|
|
6259
7579
|
publishableKey,
|
|
6260
7580
|
paypalPublishableKey,
|
|
6261
7581
|
billingApiUrl: resolvedBillingUrl,
|
|
6262
|
-
locale
|
|
7582
|
+
locale,
|
|
7583
|
+
telemetry
|
|
6263
7584
|
});
|
|
6264
7585
|
flopayRef.current = instance;
|
|
6265
7586
|
setFloPay(instance);
|
|
@@ -6276,10 +7597,11 @@ function FloPayCheckout({
|
|
|
6276
7597
|
createSessionHash,
|
|
6277
7598
|
effectiveCreateSessionMode,
|
|
6278
7599
|
initSessionDependency,
|
|
7600
|
+
invokeMerchantCallback,
|
|
6279
7601
|
nonceProp,
|
|
6280
7602
|
runSavedPaymentFlow
|
|
6281
7603
|
]);
|
|
6282
|
-
const handleConfirmCheckout =
|
|
7604
|
+
const handleConfirmCheckout = useCallback4(async () => {
|
|
6283
7605
|
if (confirmProcessing || !session) return;
|
|
6284
7606
|
setConfirmProcessing(true);
|
|
6285
7607
|
setModeError(null);
|
|
@@ -6443,7 +7765,7 @@ function FloPayCheckout({
|
|
|
6443
7765
|
}
|
|
6444
7766
|
),
|
|
6445
7767
|
/* @__PURE__ */ jsx8(
|
|
6446
|
-
|
|
7768
|
+
InstrumentedDirectPayPalButton,
|
|
6447
7769
|
{
|
|
6448
7770
|
sessionId: activeSessionId,
|
|
6449
7771
|
nonce: session.clientSecret || void 0,
|
|
@@ -6458,6 +7780,8 @@ function FloPayCheckout({
|
|
|
6458
7780
|
onDecline,
|
|
6459
7781
|
onButtonClick,
|
|
6460
7782
|
session,
|
|
7783
|
+
telemetry,
|
|
7784
|
+
telemetryContext: telemetryCheckoutContext,
|
|
6461
7785
|
debug
|
|
6462
7786
|
}
|
|
6463
7787
|
)
|
|
@@ -6846,7 +8170,7 @@ function InterimButtonsView({
|
|
|
6846
8170
|
// src/checkout-form.tsx
|
|
6847
8171
|
import { PaymentAPI as PaymentAPI5 } from "@flopay/js";
|
|
6848
8172
|
import { FloPayError as FloPayError6 } from "@flopay/shared";
|
|
6849
|
-
import { forwardRef as forwardRef2, useCallback as
|
|
8173
|
+
import { forwardRef as forwardRef2, useCallback as useCallback5, useEffect as useEffect7, useImperativeHandle as useImperativeHandle2, useState as useState5 } from "react";
|
|
6850
8174
|
import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
6851
8175
|
var WALLET_RESUME_KEY = "flopay_wallet_resume";
|
|
6852
8176
|
var CheckoutForm = forwardRef2(
|
|
@@ -6888,20 +8212,20 @@ function CheckoutFormInner({
|
|
|
6888
8212
|
const isSubmitting = externalProcessing ?? processing;
|
|
6889
8213
|
const isSelfContained = !onTokenizedBody;
|
|
6890
8214
|
const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
|
|
6891
|
-
const updateError =
|
|
8215
|
+
const updateError = useCallback5(
|
|
6892
8216
|
(err) => {
|
|
6893
8217
|
setError(err);
|
|
6894
8218
|
onErrorChange?.(err);
|
|
6895
8219
|
},
|
|
6896
8220
|
[onErrorChange]
|
|
6897
8221
|
);
|
|
6898
|
-
const emitDecline =
|
|
8222
|
+
const emitDecline = useCallback5(
|
|
6899
8223
|
(input, overrides) => {
|
|
6900
8224
|
onDecline?.(buildDeclineEvent("card", input, overrides));
|
|
6901
8225
|
},
|
|
6902
8226
|
[onDecline]
|
|
6903
8227
|
);
|
|
6904
|
-
const processPaymentInternal =
|
|
8228
|
+
const processPaymentInternal = useCallback5(
|
|
6905
8229
|
async (tokenizedBody, completionPaymentMethodId) => {
|
|
6906
8230
|
setProcessing(true);
|
|
6907
8231
|
updateError(null);
|
|
@@ -7013,7 +8337,7 @@ function CheckoutFormInner({
|
|
|
7013
8337
|
},
|
|
7014
8338
|
[baseUrl, sessionId, nonce, userId, email, firstName, lastName, chv, flopay, paypalFlopay, onComplete, onError, onDecline, updateError, emitDecline]
|
|
7015
8339
|
);
|
|
7016
|
-
const dispatchTokenizedBody =
|
|
8340
|
+
const dispatchTokenizedBody = useCallback5(
|
|
7017
8341
|
(tokenizedBody) => {
|
|
7018
8342
|
if (onTokenizedBody) {
|
|
7019
8343
|
onTokenizedBody(tokenizedBody);
|
|
@@ -7068,7 +8392,7 @@ function CheckoutFormInner({
|
|
|
7068
8392
|
localStorage.removeItem(WALLET_RESUME_KEY);
|
|
7069
8393
|
}
|
|
7070
8394
|
}, [sessionId, dispatchTokenizedBody]);
|
|
7071
|
-
const handleSubmit =
|
|
8395
|
+
const handleSubmit = useCallback5(
|
|
7072
8396
|
async (e) => {
|
|
7073
8397
|
e.preventDefault();
|
|
7074
8398
|
if (!flopay || !elements || isSubmitting) return;
|
|
@@ -7191,7 +8515,7 @@ function CheckoutFormInner({
|
|
|
7191
8515
|
// src/paypal-button.tsx
|
|
7192
8516
|
import { PaymentAPI as PaymentAPI6 } from "@flopay/js";
|
|
7193
8517
|
import { FloPayError as FloPayError7 } from "@flopay/shared";
|
|
7194
|
-
import { useCallback as
|
|
8518
|
+
import { useCallback as useCallback6, useEffect as useEffect8, useRef as useRef7, useState as useState6 } from "react";
|
|
7195
8519
|
import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
7196
8520
|
function PayPalButton({
|
|
7197
8521
|
sessionId,
|
|
@@ -7212,9 +8536,9 @@ function PayPalButton({
|
|
|
7212
8536
|
const contextBillingUrl = useBillingApiUrl();
|
|
7213
8537
|
const [ready, setReady] = useState6(false);
|
|
7214
8538
|
const [submitting, setSubmitting] = useState6(false);
|
|
7215
|
-
const paypalResumeAttempted =
|
|
8539
|
+
const paypalResumeAttempted = useRef7(false);
|
|
7216
8540
|
const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
|
|
7217
|
-
const processPaymentInternal =
|
|
8541
|
+
const processPaymentInternal = useCallback6(
|
|
7218
8542
|
async (tokenizedBody) => {
|
|
7219
8543
|
try {
|
|
7220
8544
|
const api = new PaymentAPI6(baseUrl);
|
|
@@ -7242,7 +8566,7 @@ function PayPalButton({
|
|
|
7242
8566
|
},
|
|
7243
8567
|
[baseUrl, sessionId, nonce, userId, email, firstName, lastName, chv, onComplete, onErrorChange]
|
|
7244
8568
|
);
|
|
7245
|
-
const dispatchTokenizedBody =
|
|
8569
|
+
const dispatchTokenizedBody = useCallback6(
|
|
7246
8570
|
(body) => {
|
|
7247
8571
|
if (onTokenizedBody) {
|
|
7248
8572
|
onTokenizedBody(body);
|
|
@@ -7300,7 +8624,7 @@ function PayPalButton({
|
|
|
7300
8624
|
}
|
|
7301
8625
|
})();
|
|
7302
8626
|
}, [flopay, dispatchTokenizedBody, onErrorChange]);
|
|
7303
|
-
const handlePayPalConfirm =
|
|
8627
|
+
const handlePayPalConfirm = useCallback6(async () => {
|
|
7304
8628
|
if (!flopay || !elements) return;
|
|
7305
8629
|
try {
|
|
7306
8630
|
setSubmitting(true);
|
|
@@ -7380,7 +8704,7 @@ function PayPalButton({
|
|
|
7380
8704
|
}
|
|
7381
8705
|
|
|
7382
8706
|
// src/automatic-payment-button.tsx
|
|
7383
|
-
import { useCallback as
|
|
8707
|
+
import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef8, useState as useState7 } from "react";
|
|
7384
8708
|
import { PaymentAPI as PaymentAPI7 } from "@flopay/js";
|
|
7385
8709
|
import { FloPayError as FloPayError8, resolveBillingApiUrl as resolveBillingApiUrl4, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme3, resolveTheme as resolveTheme3 } from "@flopay/shared";
|
|
7386
8710
|
import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
@@ -7499,12 +8823,12 @@ function FloPayAutomaticPaymentButton({
|
|
|
7499
8823
|
const [overlayStatus, setOverlayStatus] = useState7(null);
|
|
7500
8824
|
const [overlayError, setOverlayError] = useState7(null);
|
|
7501
8825
|
const [fallbackSession, setFallbackSession] = useState7(null);
|
|
7502
|
-
const isMountedRef =
|
|
7503
|
-
const threeDsAbortRef =
|
|
7504
|
-
const fallbackSessionRef =
|
|
7505
|
-
const onSuccessRef =
|
|
7506
|
-
const onErrorRef =
|
|
7507
|
-
const onDeclineRef =
|
|
8826
|
+
const isMountedRef = useRef8(true);
|
|
8827
|
+
const threeDsAbortRef = useRef8(null);
|
|
8828
|
+
const fallbackSessionRef = useRef8(fallbackSession);
|
|
8829
|
+
const onSuccessRef = useRef8(onSuccess);
|
|
8830
|
+
const onErrorRef = useRef8(onError);
|
|
8831
|
+
const onDeclineRef = useRef8(onDecline);
|
|
7508
8832
|
useEffect9(() => {
|
|
7509
8833
|
fallbackSessionRef.current = fallbackSession;
|
|
7510
8834
|
}, [fallbackSession]);
|
|
@@ -7525,7 +8849,7 @@ function FloPayAutomaticPaymentButton({
|
|
|
7525
8849
|
};
|
|
7526
8850
|
}, []);
|
|
7527
8851
|
useEffect9(() => {
|
|
7528
|
-
if (
|
|
8852
|
+
if (typeof window === "undefined") {
|
|
7529
8853
|
return;
|
|
7530
8854
|
}
|
|
7531
8855
|
const handleKeyDown = (event) => {
|
|
@@ -7537,14 +8861,14 @@ function FloPayAutomaticPaymentButton({
|
|
|
7537
8861
|
return () => {
|
|
7538
8862
|
window.removeEventListener("keydown", handleKeyDown);
|
|
7539
8863
|
};
|
|
7540
|
-
}, [
|
|
7541
|
-
const emitDecline =
|
|
8864
|
+
}, []);
|
|
8865
|
+
const emitDecline = useCallback7((error, method = DEFAULT_SAVED_PAYMENT_DECLINE_METHOD) => {
|
|
7542
8866
|
onDeclineRef.current?.(buildDeclineEvent(method, error, {
|
|
7543
8867
|
code: error.code,
|
|
7544
8868
|
declineCode: error.declineCode
|
|
7545
8869
|
}));
|
|
7546
8870
|
}, []);
|
|
7547
|
-
const showSuccess =
|
|
8871
|
+
const showSuccess = useCallback7(async (event) => {
|
|
7548
8872
|
if (!isMountedRef.current) return;
|
|
7549
8873
|
setOverlayError(null);
|
|
7550
8874
|
setOverlayStatus("success");
|
|
@@ -7552,7 +8876,7 @@ function FloPayAutomaticPaymentButton({
|
|
|
7552
8876
|
if (!isMountedRef.current) return;
|
|
7553
8877
|
onSuccessRef.current?.(event);
|
|
7554
8878
|
}, []);
|
|
7555
|
-
const showError =
|
|
8879
|
+
const showError = useCallback7(async (error, options) => {
|
|
7556
8880
|
if (!isMountedRef.current) return;
|
|
7557
8881
|
onErrorRef.current?.(error);
|
|
7558
8882
|
if (options?.emitDecline) {
|
|
@@ -7562,7 +8886,7 @@ function FloPayAutomaticPaymentButton({
|
|
|
7562
8886
|
setOverlayStatus("error");
|
|
7563
8887
|
await sleep2(PROCESSING_OVERLAY_ERROR_DELAY_MS);
|
|
7564
8888
|
}, [emitDecline]);
|
|
7565
|
-
const processResolvedSession =
|
|
8889
|
+
const processResolvedSession = useCallback7(async (apiResult, resolvedSessionId, options) => {
|
|
7566
8890
|
const session = apiResult.data.session ?? null;
|
|
7567
8891
|
if (!session) {
|
|
7568
8892
|
throw new FloPayError8("No session data returned", "api_error");
|
|
@@ -7718,7 +9042,7 @@ function FloPayAutomaticPaymentButton({
|
|
|
7718
9042
|
showError,
|
|
7719
9043
|
showSuccess
|
|
7720
9044
|
]);
|
|
7721
|
-
const handleButtonClick =
|
|
9045
|
+
const handleButtonClick = useCallback7(async (event) => {
|
|
7722
9046
|
buttonProps.onClick?.(event);
|
|
7723
9047
|
if (event.defaultPrevented || disabled || isProcessing) {
|
|
7724
9048
|
return;
|
|
@@ -7800,7 +9124,7 @@ function FloPayAutomaticPaymentButton({
|
|
|
7800
9124
|
showError,
|
|
7801
9125
|
showSuccess
|
|
7802
9126
|
]);
|
|
7803
|
-
const handleFallbackComplete =
|
|
9127
|
+
const handleFallbackComplete = useCallback7((result) => {
|
|
7804
9128
|
const activeFallback = fallbackSessionRef.current;
|
|
7805
9129
|
setFallbackSession(null);
|
|
7806
9130
|
onSuccessRef.current?.({
|
|
@@ -7810,10 +9134,10 @@ function FloPayAutomaticPaymentButton({
|
|
|
7810
9134
|
autoCompleted: false
|
|
7811
9135
|
});
|
|
7812
9136
|
}, []);
|
|
7813
|
-
const handleFallbackError =
|
|
9137
|
+
const handleFallbackError = useCallback7((error) => {
|
|
7814
9138
|
onErrorRef.current?.(error);
|
|
7815
9139
|
}, []);
|
|
7816
|
-
const handleFallbackDecline =
|
|
9140
|
+
const handleFallbackDecline = useCallback7((decline) => {
|
|
7817
9141
|
onDeclineRef.current?.(decline);
|
|
7818
9142
|
}, []);
|
|
7819
9143
|
const themeBundle = useMemo5(() => resolveTheme3(theme), [theme]);
|