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