@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/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
- () => ({ flopay, paypalFlopay, elements, billingApiUrl: resolvedBillingApiUrl }),
97
- [flopay, paypalFlopay, elements, resolvedBillingApiUrl]
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 useCallback2, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef5, useState as useState4 } from "react";
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 = useRef(null);
190
- const elementRef = useRef(null);
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
- if (onReady) element.on("ready", onReady);
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 useRef2 } from "react";
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 = useRef2(null);
279
- const onReadyRef = useRef2(onReady);
280
- const onErrorRef = useRef2(onError);
281
- const onValidationRef = useRef2(onValidation);
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 = useRef2(theme);
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 useRef4, useState as useState3 } from "react";
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 useRef3, useState as useState2 } from "react";
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
- function DirectPayPalButton({
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 containerRef = useRef3(null);
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 = useRef3(onTokenizedBody);
811
- const onCompleteRef = useRef3(onComplete);
812
- const onErrorChangeRef = useRef3(onErrorChange);
813
- const onDeclineRef = useRef3(onDecline);
814
- const onButtonClickRef = useRef3(onButtonClick);
815
- const onLoadStateChangeRef = useRef3(onLoadStateChange);
816
- const runBeforeButtonClickRef = useRef3(runBeforeButtonClick);
817
- const sessionRef = useRef3(session);
818
- const emailRef = useRef3(email);
819
- const nonceRef = useRef3(nonce);
820
- const beforeClickRef = useRef3(null);
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
- onLoadStateChangeRef.current?.(ready && !failed);
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
- onCompleteRef.current?.({ status: "succeeded", checkoutMethod: "paypal" });
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
- beforeClickRef.current = {
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
- onDeclineRef.current?.(buildDeclineEvent("paypal", "PayPal checkout was cancelled."));
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
- forwardError(message);
1166
- onDeclineRef.current?.(buildDeclineEvent("paypal", message));
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 = useRef4(false);
1531
- const beforeClickRef = useRef4(null);
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 = useCallback(async (event) => {
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
- beforeClickRef.current = null;
2352
+ attemptContextBySurfaceRef.current.delete(surfaceKey);
1607
2353
  event.reject();
1608
2354
  return;
1609
2355
  }
1610
- beforeClickRef.current = {
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 = useCallback(async (event) => {
2366
+ }, [attemptContextBySurfaceRef, isProcessing, onButtonClick, runBeforeButtonClick, startAttempt, submitting, surfaceKey]);
2367
+ const handlePayPalConfirm = useCallback3(async (event) => {
1619
2368
  if (!stripe || !elements) return;
1620
- let prepared = beforeClickRef.current;
1621
- beforeClickRef.current = null;
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
- const message = pmError.message ?? "PayPal payment failed.";
1647
- onErrorChange?.(message);
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
- const message = confirmError.message ?? "PayPal payment failed.";
1708
- onErrorChange?.(message);
1709
- onDecline?.(buildDeclineEvent("paypal", message, {
1710
- code: confirmError.code
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
- onErrorChange?.(err instanceof Error ? err.message : "PayPal payment failed. Please try again.");
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__ */ jsx7(
1746
- ExpressCheckoutElement,
2506
+ children: /* @__PURE__ */ jsxs4(
2507
+ "div",
1747
2508
  {
1748
- onReady: (event) => setLoadState(resolveExpressCheckoutLoadState(event, ["paypal"])),
1749
- onLoadError: () => setLoadState("load_error"),
1750
- onClick: handlePayPalClick,
1751
- onConfirm: handlePayPalConfirm,
1752
- onCancel: () => {
1753
- beforeClickRef.current = null;
1754
- onDecline?.(buildDeclineEvent("paypal", "PayPal checkout was cancelled."));
1755
- },
1756
- options: {
1757
- buttonType: { paypal: "paypal" },
1758
- billingAddressRequired: false,
1759
- phoneNumberRequired: false,
1760
- shippingAddressRequired: false,
1761
- paymentMethods: {
1762
- applePay: "never",
1763
- googlePay: "never",
1764
- paypal: "auto",
1765
- link: "never"
1766
- }
1767
- }
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 lastWalletMethodRef = useRef4("card");
1798
- const beforeClickRef = useRef4(null);
1799
- const handleWalletConfirm = useCallback(
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 walletType = event.expressPaymentType;
1803
- let prepared = beforeClickRef.current;
1804
- beforeClickRef.current = null;
1805
- const buttonMethod = walletType === "apple_pay" ? "apple_pay" : "google_pay";
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
- onErrorChange?.(submitError.message ?? "Wallet payment failed.");
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
- onErrorChange?.(pmError?.message ?? "Failed to create payment method.");
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
- const message = confirmError.message ?? "Wallet payment failed.";
1866
- onErrorChange?.(message);
1867
- onDecline?.(buildDeclineEvent(method, message, {
1868
- code: confirmError.code
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
- onErrorChange?.(err instanceof Error ? err.message : "Wallet payment failed. Please try again.");
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__ */ jsx7(
1915
- ExpressCheckoutElement,
2774
+ children: /* @__PURE__ */ jsxs4(
2775
+ "div",
1916
2776
  {
1917
- onReady: (event) => {
1918
- setLoadState(resolveExpressCheckoutLoadState(event, availableMethodKeys));
1919
- },
1920
- onLoadError: (_event) => {
1921
- setLoadState("load_error");
1922
- },
1923
- onClick: async (event) => {
1924
- lastWalletMethodRef.current = event.expressPaymentType === "apple_pay" ? "apple_pay" : "google_pay";
1925
- const beforeClick = runBeforeButtonClick ? await runBeforeButtonClick(lastWalletMethodRef.current) : { proceed: true };
1926
- if (!beforeClick.proceed) {
1927
- beforeClickRef.current = null;
1928
- event.reject();
1929
- return;
1930
- }
1931
- beforeClickRef.current = {
1932
- accountPatch: beforeClick.accountPatch,
1933
- sessionId: beforeClick.sessionId,
1934
- nonce: beforeClick.nonce
1935
- };
1936
- onButtonClick?.(lastWalletMethodRef.current);
1937
- event.resolve();
1938
- },
1939
- onConfirm: handleWalletConfirm,
1940
- onCancel: () => {
1941
- beforeClickRef.current = null;
1942
- onDecline?.(buildDeclineEvent(lastWalletMethodRef.current, "Wallet checkout was cancelled."));
1943
- },
1944
- options: {
1945
- buttonType: { applePay: "plain", googlePay: "plain" },
1946
- paymentMethods: expressMethodMap,
1947
- layout: { maxColumns: 1, overflow: "never" }
1948
- }
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 = useRef4(false);
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 = useCallback(async () => {
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: () => setLoadState("load_error"),
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 = useRef4(false);
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 = useCallback(async (method) => {
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 = useCallback((method) => {
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 = useRef4(zipProp ?? "");
2653
- const selectedCountryRef = useRef4(countryProp ?? "US");
2654
- const addressLine1Ref = useRef4(addressLine1Prop ?? "");
2655
- const addressLine2Ref = useRef4(addressLine2Prop ?? "");
2656
- const cityRef = useRef4(cityProp ?? "");
2657
- const stateRef = useRef4(stateProp ?? "");
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 = useCallback(() => {
3618
+ const expandToCard = useCallback3(() => {
2700
3619
  setViewState("expanding");
2701
3620
  setTimeout(() => setViewState("card"), TRANSITION_MS);
2702
3621
  }, []);
2703
- const collapseToButtons = useCallback(() => {
3622
+ const collapseToButtons = useCallback3(() => {
2704
3623
  setViewState("collapsing");
2705
3624
  setTimeout(() => setViewState("buttons"), TRANSITION_MS);
2706
3625
  }, []);
2707
- const expandToApm = useCallback((method) => {
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 = useCallback(() => {
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 = useRef4(false);
3646
+ const processingRef = useRef5(false);
2728
3647
  const [paypalDirectRetry, setPaypalDirectRetry] = useState3(null);
2729
- const paypalDirectRetryRef = useRef4(paypalDirectRetry);
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 = useCallback(
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 = useCallback(
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 = useRef4({
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 = useRef4(false);
2924
- const buildVaultAccountSnapshot = useCallback(() => {
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 = useRef4(false);
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 = useRef4(false);
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 = useCallback((value) => {
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 = useCallback(
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 = useCallback(async (method) => {
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 = useCallback(
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 = useCallback(
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 = useRef4(false);
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 = useCallback(
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(baseUrl);
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 = useRef5(null);
6629
+ const flopayRef = useRef6(null);
5643
6630
  const [paypalFlopay, setPaypalFloPay] = useState4(null);
5644
- const paypalFlopayRef = useRef5(null);
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 = useRef5(false);
5660
- const paypalResumeAttempted = useRef5(false);
5661
- const savedPaymentKeysRef = useRef5(null);
5662
- const onCompleteRef = useRef5(onComplete);
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 = useRef5(onError);
6655
+ const onErrorRef = useRef6(onError);
5665
6656
  onErrorRef.current = onError;
5666
- const onDeclineRef = useRef5(onDecline);
6657
+ const onDeclineRef = useRef6(onDecline);
5667
6658
  onDeclineRef.current = onDecline;
5668
- const onSessionCompletedRef = useRef5(onSessionCompleted);
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: 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 = useCallback2(
6702
+ const emitDecline = useCallback4(
5706
6703
  (method, input, overrides) => {
5707
- onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
6704
+ invokeMerchantCallback(() => {
6705
+ onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
6706
+ });
5708
6707
  },
5709
- []
6708
+ [invokeMerchantCallback]
5710
6709
  );
5711
- const runSavedPaymentFlow = useCallback2(
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
- onErrorRef.current?.(floPayErr);
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("PayPal payment was declined. Please try again.", "api_error"),
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 = useRef5(null);
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 = useRef5(effectiveCreateSession);
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 api = new PaymentAPI4(resolvedBillingUrl);
5996
- const cached = readCachedInlineSession(cacheKey);
5997
- let sid = cached?.sid ?? null;
5998
- let realResult = null;
5999
- if (sid) {
6000
- try {
6001
- realResult = await api.getUnifiedCheckoutSession(sid, cached?.nonce);
6002
- const status = realResult.data.session?.status;
6003
- if (status === "complete") {
6004
- if (wasSessionRecentlyCompleted(sid)) {
6005
- return { sid, result: realResult };
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
- if (!sid) {
6017
- const paramsWithAnalytics = {
6018
- ...params,
6019
- avsCheck: !!enableAVS,
6020
- avsConfig: typeof enableAVS === "object" ? enableAVS : void 0,
6021
- checkoutType: "embedded_checkout",
6022
- checkoutLayout: children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout"
6023
- };
6024
- realResult = await api.createAndFetchSession(paramsWithAnalytics);
6025
- sid = realResult.data.session?.id ?? "";
6026
- if (sid) {
6027
- persistCachedInlineSession(cacheKey, sid, realResult.data.session?.clientSecret);
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 = useCallback2(
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 = useCallback2(async (patch) => {
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
- onCompleteRef.current?.({ status: "succeeded" });
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
- onSessionCompletedRef.current?.(sess.successUrl ?? "");
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 = useCallback2(async () => {
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
- DirectPayPalButton,
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 useCallback3, useEffect as useEffect7, useImperativeHandle as useImperativeHandle2, useState as useState5 } from "react";
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 = useCallback3(
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 = useCallback3(
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 = useCallback3(
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 = useCallback3(
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 = useCallback3(
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 useCallback4, useEffect as useEffect8, useRef as useRef6, useState as useState6 } from "react";
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 = useRef6(false);
8539
+ const paypalResumeAttempted = useRef7(false);
7216
8540
  const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
7217
- const processPaymentInternal = useCallback4(
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 = useCallback4(
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 = useCallback4(async () => {
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 useCallback5, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef7, useState as useState7 } from "react";
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 = useRef7(true);
7503
- const threeDsAbortRef = useRef7(null);
7504
- const fallbackSessionRef = useRef7(fallbackSession);
7505
- const onSuccessRef = useRef7(onSuccess);
7506
- const onErrorRef = useRef7(onError);
7507
- const onDeclineRef = useRef7(onDecline);
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 (!fallbackSession || typeof window === "undefined") {
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
- }, [fallbackSession]);
7541
- const emitDecline = useCallback5((error, method = DEFAULT_SAVED_PAYMENT_DECLINE_METHOD) => {
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 = useCallback5(async (event) => {
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 = useCallback5(async (error, options) => {
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 = useCallback5(async (apiResult, resolvedSessionId, options) => {
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 = useCallback5(async (event) => {
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 = useCallback5((result) => {
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 = useCallback5((error) => {
9137
+ const handleFallbackError = useCallback7((error) => {
7814
9138
  onErrorRef.current?.(error);
7815
9139
  }, []);
7816
- const handleFallbackDecline = useCallback5((decline) => {
9140
+ const handleFallbackDecline = useCallback7((decline) => {
7817
9141
  onDeclineRef.current?.(decline);
7818
9142
  }, []);
7819
9143
  const themeBundle = useMemo5(() => resolveTheme3(theme), [theme]);