@flopay/react 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/provider.tsx
2
- import { useEffect, useState, useMemo } from "react";
2
+ import { useCallback, useEffect, useState, useMemo, useRef } from "react";
3
3
  import { resolveBillingApiUrl } from "@flopay/shared";
4
4
 
5
5
  // src/context.ts
@@ -16,6 +16,59 @@ var CheckoutContext = createContext({
16
16
  error: null
17
17
  });
18
18
 
19
+ // src/telemetry-bridge.ts
20
+ var FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
21
+ var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
22
+ function noopTelemetryBridge() {
23
+ return {
24
+ error: () => {
25
+ },
26
+ log: () => {
27
+ },
28
+ performance: () => {
29
+ },
30
+ terminal: () => {
31
+ },
32
+ now: () => globalThis.performance?.now() ?? 0,
33
+ elapsed: (startedAt) => Math.max(0, (globalThis.performance?.now() ?? startedAt) - startedAt),
34
+ setCheckoutContext: () => {
35
+ },
36
+ beginCheckout: () => globalThis.performance?.now() ?? 0,
37
+ disable: () => {
38
+ },
39
+ flush: async () => {
40
+ },
41
+ destroy: () => {
42
+ }
43
+ };
44
+ }
45
+ function normalizeTelemetryBridge(source) {
46
+ const fallback = noopTelemetryBridge();
47
+ const bind = (candidate, defaultValue) => candidate ? candidate.bind(source) : defaultValue;
48
+ const now = bind(source.now, fallback.now);
49
+ return {
50
+ error: bind(source.error, fallback.error),
51
+ log: bind(source.log, fallback.log),
52
+ performance: bind(source.performance, fallback.performance),
53
+ terminal: bind(source.terminal, fallback.terminal),
54
+ now,
55
+ elapsed: source.elapsed ? source.elapsed.bind(source) : (startedAt) => Math.max(0, now() - startedAt),
56
+ setCheckoutContext: bind(source.setCheckoutContext, fallback.setCheckoutContext),
57
+ beginCheckout: bind(source.beginCheckout, fallback.beginCheckout),
58
+ disable: bind(source.disable, fallback.disable),
59
+ flush: bind(source.flush, fallback.flush),
60
+ destroy: bind(source.destroy, fallback.destroy)
61
+ };
62
+ }
63
+ function createTelemetryBridge(options) {
64
+ const factory = globalThis[TELEMETRY_REPORTER_FACTORY];
65
+ return normalizeTelemetryBridge(factory?.(options) ?? {});
66
+ }
67
+ function getFloPayTelemetryBridge(floPay) {
68
+ if (!floPay) return void 0;
69
+ return floPay[FLOPAY_TELEMETRY_BRIDGE];
70
+ }
71
+
19
72
  // src/provider.tsx
20
73
  import { jsx } from "react/jsx-runtime";
21
74
  function FloPayProvider({
@@ -31,6 +84,18 @@ function FloPayProvider({
31
84
  paypalFloPayProp instanceof Promise || !paypalFloPayProp ? null : paypalFloPayProp
32
85
  );
33
86
  const [elements, setElements] = useState(null);
87
+ const mountedAt = useRef(null);
88
+ const renderedElements = useRef(null);
89
+ const interactiveElements = useRef(null);
90
+ useEffect(() => {
91
+ if (!flopay) return;
92
+ const telemetry = getFloPayTelemetryBridge(flopay);
93
+ mountedAt.current = telemetry?.beginCheckout() ?? telemetry?.now() ?? 0;
94
+ telemetry?.log({ name: "checkout.mount", stage: "checkout_mount" });
95
+ return () => {
96
+ telemetry?.log({ name: "checkout.unmount", stage: "unmount" });
97
+ };
98
+ }, [flopay]);
34
99
  useEffect(() => {
35
100
  let cancelled = false;
36
101
  if (floPayProp instanceof Promise) {
@@ -91,18 +156,46 @@ function FloPayProvider({
91
156
  options?.paymentMethodCreation,
92
157
  options?.setupFutureUsage
93
158
  ]);
159
+ useEffect(() => {
160
+ if (!flopay || !elements || renderedElements.current === elements) return;
161
+ renderedElements.current = elements;
162
+ const telemetry = getFloPayTelemetryBridge(flopay);
163
+ telemetry?.log({ name: "checkout.rendered", stage: "checkout_render" });
164
+ telemetry?.performance({
165
+ stage: "checkout_render",
166
+ durationMs: telemetry.elapsed(mountedAt.current ?? 0),
167
+ durationMode: "machine"
168
+ });
169
+ }, [elements, flopay]);
170
+ const reportInteractive = useCallback(() => {
171
+ if (!flopay || !elements || interactiveElements.current === elements) return;
172
+ interactiveElements.current = elements;
173
+ const telemetry = getFloPayTelemetryBridge(flopay);
174
+ telemetry?.log({ name: "checkout.interactive", stage: "checkout_interactive" });
175
+ telemetry?.performance({
176
+ stage: "checkout_interactive",
177
+ durationMs: telemetry.elapsed(mountedAt.current ?? 0),
178
+ durationMode: "machine"
179
+ });
180
+ }, [elements, flopay]);
94
181
  const resolvedBillingApiUrl = resolveBillingApiUrl(options?.billingApiUrl);
95
182
  const value = useMemo(
96
- () => ({ flopay, paypalFlopay, elements, billingApiUrl: resolvedBillingApiUrl }),
97
- [flopay, paypalFlopay, elements, resolvedBillingApiUrl]
183
+ () => ({
184
+ flopay,
185
+ paypalFlopay,
186
+ elements,
187
+ billingApiUrl: resolvedBillingApiUrl,
188
+ reportInteractive
189
+ }),
190
+ [flopay, paypalFlopay, elements, resolvedBillingApiUrl, reportInteractive]
98
191
  );
99
192
  return /* @__PURE__ */ jsx(FloPayContext.Provider, { value, children });
100
193
  }
101
194
 
102
195
  // src/flopay-checkout.tsx
103
- import React8, { useCallback as useCallback3, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef5, useState as useState4 } from "react";
196
+ import React8, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo4, useRef as useRef6, useState as useState4 } from "react";
104
197
  import { PaymentAPI as PaymentAPI4 } from "@flopay/js";
105
- import { SDK_VERSION, FloPayError as FloPayError5, resolveBillingApiUrl as resolveBillingApiUrl3, buildCheckoutDisplayData, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme2, resolveTheme as resolveTheme2 } from "@flopay/shared";
198
+ import { SDK_VERSION as SDK_VERSION2, FloPayError as FloPayError5, resolveBillingApiUrl as resolveBillingApiUrl3, buildCheckoutDisplayData, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme2, resolveTheme as resolveTheme2 } from "@flopay/shared";
106
199
 
107
200
  // src/card-button-content.tsx
108
201
  import "react";
@@ -172,7 +265,7 @@ function TitleContentSlot({
172
265
  }
173
266
 
174
267
  // src/elements.tsx
175
- import { useEffect as useEffect2, useRef, useContext } from "react";
268
+ import { useEffect as useEffect2, useRef as useRef2, useContext } from "react";
176
269
  import { jsx as jsx3 } from "react/jsx-runtime";
177
270
  function createElementComponent(elementType, displayName) {
178
271
  function ElementComponent({
@@ -186,9 +279,9 @@ function createElementComponent(elementType, displayName) {
186
279
  onBlur,
187
280
  onEscape
188
281
  }) {
189
- const containerRef = useRef(null);
190
- const elementRef = useRef(null);
191
- const { elements } = useContext(FloPayContext);
282
+ const containerRef = useRef2(null);
283
+ const elementRef = useRef2(null);
284
+ const { elements, reportInteractive } = useContext(FloPayContext);
192
285
  useEffect2(() => {
193
286
  if (!elements || !containerRef.current) return;
194
287
  let mounted = true;
@@ -203,7 +296,10 @@ function createElementComponent(elementType, displayName) {
203
296
  element.mount(containerRef.current);
204
297
  elementRef.current = element;
205
298
  if (onChange) element.on("change", onChange);
206
- if (onReady) element.on("ready", onReady);
299
+ element.on("ready", () => {
300
+ reportInteractive?.();
301
+ onReady?.();
302
+ });
207
303
  if (onFocus) element.on("focus", onFocus);
208
304
  if (onBlur) element.on("blur", onBlur);
209
305
  if (onEscape) element.on("escape", onEscape);
@@ -218,7 +314,7 @@ function createElementComponent(elementType, displayName) {
218
314
  elementRef.current = null;
219
315
  }
220
316
  };
221
- }, [elements]);
317
+ }, [elements, reportInteractive]);
222
318
  return /* @__PURE__ */ jsx3("div", { ref: containerRef, className, id, style });
223
319
  }
224
320
  ElementComponent.displayName = displayName;
@@ -262,7 +358,7 @@ import {
262
358
  import { PaymentAPI as PaymentAPI2 } from "@flopay/js";
263
359
 
264
360
  // src/vault-card-fields.tsx
265
- import { useEffect as useEffect3, useRef as useRef2 } from "react";
361
+ import { useEffect as useEffect3, useRef as useRef3 } from "react";
266
362
  import { jsx as jsx4 } from "react/jsx-runtime";
267
363
  function VaultCardFields({
268
364
  capture,
@@ -275,14 +371,14 @@ function VaultCardFields({
275
371
  onError,
276
372
  onValidation
277
373
  }) {
278
- const containerRef = useRef2(null);
279
- const onReadyRef = useRef2(onReady);
280
- const onErrorRef = useRef2(onError);
281
- const onValidationRef = useRef2(onValidation);
374
+ const containerRef = useRef3(null);
375
+ const onReadyRef = useRef3(onReady);
376
+ const onErrorRef = useRef3(onError);
377
+ const onValidationRef = useRef3(onValidation);
282
378
  onReadyRef.current = onReady;
283
379
  onErrorRef.current = onError;
284
380
  onValidationRef.current = onValidation;
285
- const themeRef = useRef2(theme);
381
+ const themeRef = useRef3(theme);
286
382
  themeRef.current = theme;
287
383
  useEffect3(() => {
288
384
  const el = containerRef.current;
@@ -333,7 +429,7 @@ function VaultCardFields({
333
429
  }
334
430
 
335
431
  // src/split-card-form.tsx
336
- import React7, { forwardRef, useCallback as useCallback2, useContext as useContext3, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef4, useState as useState3 } from "react";
432
+ import React7, { forwardRef, useCallback as useCallback3, useContext as useContext3, useEffect as useEffect5, useImperativeHandle, useMemo as useMemo3, useRef as useRef5, useState as useState3 } from "react";
337
433
 
338
434
  // src/hooks.ts
339
435
  import { useContext as useContext2 } from "react";
@@ -770,10 +866,23 @@ function isInAppBrowser(userAgent) {
770
866
  }
771
867
 
772
868
  // src/direct-paypal-button.tsx
773
- import { useCallback, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState2 } from "react";
869
+ import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState2 } from "react";
774
870
  import { loadScript } from "@paypal/paypal-js";
775
871
  import { PaymentAPI } from "@flopay/js";
776
- import { FloPayError as FloPayError2, normalizeGatewayEnvironment } from "@flopay/shared";
872
+ import { FloPayError as FloPayError2, SDK_VERSION, normalizeGatewayEnvironment } from "@flopay/shared";
873
+
874
+ // src/merchant-callback.ts
875
+ function invokeMerchantCallback(callback) {
876
+ if (!callback) return;
877
+ const reportFailure = (error) => {
878
+ console.error("[FloPay] Merchant callback failed; checkout continued.", error);
879
+ };
880
+ try {
881
+ void Promise.resolve(callback()).catch(reportFailure);
882
+ } catch (error) {
883
+ reportFailure(error);
884
+ }
885
+ }
777
886
 
778
887
  // src/external-method-recovery.ts
779
888
  var EXTERNAL_METHOD_CALLBACK_GRACE_MS = 600;
@@ -831,7 +940,7 @@ var DIRECT_PAYPAL_RECOVERY_ACTION_STYLE = {
831
940
  function buildDirectPayPalRecoveryMessage(popupBlocked) {
832
941
  return popupBlocked ? `${DIRECT_PAYPAL_RECOVERY_MESSAGE} Allow pop-ups for this site, then try again.` : DIRECT_PAYPAL_RECOVERY_MESSAGE;
833
942
  }
834
- function DirectPayPalButton({
943
+ function DirectPayPalButtonImplementation({
835
944
  sessionId,
836
945
  nonce,
837
946
  billingApiUrl,
@@ -851,14 +960,62 @@ function DirectPayPalButton({
851
960
  runBeforeButtonClick,
852
961
  session,
853
962
  existingOrderId,
963
+ telemetry,
964
+ telemetryContext,
854
965
  debug = false
855
966
  }) {
856
- const containerRef = useRef3(null);
857
- const focusTargetRef = useRef3(null);
858
- const pendingProviderFocusRef = useRef3(false);
967
+ const flopay = useFloPay();
968
+ const standaloneTelemetry = useMemo2(() => {
969
+ if (flopay) return null;
970
+ const reporter = createTelemetryBridge({
971
+ billingApiUrl,
972
+ sdkPackage: "@flopay/react",
973
+ sdkVersion: SDK_VERSION,
974
+ enabled: telemetry !== false
975
+ });
976
+ reporter.setCheckoutContext(telemetryContext ?? {});
977
+ reporter.beginCheckout(telemetryContext ?? {});
978
+ return reporter;
979
+ }, [
980
+ billingApiUrl,
981
+ flopay,
982
+ telemetry,
983
+ telemetryContext?.checkoutMode,
984
+ telemetryContext?.layout
985
+ ]);
986
+ const floPayTelemetry = useMemo2(() => getFloPayTelemetryBridge(flopay), [flopay]);
987
+ useEffect4(() => () => {
988
+ if (!standaloneTelemetry) return;
989
+ void standaloneTelemetry.flush().catch(() => {
990
+ }).finally(() => standaloneTelemetry.destroy());
991
+ }, [standaloneTelemetry]);
992
+ const telemetrySource = useMemo2(() => ({
993
+ error: (input) => {
994
+ if (floPayTelemetry) floPayTelemetry.error(input);
995
+ else standaloneTelemetry?.error(input);
996
+ },
997
+ log: (input) => {
998
+ if (floPayTelemetry) floPayTelemetry.log(input);
999
+ else standaloneTelemetry?.log(input);
1000
+ },
1001
+ performance: (input) => {
1002
+ if (floPayTelemetry) floPayTelemetry.performance(input);
1003
+ else standaloneTelemetry?.performance(input);
1004
+ },
1005
+ terminal: (input) => {
1006
+ if (floPayTelemetry) floPayTelemetry.terminal(input);
1007
+ else standaloneTelemetry?.terminal(input);
1008
+ },
1009
+ startTiming: () => floPayTelemetry?.now() ?? standaloneTelemetry?.now() ?? 0,
1010
+ elapsed: (startedAt) => floPayTelemetry?.elapsed(startedAt) ?? Math.max(0, (standaloneTelemetry?.now() ?? startedAt) - startedAt)
1011
+ }), [floPayTelemetry, standaloneTelemetry]);
1012
+ const containerRef = useRef4(null);
1013
+ const providerStartedAt = useRef4(0);
1014
+ const focusTargetRef = useRef4(null);
1015
+ const pendingProviderFocusRef = useRef4(false);
859
1016
  const [ready, setReady] = useState2(false);
860
1017
  const [renderGeneration, setRenderGeneration] = useState2(0);
861
- const activeRenderGenerationRef = useRef3(0);
1018
+ const activeRenderGenerationRef = useRef4(0);
862
1019
  const [showRetryFocusTarget, setShowRetryFocusTarget] = useState2(false);
863
1020
  const [failed, setFailed] = useState2(false);
864
1021
  const [submitting, setSubmitting] = useState2(false);
@@ -868,23 +1025,23 @@ function DirectPayPalButton({
868
1025
  if (!debug) return;
869
1026
  setDebugLines((prev) => [...prev, `${(/* @__PURE__ */ new Date()).toISOString().slice(11, 23)} ${line}`]);
870
1027
  };
871
- const onTokenizedBodyRef = useRef3(onTokenizedBody);
872
- const onCompleteRef = useRef3(onComplete);
873
- const onErrorChangeRef = useRef3(onErrorChange);
874
- const onDeclineRef = useRef3(onDecline);
875
- const onTechnicalFailureRef = useRef3(onTechnicalFailure);
876
- const onButtonClickRef = useRef3(onButtonClick);
877
- const onLoadStateChangeRef = useRef3(onLoadStateChange);
878
- const runBeforeButtonClickRef = useRef3(runBeforeButtonClick);
879
- const sessionRef = useRef3(session);
880
- const emailRef = useRef3(email);
881
- const nonceRef = useRef3(nonce);
882
- const beforeClickRef = useRef3(null);
883
- const attemptGenerationRef = useRef3(0);
884
- const attemptRef = useRef3(null);
885
- const invalidatedAttemptGenerationRef = useRef3(null);
886
- const attemptContextBySurfaceRef = useRef3(/* @__PURE__ */ new Map());
887
- const approvalContextByTokenRef = useRef3(/* @__PURE__ */ new Map());
1028
+ const onTokenizedBodyRef = useRef4(onTokenizedBody);
1029
+ const onCompleteRef = useRef4(onComplete);
1030
+ const onErrorChangeRef = useRef4(onErrorChange);
1031
+ const onDeclineRef = useRef4(onDecline);
1032
+ const onTechnicalFailureRef = useRef4(onTechnicalFailure);
1033
+ const onButtonClickRef = useRef4(onButtonClick);
1034
+ const onLoadStateChangeRef = useRef4(onLoadStateChange);
1035
+ const runBeforeButtonClickRef = useRef4(runBeforeButtonClick);
1036
+ const sessionRef = useRef4(session);
1037
+ const emailRef = useRef4(email);
1038
+ const nonceRef = useRef4(nonce);
1039
+ const beforeClickRef = useRef4(null);
1040
+ const attemptGenerationRef = useRef4(0);
1041
+ const attemptRef = useRef4(null);
1042
+ const invalidatedAttemptGenerationRef = useRef4(null);
1043
+ const attemptContextBySurfaceRef = useRef4(/* @__PURE__ */ new Map());
1044
+ const approvalContextByTokenRef = useRef4(/* @__PURE__ */ new Map());
888
1045
  useEffect4(() => {
889
1046
  onTokenizedBodyRef.current = onTokenizedBody;
890
1047
  }, [onTokenizedBody]);
@@ -924,16 +1081,16 @@ function DirectPayPalButton({
924
1081
  useEffect4(() => {
925
1082
  if (showRetryFocusTarget) focusTargetRef.current?.focus();
926
1083
  }, [showRetryFocusTarget, renderGeneration]);
927
- const focusRetryTarget = useCallback(() => {
1084
+ const focusRetryTarget = useCallback2(() => {
928
1085
  window.setTimeout(() => {
929
1086
  focusTargetRef.current?.focus();
930
1087
  }, 0);
931
1088
  }, []);
932
- const remountPayPalButtons = useCallback(() => {
1089
+ const remountPayPalButtons = useCallback2(() => {
933
1090
  setReady(false);
934
1091
  setRenderGeneration((current) => current + 1);
935
1092
  }, []);
936
- const focusPayPalSurface = useCallback(() => {
1093
+ const focusPayPalSurface = useCallback2(() => {
937
1094
  setShowRetryFocusTarget(false);
938
1095
  const target = containerRef.current?.querySelector(
939
1096
  'iframe, button, [tabindex]:not([tabindex="-1"])'
@@ -945,7 +1102,7 @@ function DirectPayPalButton({
945
1102
  pendingProviderFocusRef.current = false;
946
1103
  focusPayPalSurface();
947
1104
  }, [focusPayPalSurface, ready, renderGeneration]);
948
- const notifyTechnicalFailure = useCallback((err, options) => {
1105
+ const notifyTechnicalFailure = useCallback2((err, options) => {
949
1106
  const handler = onTechnicalFailureRef.current;
950
1107
  if (handler) {
951
1108
  handler("paypal", err, options);
@@ -1042,7 +1199,7 @@ function DirectPayPalButton({
1042
1199
  };
1043
1200
  }, []);
1044
1201
  useEffect4(() => {
1045
- onLoadStateChangeRef.current?.(ready && !failed);
1202
+ invokeMerchantCallback(() => onLoadStateChangeRef.current?.(ready && !failed));
1046
1203
  }, [ready, failed]);
1047
1204
  const normalizedEnv = normalizeGatewayEnvironment(environment);
1048
1205
  useEffect4(() => {
@@ -1053,6 +1210,13 @@ function DirectPayPalButton({
1053
1210
  if (!clientId) {
1054
1211
  appendDebug("FAIL: clientId empty \u2014 gateway misconfigured");
1055
1212
  setFailed(true);
1213
+ telemetrySource.error({
1214
+ errorCode: "CONFIGURATION_INVALID",
1215
+ stage: "provider_load",
1216
+ provider: "paypal",
1217
+ paymentMethodCategory: "paypal",
1218
+ requestCategory: "provider_sdk"
1219
+ });
1056
1220
  console.error("[FloPay] DirectPayPal: clientId empty \u2014 gateway misconfigured");
1057
1221
  return;
1058
1222
  }
@@ -1060,8 +1224,33 @@ function DirectPayPalButton({
1060
1224
  appendDebug("FAIL: containerRef not attached");
1061
1225
  return;
1062
1226
  }
1227
+ providerStartedAt.current = telemetrySource.startTiming();
1228
+ telemetrySource.log({
1229
+ name: "provider.load.started",
1230
+ stage: "provider_load",
1231
+ provider: "paypal",
1232
+ paymentMethodCategory: "paypal"
1233
+ });
1063
1234
  let cancelled = false;
1064
1235
  let activeButtons = null;
1236
+ let overlayStartedAt = null;
1237
+ const finishOverlay = () => {
1238
+ if (overlayStartedAt === null) return;
1239
+ telemetrySource.log({
1240
+ name: "provider.overlay.returned",
1241
+ stage: "overlay_return",
1242
+ provider: "paypal",
1243
+ paymentMethodCategory: "paypal"
1244
+ });
1245
+ telemetrySource.performance({
1246
+ stage: "overlay_return",
1247
+ durationMs: telemetrySource.elapsed(overlayStartedAt),
1248
+ durationMode: "buyer",
1249
+ provider: "paypal",
1250
+ paymentMethodCategory: "paypal"
1251
+ });
1252
+ overlayStartedAt = null;
1253
+ };
1065
1254
  let rendered = false;
1066
1255
  const container = containerRef.current;
1067
1256
  let containerObserver = null;
@@ -1165,7 +1354,7 @@ function DirectPayPalButton({
1165
1354
  if (cancelled) return;
1166
1355
  if (isZoidLifecycleMessage(message)) return;
1167
1356
  const friendly = applyFriendlyMessageOverride(message) ?? message;
1168
- onErrorChangeRef.current?.(friendly);
1357
+ invokeMerchantCallback(() => onErrorChangeRef.current?.(friendly));
1169
1358
  };
1170
1359
  const markRenderFailed = (message) => {
1171
1360
  if (cancelled) return;
@@ -1174,24 +1363,55 @@ function DirectPayPalButton({
1174
1363
  return;
1175
1364
  }
1176
1365
  setFailed(true);
1366
+ if (!message.includes("paypal_ineligible")) {
1367
+ telemetrySource.error({
1368
+ errorCode: "PROVIDER_LOAD_FAILED",
1369
+ stage: "provider_load",
1370
+ provider: "paypal",
1371
+ paymentMethodCategory: "paypal",
1372
+ requestCategory: "provider_sdk"
1373
+ });
1374
+ }
1177
1375
  console.error("[FloPay] DirectPayPal load/render failure:", message);
1178
1376
  };
1179
1377
  setFailed(false);
1180
1378
  const dispatchTokenizedBody = async (body, prepared = beforeClickRef.current) => {
1181
1379
  const effectiveSessionId = prepared?.sessionId ?? sessionId;
1182
1380
  if (onTokenizedBodyRef.current) {
1183
- onTokenizedBodyRef.current(body, {
1381
+ await onTokenizedBodyRef.current(body, {
1184
1382
  sessionId: effectiveSessionId,
1185
1383
  accountPatch: prepared?.accountPatch,
1186
1384
  nonce: prepared?.nonce
1187
1385
  });
1188
1386
  return;
1189
1387
  }
1388
+ const processingStartedAt = telemetrySource.startTiming();
1389
+ telemetrySource.log({
1390
+ name: "payment.processing.started",
1391
+ stage: "processing",
1392
+ provider: "paypal",
1393
+ paymentMethodCategory: "paypal"
1394
+ });
1395
+ const finishProcessing = () => {
1396
+ telemetrySource.log({
1397
+ name: "payment.processing.completed",
1398
+ stage: "processing",
1399
+ provider: "paypal",
1400
+ paymentMethodCategory: "paypal"
1401
+ });
1402
+ telemetrySource.performance({
1403
+ stage: "processing",
1404
+ durationMs: telemetrySource.elapsed(processingStartedAt),
1405
+ durationMode: "machine",
1406
+ provider: "paypal",
1407
+ paymentMethodCategory: "paypal"
1408
+ });
1409
+ };
1190
1410
  try {
1191
1411
  const currentSession = sessionRef.current;
1192
1412
  const currentEmail = prepared?.accountPatch?.email ?? emailRef.current;
1193
1413
  const effectiveUserId = prepared?.accountPatch?.userId ?? currentSession?.customer?.id ?? currentSession?.accountData?.userId ?? "";
1194
- const api = new PaymentAPI(baseUrl);
1414
+ const api = new PaymentAPI(baseUrl, { telemetry: false });
1195
1415
  const response = await api.processPayment(
1196
1416
  effectiveUserId,
1197
1417
  {
@@ -1209,23 +1429,57 @@ function DirectPayPalButton({
1209
1429
  }
1210
1430
  );
1211
1431
  if (response.ok) {
1212
- onCompleteRef.current?.({ status: "succeeded", checkoutMethod: "paypal" });
1432
+ finishProcessing();
1433
+ telemetrySource.terminal({
1434
+ outcome: "payment_succeeded",
1435
+ provider: "paypal",
1436
+ paymentMethodCategory: "paypal"
1437
+ });
1438
+ } else {
1439
+ const json = await response.json().catch(() => null);
1440
+ const rawMessage = json?.["message"] ?? "PayPal payment failed.";
1441
+ const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
1442
+ finishProcessing();
1443
+ if (typeof json?.["declineCode"] === "string") {
1444
+ telemetrySource.terminal({
1445
+ outcome: "payment_declined",
1446
+ provider: "paypal",
1447
+ paymentMethodCategory: "paypal"
1448
+ });
1449
+ } else {
1450
+ telemetrySource.error({
1451
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1452
+ stage: "processing",
1453
+ provider: "paypal",
1454
+ paymentMethodCategory: "paypal",
1455
+ requestCategory: "process_payment",
1456
+ statusClass: response.status >= 500 ? "5xx" : response.status >= 400 ? "4xx" : response.status >= 300 ? "3xx" : "unknown"
1457
+ });
1458
+ }
1459
+ forwardError(message);
1460
+ invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
1461
+ code: json?.["code"],
1462
+ declineCode: json?.["declineCode"]
1463
+ })));
1213
1464
  return;
1214
1465
  }
1215
- const json = await response.json().catch(() => null);
1216
- const rawMessage = json?.["message"] ?? "PayPal payment failed.";
1217
- const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
1218
- forwardError(message);
1219
- onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
1220
- code: json?.["code"],
1221
- declineCode: json?.["declineCode"]
1222
- }));
1223
1466
  } catch (err) {
1467
+ finishProcessing();
1468
+ telemetrySource.error({
1469
+ errorCode: "NETWORK_REQUEST_FAILED",
1470
+ stage: "processing",
1471
+ provider: "paypal",
1472
+ paymentMethodCategory: "paypal",
1473
+ requestCategory: "process_payment",
1474
+ statusClass: "network_error"
1475
+ });
1224
1476
  const rawMessage = err instanceof Error ? err.message : "PayPal payment failed.";
1225
1477
  const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
1226
1478
  forwardError(message);
1227
- onDeclineRef.current?.(buildDeclineEvent("paypal", message));
1479
+ invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message)));
1480
+ return;
1228
1481
  }
1482
+ invokeMerchantCallback(() => onCompleteRef.current?.({ status: "succeeded", checkoutMethod: "paypal" }));
1229
1483
  };
1230
1484
  const createPaypalIntent = async (fallbackMessage) => {
1231
1485
  const prepared = beforeClickRef.current;
@@ -1234,6 +1488,13 @@ function DirectPayPalButton({
1234
1488
  const intentHeaders = { "Content-Type": "application/json" };
1235
1489
  const currentNonce = prepared?.nonce ?? nonceRef.current;
1236
1490
  if (currentNonce) intentHeaders["x-checkout-session-token"] = currentNonce;
1491
+ telemetrySource.log({
1492
+ name: "payment.intent.started",
1493
+ stage: "processing",
1494
+ provider: "paypal",
1495
+ paymentMethodCategory: "paypal",
1496
+ requestCategory: "intent_create"
1497
+ });
1237
1498
  const response = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
1238
1499
  method: "POST",
1239
1500
  headers: intentHeaders,
@@ -1252,6 +1513,14 @@ function DirectPayPalButton({
1252
1513
  if (!id) {
1253
1514
  throw new Error(fallbackMessage);
1254
1515
  }
1516
+ telemetrySource.log({
1517
+ name: "payment.intent.completed",
1518
+ stage: "processing",
1519
+ provider: "paypal",
1520
+ paymentMethodCategory: "paypal",
1521
+ requestCategory: "intent_create",
1522
+ statusClass: "2xx"
1523
+ });
1255
1524
  return id;
1256
1525
  };
1257
1526
  const effectRenderGeneration = renderGeneration;
@@ -1286,6 +1555,12 @@ function DirectPayPalButton({
1286
1555
  if (!cancelled && !paypal?.Buttons) appendDebug("FAIL: namespace missing Buttons factory");
1287
1556
  return;
1288
1557
  }
1558
+ telemetrySource.log({
1559
+ name: "provider.availability.checked",
1560
+ stage: "provider_ready",
1561
+ provider: "paypal",
1562
+ paymentMethodCategory: "paypal"
1563
+ });
1289
1564
  const handleApprove = async (data) => {
1290
1565
  if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
1291
1566
  const token = data.subscriptionID ?? data.orderID ?? "";
@@ -1294,9 +1569,10 @@ function DirectPayPalButton({
1294
1569
  if (!finishAttempt(context.generation)) return;
1295
1570
  approvalContextByTokenRef.current.delete(token);
1296
1571
  attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1572
+ finishOverlay();
1297
1573
  try {
1298
1574
  setSubmitting(true);
1299
- onErrorChangeRef.current?.(null);
1575
+ invokeMerchantCallback(() => onErrorChangeRef.current?.(null));
1300
1576
  if (!token) {
1301
1577
  throw new FloPayError2(
1302
1578
  "PayPal did not return an approval token.",
@@ -1346,7 +1622,7 @@ function DirectPayPalButton({
1346
1622
  return;
1347
1623
  }
1348
1624
  }
1349
- onButtonClickRef.current?.("paypal");
1625
+ invokeMerchantCallback(() => onButtonClickRef.current?.("paypal"));
1350
1626
  const generation = startAttempt();
1351
1627
  const context = {
1352
1628
  generation,
@@ -1356,6 +1632,25 @@ function DirectPayPalButton({
1356
1632
  beforeClickRef.current = context;
1357
1633
  attemptContextBySurfaceRef.current.set(effectRenderGeneration, context);
1358
1634
  await actions.resolve();
1635
+ telemetrySource.log({
1636
+ name: "payment.method.selected",
1637
+ stage: "processing",
1638
+ provider: "paypal",
1639
+ paymentMethodCategory: "paypal"
1640
+ });
1641
+ telemetrySource.log({
1642
+ name: "provider.popup.opened",
1643
+ stage: "overlay_open",
1644
+ provider: "paypal",
1645
+ paymentMethodCategory: "paypal"
1646
+ });
1647
+ telemetrySource.log({
1648
+ name: "provider.overlay.opened",
1649
+ stage: "overlay_open",
1650
+ provider: "paypal",
1651
+ paymentMethodCategory: "paypal"
1652
+ });
1653
+ overlayStartedAt = telemetrySource.startTiming();
1359
1654
  },
1360
1655
  // When the SDK is already holding an order/subscription id from a
1361
1656
  // prior backend round-trip (the `paypal_direct_required` retry
@@ -1368,6 +1663,12 @@ function DirectPayPalButton({
1368
1663
  const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
1369
1664
  if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1370
1665
  beforeClickRef.current = null;
1666
+ finishOverlay();
1667
+ telemetrySource.terminal({
1668
+ outcome: "payment_cancelled",
1669
+ provider: "paypal",
1670
+ paymentMethodCategory: "paypal"
1671
+ });
1371
1672
  pendingProviderFocusRef.current = true;
1372
1673
  invalidateAttempt({ generation: context?.generation, remount: true, focus: false });
1373
1674
  },
@@ -1383,6 +1684,15 @@ function DirectPayPalButton({
1383
1684
  finishAttempt(context?.generation);
1384
1685
  return;
1385
1686
  }
1687
+ finishOverlay();
1688
+ const lower = message.toLowerCase();
1689
+ telemetrySource.error({
1690
+ errorCode: lower.includes("popup") && lower.includes("block") ? "POPUP_BLOCKED" : "PROVIDER_RUNTIME_FAILED",
1691
+ stage: "provider_ready",
1692
+ provider: "paypal",
1693
+ paymentMethodCategory: "paypal",
1694
+ requestCategory: "provider_sdk"
1695
+ });
1386
1696
  if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1387
1697
  beforeClickRef.current = null;
1388
1698
  invalidateAttempt({ generation: context?.generation, remount: true, showRetry: true, focus: true });
@@ -1394,6 +1704,12 @@ function DirectPayPalButton({
1394
1704
  });
1395
1705
  const eligible = buttons.isEligible();
1396
1706
  appendDebug(`isEligible=${eligible}`);
1707
+ telemetrySource.log({
1708
+ name: "provider.eligibility.checked",
1709
+ stage: "provider_ready",
1710
+ provider: "paypal",
1711
+ paymentMethodCategory: "paypal"
1712
+ });
1397
1713
  if (!eligible) {
1398
1714
  setReady(false);
1399
1715
  markRenderFailed(
@@ -1465,6 +1781,19 @@ function DirectPayPalButton({
1465
1781
  activeButtons = typedButtons;
1466
1782
  rendered = true;
1467
1783
  setReady(true);
1784
+ telemetrySource.log({
1785
+ name: "provider.ready",
1786
+ stage: "provider_ready",
1787
+ provider: "paypal",
1788
+ paymentMethodCategory: "paypal"
1789
+ });
1790
+ telemetrySource.performance({
1791
+ stage: "provider_ready",
1792
+ durationMs: telemetrySource.elapsed(providerStartedAt.current),
1793
+ durationMode: "machine",
1794
+ provider: "paypal",
1795
+ paymentMethodCategory: "paypal"
1796
+ });
1468
1797
  }).catch((err) => {
1469
1798
  const message = err instanceof Error ? err.message : "PayPal failed to render.";
1470
1799
  appendDebug(`render:rejected msg=${message.slice(0, 120)}`);
@@ -1486,7 +1815,7 @@ function DirectPayPalButton({
1486
1815
  });
1487
1816
  }
1488
1817
  };
1489
- }, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration]);
1818
+ }, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration, telemetrySource]);
1490
1819
  const debugPanel = debug ? /* @__PURE__ */ jsxs3(
1491
1820
  "pre",
1492
1821
  {
@@ -1567,6 +1896,12 @@ ${debugLines.join("\n")}`
1567
1896
  ] })
1568
1897
  );
1569
1898
  }
1899
+ function DirectPayPalButton(props) {
1900
+ return /* @__PURE__ */ jsx6(DirectPayPalButtonImplementation, { ...props });
1901
+ }
1902
+ function InstrumentedDirectPayPalButton(props) {
1903
+ return /* @__PURE__ */ jsx6(DirectPayPalButtonImplementation, { ...props });
1904
+ }
1570
1905
 
1571
1906
  // src/split-card-form.tsx
1572
1907
  import { FloPayError as FloPayError3, isSetupIntentClientSecret as isSetupIntentClientSecret2, resolveTheme } from "@flopay/shared";
@@ -1685,17 +2020,17 @@ function buildExternalMethodRecoveryMessage(method, popupBlocked) {
1685
2020
  return popupBlocked ? `${base} Allow pop-ups for this site, then try again.` : base;
1686
2021
  }
1687
2022
  function useExternalAttemptReconciliation(onMissingTerminal) {
1688
- const generationRef = useRef4(0);
1689
- const invalidatedGenerationRef = useRef4(null);
1690
- const attemptRef = useRef4(null);
1691
- const clearAttemptTimer = useCallback2((targetAttempt = attemptRef.current) => {
2023
+ const generationRef = useRef5(0);
2024
+ const invalidatedGenerationRef = useRef5(null);
2025
+ const attemptRef = useRef5(null);
2026
+ const clearAttemptTimer = useCallback3((targetAttempt = attemptRef.current) => {
1692
2027
  const attempt = targetAttempt;
1693
2028
  if (attempt?.timer) {
1694
2029
  clearTimeout(attempt.timer);
1695
2030
  attempt.timer = null;
1696
2031
  }
1697
2032
  }, []);
1698
- const armAttemptTimer = useCallback2((attempt, delayMs) => {
2033
+ const armAttemptTimer = useCallback3((attempt, delayMs) => {
1699
2034
  if (typeof document !== "undefined" && document.visibilityState !== "visible") return;
1700
2035
  clearAttemptTimer(attempt);
1701
2036
  attempt.timer = setTimeout(() => {
@@ -1710,11 +2045,11 @@ function useExternalAttemptReconciliation(onMissingTerminal) {
1710
2045
  );
1711
2046
  }, delayMs);
1712
2047
  }, [clearAttemptTimer, onMissingTerminal]);
1713
- const armRecoveryTimer = useCallback2((attempt) => {
2048
+ const armRecoveryTimer = useCallback3((attempt) => {
1714
2049
  if (!attempt.yieldedControl) return;
1715
2050
  armAttemptTimer(attempt, EXTERNAL_METHOD_CALLBACK_GRACE_MS);
1716
2051
  }, [armAttemptTimer]);
1717
- const startAttempt = useCallback2((method) => {
2052
+ const startAttempt = useCallback3((method) => {
1718
2053
  clearAttemptTimer();
1719
2054
  const generation = generationRef.current + 1;
1720
2055
  generationRef.current = generation;
@@ -1723,7 +2058,7 @@ function useExternalAttemptReconciliation(onMissingTerminal) {
1723
2058
  attemptRef.current = attempt;
1724
2059
  return generation;
1725
2060
  }, [clearAttemptTimer]);
1726
- const finishAttempt = useCallback2((generation) => {
2061
+ const finishAttempt = useCallback3((generation) => {
1727
2062
  const attempt = attemptRef.current;
1728
2063
  if (typeof generation === "number" && attempt?.generation !== generation) return false;
1729
2064
  clearAttemptTimer(attempt);
@@ -1733,7 +2068,7 @@ function useExternalAttemptReconciliation(onMissingTerminal) {
1733
2068
  }
1734
2069
  return true;
1735
2070
  }, [clearAttemptTimer]);
1736
- const invalidateAttempt = useCallback2((generation) => {
2071
+ const invalidateAttempt = useCallback3((generation) => {
1737
2072
  const attempt = attemptRef.current;
1738
2073
  const targetGeneration = generation ?? attempt?.generation ?? generationRef.current;
1739
2074
  if (!generation || attempt?.generation === generation) {
@@ -1742,20 +2077,20 @@ function useExternalAttemptReconciliation(onMissingTerminal) {
1742
2077
  }
1743
2078
  invalidatedGenerationRef.current = targetGeneration;
1744
2079
  }, [clearAttemptTimer]);
1745
- const isAttemptInvalidated = useCallback2(
2080
+ const isAttemptInvalidated = useCallback3(
1746
2081
  (generation) => invalidatedGenerationRef.current === (generation ?? generationRef.current),
1747
2082
  []
1748
2083
  );
1749
- const isAttemptCurrent = useCallback2(
2084
+ const isAttemptCurrent = useCallback3(
1750
2085
  (generation) => attemptRef.current?.generation === generation && invalidatedGenerationRef.current !== generation,
1751
2086
  []
1752
2087
  );
1753
- const scheduleRecoveryIfReturned = useCallback2(() => {
2088
+ const scheduleRecoveryIfReturned = useCallback3(() => {
1754
2089
  const attempt = attemptRef.current;
1755
2090
  if (!attempt) return;
1756
2091
  armRecoveryTimer(attempt);
1757
2092
  }, [armRecoveryTimer]);
1758
- const markAttemptYieldedControl = useCallback2(() => {
2093
+ const markAttemptYieldedControl = useCallback3(() => {
1759
2094
  const attempt = attemptRef.current;
1760
2095
  if (!attempt) return;
1761
2096
  attempt.yieldedControl = true;
@@ -1883,6 +2218,7 @@ function PayPalButtonInner({
1883
2218
  onLoadStateChange,
1884
2219
  placeholderBorderRadius
1885
2220
  }) {
2221
+ const flopay = useFloPay();
1886
2222
  const stripe = useStripeRaw();
1887
2223
  const elements = useStripeElements();
1888
2224
  const [loadState, setLoadState] = useState3("loading");
@@ -1890,13 +2226,13 @@ function PayPalButtonInner({
1890
2226
  onLoadStateChange?.(loadState);
1891
2227
  }, [loadState, onLoadStateChange]);
1892
2228
  const [submitting, setSubmitting] = useState3(false);
1893
- const paypalResumeAttempted = useRef4(false);
1894
- const focusTargetRef = useRef4(null);
1895
- const recoveryActionRef = useRef4(null);
2229
+ const paypalResumeAttempted = useRef5(false);
2230
+ const focusTargetRef = useRef5(null);
2231
+ const recoveryActionRef = useRef5(null);
1896
2232
  const [surfaceKey, setSurfaceKey] = useState3(0);
1897
2233
  const [showRecoveryAction, setShowRecoveryAction] = useState3(false);
1898
- const pendingProviderFocusRef = useRef4(false);
1899
- const attemptContextBySurfaceRef = useRef4(/* @__PURE__ */ new Map());
2234
+ const pendingProviderFocusRef = useRef5(false);
2235
+ const attemptContextBySurfaceRef = useRef5(/* @__PURE__ */ new Map());
1900
2236
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
1901
2237
  const {
1902
2238
  startAttempt,
@@ -1915,16 +2251,16 @@ function PayPalButtonInner({
1915
2251
  useEffect5(() => {
1916
2252
  if (showRecoveryAction) recoveryActionRef.current?.focus();
1917
2253
  }, [showRecoveryAction, surfaceKey]);
1918
- const resetSurface = useCallback2(() => {
2254
+ const resetSurface = useCallback3(() => {
1919
2255
  setSurfaceKey((key) => key + 1);
1920
2256
  }, []);
1921
- const recoverTechnicalFailure = useCallback2((err, code, generation) => {
2257
+ const recoverTechnicalFailure = useCallback3((err, code, generation) => {
1922
2258
  invalidateAttempt(generation);
1923
2259
  onTechnicalFailure?.("paypal", err, { code, popupBlocked: isPopupBlockedError(err) });
1924
2260
  setShowRecoveryAction(true);
1925
2261
  resetSurface();
1926
2262
  }, [invalidateAttempt, onTechnicalFailure, resetSurface]);
1927
- const focusProviderSurface = useCallback2(() => {
2263
+ const focusProviderSurface = useCallback3(() => {
1928
2264
  window.setTimeout(() => {
1929
2265
  const target = focusTargetRef.current?.querySelector("iframe");
1930
2266
  (target ?? focusTargetRef.current)?.focus();
@@ -1935,7 +2271,7 @@ function PayPalButtonInner({
1935
2271
  pendingProviderFocusRef.current = false;
1936
2272
  focusProviderSurface();
1937
2273
  }, [focusProviderSurface, surfaceKey]);
1938
- const handleRecoveryActionClick = useCallback2(() => {
2274
+ const handleRecoveryActionClick = useCallback3(() => {
1939
2275
  setShowRecoveryAction(false);
1940
2276
  onErrorChange?.(null);
1941
2277
  focusProviderSurface();
@@ -2006,7 +2342,7 @@ function PayPalButtonInner({
2006
2342
  }
2007
2343
  })();
2008
2344
  }, [stripe, onTokenizedBody, onErrorChange, onDecline]);
2009
- const handlePayPalClick = useCallback2(async (event) => {
2345
+ const handlePayPalClick = useCallback3(async (event) => {
2010
2346
  if (isProcessing || submitting) {
2011
2347
  event.reject();
2012
2348
  return;
@@ -2028,7 +2364,7 @@ function PayPalButtonInner({
2028
2364
  onButtonClick?.("paypal");
2029
2365
  event.resolve();
2030
2366
  }, [attemptContextBySurfaceRef, isProcessing, onButtonClick, runBeforeButtonClick, startAttempt, submitting, surfaceKey]);
2031
- const handlePayPalConfirm = useCallback2(async (event) => {
2367
+ const handlePayPalConfirm = useCallback3(async (event) => {
2032
2368
  if (!stripe || !elements) return;
2033
2369
  const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
2034
2370
  if (attemptContext && !isAttemptCurrent(attemptContext.generation)) {
@@ -2197,6 +2533,11 @@ function PayPalButtonInner({
2197
2533
  onCancel: () => {
2198
2534
  const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
2199
2535
  attemptContextBySurfaceRef.current.delete(surfaceKey);
2536
+ getFloPayTelemetryBridge(flopay)?.terminal({
2537
+ outcome: "payment_cancelled",
2538
+ provider: "paypal",
2539
+ paymentMethodCategory: "paypal"
2540
+ });
2200
2541
  invalidateAttempt(attemptContext?.generation);
2201
2542
  setShowRecoveryAction(false);
2202
2543
  pendingProviderFocusRef.current = true;
@@ -2240,6 +2581,7 @@ function WalletButtonInner({
2240
2581
  onLoadStateChange,
2241
2582
  placeholderBorderRadius
2242
2583
  }) {
2584
+ const flopay = useFloPay();
2243
2585
  const stripe = useStripeRaw();
2244
2586
  const elements = useStripeElements();
2245
2587
  const [loadState, setLoadState] = useState3("loading");
@@ -2248,15 +2590,15 @@ function WalletButtonInner({
2248
2590
  }, [loadState, onLoadStateChange]);
2249
2591
  const [submitting, setSubmitting] = useState3(false);
2250
2592
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
2251
- const focusTargetRef = useRef4(null);
2252
- const recoveryActionRef = useRef4(null);
2593
+ const focusTargetRef = useRef5(null);
2594
+ const recoveryActionRef = useRef5(null);
2253
2595
  const [surfaceKey, setSurfaceKey] = useState3(0);
2254
2596
  const [showRecoveryAction, setShowRecoveryAction] = useState3(false);
2255
2597
  const [recoveryActionMethod, setRecoveryActionMethod] = useState3("google_pay");
2256
- const pendingProviderFocusRef = useRef4(false);
2257
- const lastWalletProviderMethodRef = useRef4("google_pay");
2258
- const lastWalletMethodRef = useRef4("google_pay");
2259
- const attemptContextBySurfaceRef = useRef4(/* @__PURE__ */ new Map());
2598
+ const pendingProviderFocusRef = useRef5(false);
2599
+ const lastWalletProviderMethodRef = useRef5("google_pay");
2600
+ const lastWalletMethodRef = useRef5("google_pay");
2601
+ const attemptContextBySurfaceRef = useRef5(/* @__PURE__ */ new Map());
2260
2602
  const {
2261
2603
  startAttempt,
2262
2604
  finishAttempt,
@@ -2275,17 +2617,17 @@ function WalletButtonInner({
2275
2617
  useEffect5(() => {
2276
2618
  if (showRecoveryAction) recoveryActionRef.current?.focus();
2277
2619
  }, [showRecoveryAction, surfaceKey]);
2278
- const resetSurface = useCallback2(() => {
2620
+ const resetSurface = useCallback3(() => {
2279
2621
  setSurfaceKey((key) => key + 1);
2280
2622
  }, []);
2281
- const recoverTechnicalFailure = useCallback2((method, err, code, generation) => {
2623
+ const recoverTechnicalFailure = useCallback3((method, err, code, generation) => {
2282
2624
  invalidateAttempt(generation);
2283
2625
  onTechnicalFailure?.(method, err, { code, popupBlocked: isPopupBlockedError(err) });
2284
2626
  setRecoveryActionMethod(method);
2285
2627
  setShowRecoveryAction(true);
2286
2628
  resetSurface();
2287
2629
  }, [invalidateAttempt, onTechnicalFailure, resetSurface]);
2288
- const focusProviderSurface = useCallback2(() => {
2630
+ const focusProviderSurface = useCallback3(() => {
2289
2631
  window.setTimeout(() => {
2290
2632
  const target = focusTargetRef.current?.querySelector("iframe");
2291
2633
  (target ?? focusTargetRef.current)?.focus();
@@ -2296,12 +2638,12 @@ function WalletButtonInner({
2296
2638
  pendingProviderFocusRef.current = false;
2297
2639
  focusProviderSurface();
2298
2640
  }, [focusProviderSurface, surfaceKey]);
2299
- const handleRecoveryActionClick = useCallback2(() => {
2641
+ const handleRecoveryActionClick = useCallback3(() => {
2300
2642
  setShowRecoveryAction(false);
2301
2643
  onErrorChange?.(null);
2302
2644
  focusProviderSurface();
2303
2645
  }, [focusProviderSurface, onErrorChange]);
2304
- const handleWalletConfirm = useCallback2(
2646
+ const handleWalletConfirm = useCallback3(
2305
2647
  async (event) => {
2306
2648
  if (!stripe || !elements) return;
2307
2649
  const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
@@ -2461,6 +2803,13 @@ function WalletButtonInner({
2461
2803
  },
2462
2804
  onLoadError: (_event) => {
2463
2805
  setLoadState("load_error");
2806
+ getFloPayTelemetryBridge(flopay)?.error({
2807
+ errorCode: "PROVIDER_LOAD_FAILED",
2808
+ stage: "provider_load",
2809
+ provider: "stripe",
2810
+ paymentMethodCategory: "wallet",
2811
+ requestCategory: "provider_sdk"
2812
+ });
2464
2813
  },
2465
2814
  onClick: async (event) => {
2466
2815
  lastWalletProviderMethodRef.current = event.expressPaymentType;
@@ -2487,6 +2836,11 @@ function WalletButtonInner({
2487
2836
  onCancel: () => {
2488
2837
  const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
2489
2838
  attemptContextBySurfaceRef.current.delete(surfaceKey);
2839
+ getFloPayTelemetryBridge(flopay)?.terminal({
2840
+ outcome: "payment_cancelled",
2841
+ provider: "stripe",
2842
+ paymentMethodCategory: "wallet"
2843
+ });
2490
2844
  invalidateAttempt(attemptContext?.generation);
2491
2845
  setShowRecoveryAction(false);
2492
2846
  pendingProviderFocusRef.current = true;
@@ -2657,14 +3011,15 @@ function StripeMethodInlineForm({
2657
3011
  submitButtonStyle,
2658
3012
  errorText
2659
3013
  }) {
3014
+ const flopay = useFloPay();
2660
3015
  const stripe = useStripeRaw();
2661
3016
  const elements = useStripeElements();
2662
3017
  const [submitting, setSubmitting] = useState3(false);
2663
- const submittingRef = useRef4(false);
3018
+ const submittingRef = useRef5(false);
2664
3019
  const [isMethodComplete, setIsMethodComplete] = useState3(false);
2665
3020
  const [loadState, setLoadState] = useState3("loading");
2666
3021
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
2667
- const handlePay = useCallback2(async () => {
3022
+ const handlePay = useCallback3(async () => {
2668
3023
  if (!stripe || !elements || isProcessing || submittingRef.current || !isMethodComplete) return;
2669
3024
  submittingRef.current = true;
2670
3025
  setSubmitting(true);
@@ -2809,7 +3164,16 @@ function StripeMethodInlineForm({
2809
3164
  PaymentElement2,
2810
3165
  {
2811
3166
  onReady: () => setLoadState("ready"),
2812
- onLoadError: () => setLoadState("load_error"),
3167
+ onLoadError: () => {
3168
+ setLoadState("load_error");
3169
+ getFloPayTelemetryBridge(flopay)?.error({
3170
+ errorCode: "PROVIDER_LOAD_FAILED",
3171
+ stage: "provider_load",
3172
+ provider: "stripe",
3173
+ paymentMethodCategory: "apm",
3174
+ requestCategory: "provider_sdk"
3175
+ });
3176
+ },
2813
3177
  onChange: (event) => {
2814
3178
  const evRecord = event;
2815
3179
  setIsMethodComplete(!!evRecord.complete);
@@ -2897,7 +3261,7 @@ function StripePaymentElementInner({
2897
3261
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
2898
3262
  const [expandedMethod, setExpandedMethod] = useState3(null);
2899
3263
  const [submittingMethod, setSubmittingMethod] = useState3(null);
2900
- const submittingRef = useRef4(false);
3264
+ const submittingRef = useRef5(false);
2901
3265
  useEffect5(() => {
2902
3266
  if (paymentElementMethods.length > 0 && stripeInstance) {
2903
3267
  onLoadStateChange?.("ready");
@@ -2905,7 +3269,7 @@ function StripePaymentElementInner({
2905
3269
  onLoadStateChange?.("loading");
2906
3270
  }
2907
3271
  }, [paymentElementMethods.length, stripeInstance, onLoadStateChange]);
2908
- const handleAutoConfirm = useCallback2(async (method) => {
3272
+ const handleAutoConfirm = useCallback3(async (method) => {
2909
3273
  if (!stripeInstance) return;
2910
3274
  if (submittingRef.current || isProcessing) return;
2911
3275
  submittingRef.current = true;
@@ -3046,7 +3410,7 @@ function StripePaymentElementInner({
3046
3410
  ]);
3047
3411
  const [localExpandedMethod, setLocalExpandedMethod] = useState3(null);
3048
3412
  const activeExpandedMethod = onExpandApm ? expandedApmMethod ?? null : localExpandedMethod;
3049
- const handleMethodClick = useCallback2((method) => {
3413
+ const handleMethodClick = useCallback3((method) => {
3050
3414
  if (submittingRef.current || isProcessing) return;
3051
3415
  if (activeExpandedMethod && activeExpandedMethod !== method) return;
3052
3416
  if (needsStripeMethodExplicitConfirm(method)) {
@@ -3204,12 +3568,12 @@ function SplitCardFormInner({
3204
3568
  const [city, setCity] = useState3(cityProp ?? "");
3205
3569
  const [stateValue, setStateValue] = useState3(stateProp ?? "");
3206
3570
  const [accountPatch, setAccountPatch] = useState3({});
3207
- const zipCodeRef = useRef4(zipProp ?? "");
3208
- const selectedCountryRef = useRef4(countryProp ?? "US");
3209
- const addressLine1Ref = useRef4(addressLine1Prop ?? "");
3210
- const addressLine2Ref = useRef4(addressLine2Prop ?? "");
3211
- const cityRef = useRef4(cityProp ?? "");
3212
- const stateRef = useRef4(stateProp ?? "");
3571
+ const zipCodeRef = useRef5(zipProp ?? "");
3572
+ const selectedCountryRef = useRef5(countryProp ?? "US");
3573
+ const addressLine1Ref = useRef5(addressLine1Prop ?? "");
3574
+ const addressLine2Ref = useRef5(addressLine2Prop ?? "");
3575
+ const cityRef = useRef5(cityProp ?? "");
3576
+ const stateRef = useRef5(stateProp ?? "");
3213
3577
  const avsConfig = useMemo3(() => resolveAVSConfig(enableAVSProp), [enableAVSProp]);
3214
3578
  const enableAVS = avsConfig !== null;
3215
3579
  const vaultBlockReady = Boolean(session?.vault?.html);
@@ -3251,20 +3615,20 @@ function SplitCardFormInner({
3251
3615
  const [expandedApmMethod, setExpandedApmMethod] = useState3(null);
3252
3616
  const showCardForm = viewState === "expanding" || viewState === "card";
3253
3617
  const TRANSITION_MS = 280;
3254
- const expandToCard = useCallback2(() => {
3618
+ const expandToCard = useCallback3(() => {
3255
3619
  setViewState("expanding");
3256
3620
  setTimeout(() => setViewState("card"), TRANSITION_MS);
3257
3621
  }, []);
3258
- const collapseToButtons = useCallback2(() => {
3622
+ const collapseToButtons = useCallback3(() => {
3259
3623
  setViewState("collapsing");
3260
3624
  setTimeout(() => setViewState("buttons"), TRANSITION_MS);
3261
3625
  }, []);
3262
- const expandToApm = useCallback2((method) => {
3626
+ const expandToApm = useCallback3((method) => {
3263
3627
  setExpandedApmMethod(method);
3264
3628
  setViewState("apm-expanding");
3265
3629
  setTimeout(() => setViewState("apm-form"), TRANSITION_MS);
3266
3630
  }, []);
3267
- const collapseFromApm = useCallback2(() => {
3631
+ const collapseFromApm = useCallback3(() => {
3268
3632
  setViewState("apm-collapsing");
3269
3633
  setTimeout(() => {
3270
3634
  setViewState("buttons");
@@ -3279,9 +3643,9 @@ function SplitCardFormInner({
3279
3643
  const [fullName, setFullName] = useState3("");
3280
3644
  const [formReady, setFormReady] = useState3(false);
3281
3645
  const [overlayStatus, setOverlayStatus] = useState3(null);
3282
- const processingRef = useRef4(false);
3646
+ const processingRef = useRef5(false);
3283
3647
  const [paypalDirectRetry, setPaypalDirectRetry] = useState3(null);
3284
- const paypalDirectRetryRef = useRef4(paypalDirectRetry);
3648
+ const paypalDirectRetryRef = useRef5(paypalDirectRetry);
3285
3649
  useEffect5(() => {
3286
3650
  paypalDirectRetryRef.current = paypalDirectRetry;
3287
3651
  }, [paypalDirectRetry]);
@@ -3389,7 +3753,7 @@ function SplitCardFormInner({
3389
3753
  setupFutureUsage: "off_session",
3390
3754
  ...stripeAppearanceProp
3391
3755
  }), [amountInCents, currency, stripeAppearanceProp]);
3392
- const updateError = useCallback2(
3756
+ const updateError = useCallback3(
3393
3757
  (err) => {
3394
3758
  setError(err);
3395
3759
  onErrorChange?.(err);
@@ -3405,13 +3769,13 @@ function SplitCardFormInner({
3405
3769
  updateError(null);
3406
3770
  }
3407
3771
  }, [viewState, updateError]);
3408
- const emitDecline = useCallback2(
3772
+ const emitDecline = useCallback3(
3409
3773
  (method, input, overrides) => {
3410
3774
  onDecline?.(buildDeclineEvent(method, input, overrides));
3411
3775
  },
3412
3776
  [onDecline]
3413
3777
  );
3414
- const recoverExternalMethodTechnicalFailure = useCallback2(
3778
+ const recoverExternalMethodTechnicalFailure = useCallback3(
3415
3779
  (method, err, options) => {
3416
3780
  const popupBlocked = options?.popupBlocked ?? isPopupBlockedError(err);
3417
3781
  const message = buildExternalMethodRecoveryMessage(method, popupBlocked);
@@ -3473,7 +3837,7 @@ function SplitCardFormInner({
3473
3837
  nonce,
3474
3838
  updateError
3475
3839
  ]);
3476
- const vaultOutcomeRef = useRef4({
3840
+ const vaultOutcomeRef = useRef5({
3477
3841
  onComplete,
3478
3842
  onError,
3479
3843
  updateError,
@@ -3499,8 +3863,8 @@ function SplitCardFormInner({
3499
3863
  nonce,
3500
3864
  baseUrl
3501
3865
  };
3502
- const vaultCompletedRef = useRef4(false);
3503
- const buildVaultAccountSnapshot = useCallback2(() => {
3866
+ const vaultCompletedRef = useRef5(false);
3867
+ const buildVaultAccountSnapshot = useCallback3(() => {
3504
3868
  const { resolvedAccount: resolvedAccount2, avsConfig: avsConfig2, fullName: fullName2, avsCheckProp: avsCheckProp2 } = vaultOutcomeRef.current;
3505
3869
  const cc = selectedCountryRef.current || resolvedAccount2.country || "US";
3506
3870
  const stateVisible = avsConfig2 ? isAVSFieldVisible(avsConfig2.state, cc) : false;
@@ -3656,7 +4020,7 @@ function SplitCardFormInner({
3656
4020
  const shouldDisplayPayPalRow = shouldRenderDirectPayPal ? directPaypalReady : shouldRenderStripePayPal && isExpressCheckoutRowVisible(paypalLoadState);
3657
4021
  const shouldDisplayWalletRow = shouldRenderWallets && isExpressCheckoutRowVisible(walletLoadState);
3658
4022
  const shouldDisplayPaymentElementRow = shouldRenderPaymentElement && paymentElementLoadState !== "load_error";
3659
- const validationFiredRef = useRef4(false);
4023
+ const validationFiredRef = useRef5(false);
3660
4024
  useEffect5(() => {
3661
4025
  if (validationFiredRef.current) return;
3662
4026
  if (!showStripe && !showPayPal) {
@@ -3677,7 +4041,7 @@ function SplitCardFormInner({
3677
4041
  updateError(err.message);
3678
4042
  }
3679
4043
  }, [showStripe, showPayPal, directPaypalConfigured, paypalStripeInstance, onError, updateError]);
3680
- const deprecationLoggedRef = useRef4(false);
4044
+ const deprecationLoggedRef = useRef5(false);
3681
4045
  useEffect5(() => {
3682
4046
  if (deprecationLoggedRef.current) return;
3683
4047
  if (!hasEnabledMethods) return;
@@ -3690,14 +4054,14 @@ function SplitCardFormInner({
3690
4054
  `[FloPay] ${stale.join(" / ")} ${stale.length === 1 ? "is" : "are"} deprecated: the Apple Pay / Google Pay surface is now driven by \`gateways.stripe.enabledPaymentMethods\` on the session response. Remove the legacy prop(s) to silence this warning.`
3691
4055
  );
3692
4056
  }, [hasEnabledMethods, showApplePay, showGooglePay]);
3693
- const handleNameChange = useCallback2((value) => {
4057
+ const handleNameChange = useCallback3((value) => {
3694
4058
  setFullName(value);
3695
4059
  onFullNameChange?.(value);
3696
4060
  const parts = value.trim().split(/\s+/);
3697
4061
  onFirstNameChange?.(parts[0] ?? "");
3698
4062
  onLastNameChange?.(parts.length > 1 ? parts.slice(1).join(" ") : "");
3699
4063
  }, [onFullNameChange, onFirstNameChange, onLastNameChange]);
3700
- const applyInlineSessionPatch = useCallback2(
4064
+ const applyInlineSessionPatch = useCallback3(
3701
4065
  (patch, method) => {
3702
4066
  if (!checkout.applyInlineSessionPatch) {
3703
4067
  return Promise.resolve({ error: null, sessionId, nonce });
@@ -3719,7 +4083,7 @@ function SplitCardFormInner({
3719
4083
  },
3720
4084
  [checkout.applyInlineSessionPatch, nonce, onError, sessionId, updateError]
3721
4085
  );
3722
- const runBeforeButtonClick = useCallback2(async (method) => {
4086
+ const runBeforeButtonClick = useCallback3(async (method) => {
3723
4087
  if (!onBeforeButtonClick) return { proceed: true };
3724
4088
  try {
3725
4089
  const result = await onBeforeButtonClick({
@@ -3756,7 +4120,7 @@ function SplitCardFormInner({
3756
4120
  return { proceed: false };
3757
4121
  }
3758
4122
  }, [applyInlineSessionPatch, checkout.inlineSessionDraft, onBeforeButtonClick, onError, sessionId, updateError]);
3759
- const processPaymentInternal = useCallback2(
4123
+ const processPaymentInternal = useCallback3(
3760
4124
  async (tokenizedBody, overrides) => {
3761
4125
  if (processingRef.current) return;
3762
4126
  processingRef.current = true;
@@ -3967,7 +4331,7 @@ function SplitCardFormInner({
3967
4331
  },
3968
4332
  [baseUrl, sessionId, nonce, resolvedAccount, fullName, chv, flopay, paypalFlopay, onComplete, onError, updateError, emitDecline]
3969
4333
  );
3970
- const dispatchTokenizedBody = useCallback2(
4334
+ const dispatchTokenizedBody = useCallback3(
3971
4335
  (tokenizedBody, overrides) => {
3972
4336
  if (onTokenizedBody) {
3973
4337
  onTokenizedBody(tokenizedBody);
@@ -4004,7 +4368,7 @@ function SplitCardFormInner({
4004
4368
  }
4005
4369
  }
4006
4370
  }), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
4007
- const stripeResumeAttemptedRef = useRef4(false);
4371
+ const stripeResumeAttemptedRef = useRef5(false);
4008
4372
  useEffect5(() => {
4009
4373
  if (typeof window === "undefined" || stripeResumeAttemptedRef.current) return;
4010
4374
  const params = new URLSearchParams(window.location.search);
@@ -4083,7 +4447,7 @@ function SplitCardFormInner({
4083
4447
  }, persistedOverrides);
4084
4448
  })();
4085
4449
  }, [sessionId, dispatchTokenizedBody, flopay, updateError, emitDecline]);
4086
- const handleSubmit = useCallback2(
4450
+ const handleSubmit = useCallback3(
4087
4451
  async (e) => {
4088
4452
  e.preventDefault();
4089
4453
  if (!flopay || !elements || isSubmitting || processingRef.current) return;
@@ -5725,7 +6089,7 @@ async function recover3DSRedirectResult({
5725
6089
  return null;
5726
6090
  }
5727
6091
  try {
5728
- const api = new PaymentAPI3(billingApiUrl);
6092
+ const api = new PaymentAPI3(billingApiUrl, { telemetry: false });
5729
6093
  const unified = await api.getUnifiedCheckoutSession(sessionId, nonce);
5730
6094
  const refreshedToken = unified.data.stripe?.clientSecret;
5731
6095
  if (isStripePaymentIntentClientSecret(refreshedToken)) {
@@ -5744,7 +6108,8 @@ async function processSavedPaymentForMode({
5744
6108
  session,
5745
6109
  nonce,
5746
6110
  tokenizedData,
5747
- returnUrl
6111
+ returnUrl,
6112
+ telemetry
5748
6113
  }) {
5749
6114
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
5750
6115
  const resolvedSessionId = sessionId ?? session.id;
@@ -5755,7 +6120,10 @@ async function processSavedPaymentForMode({
5755
6120
  const lastName = session.customer?.lastName ?? session.accountData?.lastName ?? "";
5756
6121
  const country = session.customer?.country ?? session.accountData?.country ?? void 0;
5757
6122
  const zip = session.customer?.zip ?? session.accountData?.zip ?? void 0;
5758
- const api = new PaymentAPI3(baseUrl);
6123
+ const api = new PaymentAPI3(
6124
+ baseUrl,
6125
+ telemetry === false ? { telemetry: false } : void 0
6126
+ );
5759
6127
  const response = await retryOnceOnFetchFailure(() => api.processPayment(customerId, {
5760
6128
  sessionId: resolvedSessionId,
5761
6129
  nonce: resolvedNonce,
@@ -5832,7 +6200,8 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
5832
6200
  billingApiUrl,
5833
6201
  sessionId,
5834
6202
  session,
5835
- returnUrl
6203
+ returnUrl,
6204
+ telemetry
5836
6205
  }) {
5837
6206
  const stripe = flopay?.getRawProvider();
5838
6207
  if (!stripe) {
@@ -5915,6 +6284,7 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
5915
6284
  billingApiUrl,
5916
6285
  sessionId,
5917
6286
  session,
6287
+ telemetry,
5918
6288
  tokenizedData: {
5919
6289
  id: paymentIntent.id,
5920
6290
  type: "card",
@@ -5936,7 +6306,8 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
5936
6306
  billingApiUrl,
5937
6307
  sessionId,
5938
6308
  session,
5939
- returnUrl
6309
+ returnUrl,
6310
+ telemetry
5940
6311
  });
5941
6312
  }
5942
6313
  throw Object.assign(
@@ -6042,7 +6413,8 @@ async function loadSavedPaymentProviders({
6042
6413
  publishableKey,
6043
6414
  paypalPublishableKey,
6044
6415
  billingApiUrl,
6045
- locale
6416
+ locale,
6417
+ telemetry
6046
6418
  }) {
6047
6419
  if (!publishableKey) {
6048
6420
  return { flopay: null, paypalFlopay: null };
@@ -6051,11 +6423,13 @@ async function loadSavedPaymentProviders({
6051
6423
  const [instance, paypalInstanceOrError] = await Promise.all([
6052
6424
  loadFloPay(publishableKey, {
6053
6425
  billingApiUrl,
6054
- locale
6426
+ locale,
6427
+ telemetry
6055
6428
  }),
6056
6429
  needsSeparatePaypal ? loadFloPay(paypalPublishableKey, {
6057
6430
  billingApiUrl,
6058
- locale
6431
+ locale,
6432
+ telemetry
6059
6433
  }).catch((err) => {
6060
6434
  console.warn("[FloPay] Failed to load PayPal Stripe instance:", err);
6061
6435
  return null;
@@ -6075,6 +6449,33 @@ var sessionInflightMap = /* @__PURE__ */ new Map();
6075
6449
  function sleep(ms) {
6076
6450
  return new Promise((resolve) => setTimeout(resolve, ms));
6077
6451
  }
6452
+ function createStandaloneTelemetryReporter(billingApiUrl, enabled, context) {
6453
+ const reporter = createTelemetryBridge({
6454
+ billingApiUrl,
6455
+ sdkPackage: "@flopay/react",
6456
+ sdkVersion: SDK_VERSION2,
6457
+ enabled: enabled !== false
6458
+ });
6459
+ reporter.beginCheckout(context);
6460
+ return reporter;
6461
+ }
6462
+ function finishStandaloneTelemetry(reporter) {
6463
+ void reporter.flush().catch(() => {
6464
+ }).finally(() => reporter.destroy());
6465
+ }
6466
+ function reportStandaloneTelemetryError(billingApiUrl, enabled, context, errorCode, stage) {
6467
+ const reporter = createStandaloneTelemetryReporter(billingApiUrl, enabled, context);
6468
+ reporter.error({
6469
+ errorCode,
6470
+ stage,
6471
+ paymentMethodCategory: "unknown",
6472
+ ...stage === "session_read" ? { requestCategory: "session_read" } : {}
6473
+ });
6474
+ finishStandaloneTelemetry(reporter);
6475
+ }
6476
+ function isExpectedExistingSessionError(error) {
6477
+ return error.type === "validation_error" || error.code === "checkout_session_not_found" || error.code === "checkout_session_expired" || error.code === "checkout_session_completed" || error.code === "session_auto_completed";
6478
+ }
6078
6479
  function resolveDirectPaypalConfig(unified) {
6079
6480
  const clientId = unified?.data.paypal?.publishableKey;
6080
6481
  if (!clientId) return void 0;
@@ -6179,6 +6580,7 @@ function FloPayCheckout({
6179
6580
  nonce: nonceProp,
6180
6581
  createSession: createSessionParams,
6181
6582
  billingApiUrl,
6583
+ telemetry,
6182
6584
  appearance: appearanceOverride,
6183
6585
  locale,
6184
6586
  loading: loadingNode,
@@ -6224,9 +6626,9 @@ function FloPayCheckout({
6224
6626
  const checkoutLayout = children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout";
6225
6627
  const [unified, setUnified] = useState4(null);
6226
6628
  const [flopay, setFloPay] = useState4(null);
6227
- const flopayRef = useRef5(null);
6629
+ const flopayRef = useRef6(null);
6228
6630
  const [paypalFlopay, setPaypalFloPay] = useState4(null);
6229
- const paypalFlopayRef = useRef5(null);
6631
+ const paypalFlopayRef = useRef6(null);
6230
6632
  const [session, setSession] = useState4(null);
6231
6633
  const [resolvedSessionId, setResolvedSessionId] = useState4(sessionIdProp ?? "");
6232
6634
  const activeSessionId = sessionIdProp ?? resolvedSessionId;
@@ -6241,20 +6643,30 @@ function FloPayCheckout({
6241
6643
  const [createSessionPatch, setCreateSessionPatch] = useState4(void 0);
6242
6644
  const [createSessionPatchBaseHash, setCreateSessionPatchBaseHash] = useState4("");
6243
6645
  const [cardBootstrapPending, setCardBootstrapPending] = useState4(false);
6244
- const autoCheckoutAttempted = useRef5(false);
6245
- const paypalResumeAttempted = useRef5(false);
6246
- const savedPaymentKeysRef = useRef5(null);
6247
- const onCompleteRef = useRef5(onComplete);
6646
+ const autoCheckoutAttempted = useRef6(false);
6647
+ const paypalResumeAttempted = useRef6(false);
6648
+ const savedPaymentKeysRef = useRef6(null);
6649
+ const telemetryCheckoutContext = useMemo4(() => ({
6650
+ checkoutMode: checkoutModeProp ?? currentMode,
6651
+ layout: children ? "unknown" : layout === "buttons" ? "buttons" : "embedded"
6652
+ }), [checkoutModeProp, children, currentMode, layout]);
6653
+ const onCompleteRef = useRef6(onComplete);
6248
6654
  onCompleteRef.current = onComplete;
6249
- const onErrorRef = useRef5(onError);
6655
+ const onErrorRef = useRef6(onError);
6250
6656
  onErrorRef.current = onError;
6251
- const onDeclineRef = useRef5(onDecline);
6657
+ const onDeclineRef = useRef6(onDecline);
6252
6658
  onDeclineRef.current = onDecline;
6253
- const onSessionCompletedRef = useRef5(onSessionCompleted);
6659
+ useEffect6(() => {
6660
+ getFloPayTelemetryBridge(flopay)?.setCheckoutContext(telemetryCheckoutContext);
6661
+ if (paypalFlopay && paypalFlopay !== flopay) {
6662
+ getFloPayTelemetryBridge(paypalFlopay)?.setCheckoutContext(telemetryCheckoutContext);
6663
+ }
6664
+ }, [flopay, paypalFlopay, telemetryCheckoutContext]);
6665
+ const onSessionCompletedRef = useRef6(onSessionCompleted);
6254
6666
  onSessionCompletedRef.current = onSessionCompleted;
6255
6667
  useEffect6(() => {
6256
6668
  console.info("[FloPay] Checkout initialized", {
6257
- sdk_version: SDK_VERSION,
6669
+ sdk_version: SDK_VERSION2,
6258
6670
  checkout_type: checkoutType,
6259
6671
  checkout_layout: checkoutLayout,
6260
6672
  billing_api_url: resolvedBillingUrl
@@ -6287,22 +6699,112 @@ function FloPayCheckout({
6287
6699
  useEffect6(() => {
6288
6700
  setModeError(initialErrorMessage);
6289
6701
  }, [initialErrorMessage]);
6290
- const emitDecline = useCallback3(
6702
+ const emitDecline = useCallback4(
6291
6703
  (method, input, overrides) => {
6292
- onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
6704
+ invokeMerchantCallback(() => {
6705
+ onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
6706
+ });
6293
6707
  },
6294
- []
6708
+ [invokeMerchantCallback]
6295
6709
  );
6296
- const runSavedPaymentFlow = useCallback3(
6710
+ const runSavedPaymentFlow = useCallback4(
6297
6711
  async (sess, options) => {
6298
6712
  setModeError(null);
6299
6713
  setModeOverlayError(null);
6300
6714
  setModeOverlayStatus("processing");
6301
6715
  const activeSessionId2 = options?.sessionId ?? sess.id;
6716
+ const activeFloPay = flopayRef.current ?? paypalFlopayRef.current;
6717
+ const activeTelemetry = getFloPayTelemetryBridge(activeFloPay);
6718
+ const standaloneTelemetry = activeFloPay ? null : createStandaloneTelemetryReporter(
6719
+ resolvedBillingUrl,
6720
+ telemetry,
6721
+ telemetryCheckoutContext
6722
+ );
6723
+ const telemetrySource = {
6724
+ log: (input) => {
6725
+ if (activeTelemetry) activeTelemetry.log(input);
6726
+ else standaloneTelemetry?.log(input);
6727
+ },
6728
+ error: (input) => {
6729
+ if (activeTelemetry) activeTelemetry.error(input);
6730
+ else standaloneTelemetry?.error(input);
6731
+ },
6732
+ terminal: (input) => {
6733
+ if (activeTelemetry) activeTelemetry.terminal(input);
6734
+ else standaloneTelemetry?.terminal(input);
6735
+ },
6736
+ performance: (input) => {
6737
+ if (activeTelemetry) activeTelemetry.performance(input);
6738
+ else standaloneTelemetry?.performance(input);
6739
+ },
6740
+ now: () => activeTelemetry?.now() ?? standaloneTelemetry?.now() ?? 0,
6741
+ elapsed: (startedAt) => activeTelemetry?.elapsed(startedAt) ?? Math.max(0, (standaloneTelemetry?.now() ?? startedAt) - startedAt)
6742
+ };
6743
+ const processingStartedAt = telemetrySource.now();
6744
+ let recoveryFlow = Boolean(
6745
+ options?.initialAutoProcessingError || options?.initialAutoProcessingPending
6746
+ );
6747
+ let recoveryStarted = false;
6748
+ let recoveryStartedAt;
6749
+ const startRecovery = () => {
6750
+ if (recoveryStarted) return;
6751
+ recoveryStarted = true;
6752
+ recoveryFlow = true;
6753
+ recoveryStartedAt = telemetrySource.now();
6754
+ telemetrySource.log({
6755
+ name: "checkout.recovery.started",
6756
+ stage: "recovery",
6757
+ paymentMethodCategory: "saved"
6758
+ });
6759
+ telemetrySource.log({
6760
+ name: "operation.recovery.started",
6761
+ stage: "recovery",
6762
+ paymentMethodCategory: "saved"
6763
+ });
6764
+ };
6765
+ let processingFinished = false;
6766
+ const finishProcessing = () => {
6767
+ if (processingFinished) return;
6768
+ processingFinished = true;
6769
+ telemetrySource.log({
6770
+ name: "payment.processing.completed",
6771
+ stage: "processing",
6772
+ paymentMethodCategory: "saved"
6773
+ });
6774
+ telemetrySource.performance({
6775
+ stage: "processing",
6776
+ durationMs: telemetrySource.elapsed(processingStartedAt),
6777
+ durationMode: "machine",
6778
+ paymentMethodCategory: "saved"
6779
+ });
6780
+ if (recoveryStartedAt !== void 0) {
6781
+ telemetrySource.performance({
6782
+ stage: "recovery",
6783
+ durationMs: telemetrySource.elapsed(recoveryStartedAt),
6784
+ durationMode: "machine",
6785
+ paymentMethodCategory: "saved"
6786
+ });
6787
+ }
6788
+ };
6789
+ telemetrySource.log({
6790
+ name: "payment.method.selected",
6791
+ stage: "processing",
6792
+ paymentMethodCategory: "saved"
6793
+ });
6794
+ telemetrySource.log({
6795
+ name: "payment.processing.started",
6796
+ stage: "processing",
6797
+ paymentMethodCategory: "saved"
6798
+ });
6799
+ if (recoveryFlow) {
6800
+ startRecovery();
6801
+ }
6802
+ let completionCallback;
6302
6803
  try {
6303
6804
  const redirectResult = getRedirectResultFromCheckoutProcessError(options?.initialAutoProcessingError);
6304
6805
  let paymentResult;
6305
6806
  if (redirectResult) {
6807
+ startRecovery();
6306
6808
  if (redirectResult.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
6307
6809
  persistPayPalResumeState({
6308
6810
  sessionId: activeSessionId2,
@@ -6316,13 +6818,14 @@ function FloPayCheckout({
6316
6818
  attempt3DS: options?.attempt3DS,
6317
6819
  billingApiUrl: resolvedBillingUrl,
6318
6820
  sessionId: activeSessionId2,
6319
- session: sess
6821
+ session: sess,
6822
+ telemetry: false
6320
6823
  });
6321
6824
  if (redirectResult.type === "paypal_redirect_required") {
6322
6825
  clearPayPalResumeState();
6323
6826
  }
6324
6827
  } else if (options?.initialAutoProcessingPending) {
6325
- const api = new PaymentAPI4(resolvedBillingUrl);
6828
+ const api = new PaymentAPI4(resolvedBillingUrl, { telemetry: false });
6326
6829
  const completed = await api.waitForCheckoutSessionCompletion(options.initialAutoProcessingPending.sessionId, {
6327
6830
  initialDelayMs: options.initialAutoProcessingPending.retryAfterMs
6328
6831
  });
@@ -6350,11 +6853,13 @@ function FloPayCheckout({
6350
6853
  const result = await processSavedPaymentForMode({
6351
6854
  billingApiUrl: resolvedBillingUrl,
6352
6855
  sessionId: activeSessionId2,
6353
- session: sess
6856
+ session: sess,
6857
+ telemetry: false
6354
6858
  });
6355
6859
  if (result.type === "success") {
6356
6860
  paymentResult = result.result;
6357
6861
  } else {
6862
+ startRecovery();
6358
6863
  if (result.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
6359
6864
  persistPayPalResumeState({
6360
6865
  sessionId: activeSessionId2,
@@ -6368,7 +6873,8 @@ function FloPayCheckout({
6368
6873
  attempt3DS: options?.attempt3DS,
6369
6874
  billingApiUrl: resolvedBillingUrl,
6370
6875
  sessionId: activeSessionId2,
6371
- session: sess
6876
+ session: sess,
6877
+ telemetry: false
6372
6878
  });
6373
6879
  if (result.type === "paypal_redirect_required") {
6374
6880
  clearPayPalResumeState();
@@ -6376,20 +6882,57 @@ function FloPayCheckout({
6376
6882
  }
6377
6883
  }
6378
6884
  if (activeSessionId2) markSessionRecentlyCompleted(activeSessionId2);
6885
+ finishProcessing();
6886
+ if (recoveryFlow) {
6887
+ telemetrySource.log({
6888
+ name: "checkout.recovery.completed",
6889
+ stage: "recovery",
6890
+ paymentMethodCategory: "saved"
6891
+ });
6892
+ telemetrySource.log({
6893
+ name: "operation.recovery.completed",
6894
+ stage: "recovery",
6895
+ paymentMethodCategory: "saved"
6896
+ });
6897
+ }
6898
+ telemetrySource.terminal({
6899
+ outcome: "payment_succeeded",
6900
+ paymentMethodCategory: "saved"
6901
+ });
6379
6902
  setModeOverlayStatus("success");
6380
6903
  await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
6381
- onCompleteRef.current?.(paymentResult);
6382
- return true;
6904
+ completionCallback = () => onCompleteRef.current?.(paymentResult);
6383
6905
  } catch (err) {
6384
6906
  const floPayErr = normalizeSavedPaymentError(err);
6385
6907
  const method = floPayErr.checkoutMethod ?? DEFAULT_SAVED_PAYMENT_DECLINE_METHOD;
6386
6908
  setModeError(floPayErr.message);
6387
6909
  setModeOverlayError(floPayErr.message);
6388
6910
  if (options?.fallbackToFull) {
6911
+ telemetrySource.log({
6912
+ name: "operation.fallback",
6913
+ stage: "recovery",
6914
+ paymentMethodCategory: "saved"
6915
+ });
6389
6916
  setCurrentMode("full");
6390
6917
  replaceCheckoutModeQueryParam("full");
6391
6918
  }
6392
- onErrorRef.current?.(floPayErr);
6919
+ finishProcessing();
6920
+ const expectedDecline = Boolean(
6921
+ floPayErr.declineCode || floPayErr.code?.toLowerCase().includes("declin")
6922
+ );
6923
+ if (expectedDecline) {
6924
+ telemetrySource.terminal({
6925
+ outcome: "payment_declined",
6926
+ paymentMethodCategory: "saved"
6927
+ });
6928
+ } else {
6929
+ telemetrySource.error({
6930
+ errorCode: recoveryFlow ? "RECOVERY_FAILED" : "PAYMENT_PROCESSING_FAILED",
6931
+ stage: recoveryFlow ? "recovery" : "processing",
6932
+ paymentMethodCategory: "saved"
6933
+ });
6934
+ }
6935
+ invokeMerchantCallback(() => onErrorRef.current?.(floPayErr));
6393
6936
  emitDecline(method, floPayErr, {
6394
6937
  code: floPayErr.code,
6395
6938
  declineCode: floPayErr.declineCode
@@ -6399,16 +6942,35 @@ function FloPayCheckout({
6399
6942
  sleep(PROCESSING_OVERLAY_ERROR_DELAY_MS),
6400
6943
  options?.ensureProvidersReady ? options.ensureProvidersReady() : Promise.resolve()
6401
6944
  ]);
6945
+ if (recoveryFlow) {
6946
+ telemetrySource.log({
6947
+ name: "checkout.recovery.completed",
6948
+ stage: "recovery",
6949
+ paymentMethodCategory: "saved"
6950
+ });
6951
+ telemetrySource.log({
6952
+ name: "operation.recovery.completed",
6953
+ stage: "recovery",
6954
+ paymentMethodCategory: "saved"
6955
+ });
6956
+ }
6402
6957
  return false;
6403
6958
  } finally {
6959
+ finishProcessing();
6960
+ if (standaloneTelemetry) finishStandaloneTelemetry(standaloneTelemetry);
6404
6961
  setModeOverlayStatus(null);
6405
6962
  setModeOverlayError(null);
6406
6963
  }
6964
+ invokeMerchantCallback(completionCallback);
6965
+ return true;
6407
6966
  },
6408
6967
  [
6409
6968
  emitDecline,
6969
+ invokeMerchantCallback,
6410
6970
  normalizeSavedPaymentError,
6411
- resolvedBillingUrl
6971
+ resolvedBillingUrl,
6972
+ telemetry,
6973
+ telemetryCheckoutContext
6412
6974
  ]
6413
6975
  );
6414
6976
  useEffect6(() => {
@@ -6426,6 +6988,8 @@ function FloPayCheckout({
6426
6988
  }
6427
6989
  paypalResumeAttempted.current = true;
6428
6990
  void (async () => {
6991
+ let resumeTelemetry;
6992
+ let redirectResumeStartedAt = 0;
6429
6993
  setModeError(null);
6430
6994
  setModeOverlayError(null);
6431
6995
  setModeOverlayStatus("processing");
@@ -6433,7 +6997,11 @@ function FloPayCheckout({
6433
6997
  try {
6434
6998
  if (params.get("redirect_status") === "failed") {
6435
6999
  throw Object.assign(
6436
- new FloPayError5("PayPal payment was declined. Please try again.", "api_error"),
7000
+ new FloPayError5(
7001
+ "PayPal payment was declined. Please try again.",
7002
+ "api_error",
7003
+ { declineCode: "paypal_redirect_failed" }
7004
+ ),
6437
7005
  { checkoutMethod: "paypal" }
6438
7006
  );
6439
7007
  }
@@ -6444,7 +7012,22 @@ function FloPayCheckout({
6444
7012
  publishableKey: resumeState.publishableKey,
6445
7013
  paypalPublishableKey: resumeState.paypalPublishableKey,
6446
7014
  billingApiUrl: resolvedBillingUrl,
6447
- locale
7015
+ locale,
7016
+ telemetry
7017
+ });
7018
+ resumeTelemetry = getFloPayTelemetryBridge(resumePaypalFlopay ?? resumeFlopay);
7019
+ redirectResumeStartedAt = resumeTelemetry?.now() ?? 0;
7020
+ resumeTelemetry?.log({
7021
+ name: "provider.redirect.resumed",
7022
+ stage: "redirect_resume",
7023
+ provider: "paypal",
7024
+ paymentMethodCategory: "paypal"
7025
+ });
7026
+ resumeTelemetry?.log({
7027
+ name: "operation.recovery.started",
7028
+ stage: "recovery",
7029
+ provider: "paypal",
7030
+ paymentMethodCategory: "paypal"
6448
7031
  });
6449
7032
  const paypalStripe = (resumePaypalFlopay ?? resumeFlopay)?.getRawProvider();
6450
7033
  if (!paypalStripe) {
@@ -6474,7 +7057,7 @@ function FloPayCheckout({
6474
7057
  const paymentMethodId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
6475
7058
  let finalResultStatus = resultStatus;
6476
7059
  if (resumeState.sessionId) {
6477
- const resumeApi = new PaymentAPI4(resolvedBillingUrl);
7060
+ const resumeApi = new PaymentAPI4(resolvedBillingUrl, { telemetry: false });
6478
7061
  const resumeSessionResult = await resumeApi.getUnifiedCheckoutSession(resumeState.sessionId);
6479
7062
  const resumeSession = resumeSessionResult.data.session;
6480
7063
  if (resumeSession && resumeSession.status !== "complete") {
@@ -6482,6 +7065,7 @@ function FloPayCheckout({
6482
7065
  billingApiUrl: resolvedBillingUrl,
6483
7066
  sessionId: resumeState.sessionId,
6484
7067
  session: resumeSession,
7068
+ telemetry: false,
6485
7069
  tokenizedData: {
6486
7070
  id: paymentMethodId ?? paymentIntent.id,
6487
7071
  type: "card",
@@ -6499,20 +7083,84 @@ function FloPayCheckout({
6499
7083
  }
6500
7084
  }
6501
7085
  if (resumeState.sessionId) markSessionRecentlyCompleted(resumeState.sessionId);
7086
+ resumeTelemetry?.log({
7087
+ name: "operation.recovery.completed",
7088
+ stage: "recovery",
7089
+ provider: "paypal",
7090
+ paymentMethodCategory: "paypal"
7091
+ });
7092
+ resumeTelemetry?.performance({
7093
+ stage: "redirect_resume",
7094
+ durationMs: resumeTelemetry.elapsed(redirectResumeStartedAt),
7095
+ durationMode: "machine",
7096
+ provider: "paypal",
7097
+ paymentMethodCategory: "paypal"
7098
+ });
7099
+ resumeTelemetry?.terminal({
7100
+ outcome: "payment_succeeded",
7101
+ provider: "paypal",
7102
+ paymentMethodCategory: "paypal"
7103
+ });
6502
7104
  setModeOverlayStatus("success");
6503
7105
  await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
6504
- onCompleteRef.current?.({
7106
+ invokeMerchantCallback(() => onCompleteRef.current?.({
6505
7107
  status: finalResultStatus,
6506
7108
  paymentIntentId: paymentIntent.id,
6507
7109
  paymentMethodId,
6508
7110
  checkoutMethod: "paypal"
6509
- });
7111
+ }));
6510
7112
  } catch (err) {
6511
7113
  const floPayErr = normalizeSavedPaymentError(err);
6512
7114
  const method = floPayErr.checkoutMethod ?? "paypal";
7115
+ const expectedDecline = Boolean(
7116
+ floPayErr.declineCode || floPayErr.code?.toLowerCase().includes("declin")
7117
+ );
7118
+ if (resumeTelemetry) {
7119
+ resumeTelemetry.performance({
7120
+ stage: "redirect_resume",
7121
+ durationMs: resumeTelemetry.elapsed(redirectResumeStartedAt),
7122
+ durationMode: "machine",
7123
+ provider: "paypal",
7124
+ paymentMethodCategory: "paypal"
7125
+ });
7126
+ if (expectedDecline) {
7127
+ resumeTelemetry.terminal({
7128
+ outcome: "payment_declined",
7129
+ provider: "paypal",
7130
+ paymentMethodCategory: "paypal"
7131
+ });
7132
+ } else {
7133
+ resumeTelemetry.error({
7134
+ errorCode: "REDIRECT_RESUME_FAILED",
7135
+ stage: "redirect_resume",
7136
+ provider: "paypal",
7137
+ paymentMethodCategory: "paypal"
7138
+ });
7139
+ }
7140
+ } else if (expectedDecline) {
7141
+ const reporter = createStandaloneTelemetryReporter(
7142
+ resolvedBillingUrl,
7143
+ telemetry,
7144
+ telemetryCheckoutContext
7145
+ );
7146
+ reporter.terminal({
7147
+ outcome: "payment_declined",
7148
+ provider: "paypal",
7149
+ paymentMethodCategory: "paypal"
7150
+ });
7151
+ finishStandaloneTelemetry(reporter);
7152
+ } else {
7153
+ reportStandaloneTelemetryError(
7154
+ resolvedBillingUrl,
7155
+ telemetry,
7156
+ telemetryCheckoutContext,
7157
+ "REDIRECT_RESUME_FAILED",
7158
+ "redirect_resume"
7159
+ );
7160
+ }
6513
7161
  setModeError(floPayErr.message);
6514
7162
  setModeOverlayError(floPayErr.message);
6515
- onErrorRef.current?.(floPayErr);
7163
+ invokeMerchantCallback(() => onErrorRef.current?.(floPayErr));
6516
7164
  emitDecline(method, floPayErr, {
6517
7165
  code: floPayErr.code,
6518
7166
  declineCode: floPayErr.declineCode
@@ -6527,8 +7175,8 @@ function FloPayCheckout({
6527
7175
  setConfirmProcessing(false);
6528
7176
  }
6529
7177
  })();
6530
- }, [emitDecline, locale, normalizeSavedPaymentError, resolvedBillingUrl]);
6531
- const initializedHashRef = useRef5(null);
7178
+ }, [emitDecline, invokeMerchantCallback, locale, normalizeSavedPaymentError, resolvedBillingUrl, telemetry]);
7179
+ const initializedHashRef = useRef6(null);
6532
7180
  function hashCreateParams(params) {
6533
7181
  const key = JSON.stringify({
6534
7182
  c: params?.clientId,
@@ -6565,7 +7213,7 @@ function FloPayCheckout({
6565
7213
  () => effectiveCreateSession ? hashCreateParams(effectiveCreateSession) : "",
6566
7214
  [effectiveCreateSession]
6567
7215
  );
6568
- const createSessionParamsRef = useRef5(effectiveCreateSession);
7216
+ const createSessionParamsRef = useRef6(effectiveCreateSession);
6569
7217
  createSessionParamsRef.current = effectiveCreateSession;
6570
7218
  useEffect6(() => {
6571
7219
  setResolvedSessionId(sessionIdProp ?? "");
@@ -6577,44 +7225,107 @@ function FloPayCheckout({
6577
7225
  setModeOverlayStatus(null);
6578
7226
  }, [createSessionHash, initialErrorMessage, sessionIdProp]);
6579
7227
  async function resolveInlineSession(params, cacheKey) {
6580
- const api = new PaymentAPI4(resolvedBillingUrl);
6581
- const cached = readCachedInlineSession(cacheKey);
6582
- let sid = cached?.sid ?? null;
6583
- let realResult = null;
6584
- if (sid) {
6585
- try {
6586
- realResult = await api.getUnifiedCheckoutSession(sid, cached?.nonce);
6587
- const status = realResult.data.session?.status;
6588
- if (status === "complete") {
6589
- if (wasSessionRecentlyCompleted(sid)) {
6590
- return { sid, result: realResult };
7228
+ const reporter = createStandaloneTelemetryReporter(
7229
+ resolvedBillingUrl,
7230
+ telemetry,
7231
+ telemetryCheckoutContext
7232
+ );
7233
+ const api = new PaymentAPI4(resolvedBillingUrl, { telemetry: false });
7234
+ try {
7235
+ const cached = readCachedInlineSession(cacheKey);
7236
+ reporter.log({
7237
+ name: cached ? "operation.cache.hit" : "operation.cache.miss",
7238
+ stage: "session_create",
7239
+ requestCategory: "session_create"
7240
+ });
7241
+ let sid = cached?.sid ?? null;
7242
+ let realResult = null;
7243
+ if (sid) {
7244
+ try {
7245
+ realResult = await api.getUnifiedCheckoutSession(sid, cached?.nonce);
7246
+ const status = realResult.data.session?.status;
7247
+ if (status === "complete") {
7248
+ if (wasSessionRecentlyCompleted(sid)) {
7249
+ return { sid, result: realResult };
7250
+ }
7251
+ clearCachedInlineSession(cacheKey);
7252
+ sid = null;
7253
+ realResult = null;
6591
7254
  }
7255
+ } catch {
7256
+ reporter.log({
7257
+ name: "operation.fallback",
7258
+ stage: "session_read",
7259
+ requestCategory: "session_read"
7260
+ });
6592
7261
  clearCachedInlineSession(cacheKey);
6593
7262
  sid = null;
6594
- realResult = null;
6595
7263
  }
6596
- } catch {
6597
- clearCachedInlineSession(cacheKey);
6598
- sid = null;
6599
7264
  }
6600
- }
6601
- if (!sid) {
6602
- const paramsWithAnalytics = {
6603
- ...params,
6604
- avsCheck: !!enableAVS,
6605
- avsConfig: typeof enableAVS === "object" ? enableAVS : void 0,
6606
- checkoutType: "embedded_checkout",
6607
- checkoutLayout: children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout"
6608
- };
6609
- realResult = await api.createAndFetchSession(paramsWithAnalytics);
6610
- sid = realResult.data.session?.id ?? "";
6611
- if (sid) {
6612
- persistCachedInlineSession(cacheKey, sid, realResult.data.session?.clientSecret);
7265
+ if (!sid) {
7266
+ const sessionCreateStartedAt = reporter.now();
7267
+ reporter.log({
7268
+ name: "session.create.started",
7269
+ stage: "session_create",
7270
+ requestCategory: "session_create"
7271
+ });
7272
+ const paramsWithAnalytics = {
7273
+ ...params,
7274
+ avsCheck: !!enableAVS,
7275
+ avsConfig: typeof enableAVS === "object" ? enableAVS : void 0,
7276
+ checkoutType: "embedded_checkout",
7277
+ checkoutLayout: children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout"
7278
+ };
7279
+ try {
7280
+ realResult = await api.createAndFetchSession(paramsWithAnalytics);
7281
+ reporter.log({
7282
+ name: "session.request.completed",
7283
+ stage: "session_complete",
7284
+ requestCategory: "session_create",
7285
+ statusClass: "2xx"
7286
+ });
7287
+ reporter.performance({
7288
+ stage: "session_create",
7289
+ durationMs: reporter.now() - sessionCreateStartedAt,
7290
+ durationMode: "machine",
7291
+ requestCategory: "session_create",
7292
+ statusClass: "2xx"
7293
+ });
7294
+ } catch (error) {
7295
+ reporter.performance({
7296
+ stage: "session_create",
7297
+ durationMs: reporter.now() - sessionCreateStartedAt,
7298
+ durationMode: "machine",
7299
+ requestCategory: "session_create",
7300
+ statusClass: "network_error"
7301
+ });
7302
+ if (error instanceof FloPayError5 && error.type === "validation_error") {
7303
+ reporter.terminal({
7304
+ outcome: "validation_rejected",
7305
+ stage: "session_create",
7306
+ paymentMethodCategory: "unknown"
7307
+ });
7308
+ } else {
7309
+ reporter.error({
7310
+ errorCode: "CHECKOUT_SESSION_CREATE_FAILED",
7311
+ stage: "session_create",
7312
+ paymentMethodCategory: "unknown",
7313
+ requestCategory: "session_create"
7314
+ });
7315
+ }
7316
+ throw error;
7317
+ }
7318
+ sid = realResult.data.session?.id ?? "";
7319
+ if (sid) {
7320
+ persistCachedInlineSession(cacheKey, sid, realResult.data.session?.clientSecret);
7321
+ }
6613
7322
  }
7323
+ return { sid: sid ?? "", result: realResult };
7324
+ } finally {
7325
+ finishStandaloneTelemetry(reporter);
6614
7326
  }
6615
- return { sid: sid ?? "", result: realResult };
6616
7327
  }
6617
- const bootstrapInlineSession = useCallback3(
7328
+ const bootstrapInlineSession = useCallback4(
6618
7329
  async (patch) => {
6619
7330
  const baseParams = createSessionParamsRef.current;
6620
7331
  if (!baseParams) {
@@ -6658,7 +7369,8 @@ function FloPayCheckout({
6658
7369
  publishableKey,
6659
7370
  paypalPublishableKey,
6660
7371
  billingApiUrl: resolvedBillingUrl,
6661
- locale
7372
+ locale,
7373
+ telemetry
6662
7374
  });
6663
7375
  flopayRef.current = instance;
6664
7376
  setFloPay(instance);
@@ -6673,9 +7385,9 @@ function FloPayCheckout({
6673
7385
  }
6674
7386
  return resolved;
6675
7387
  },
6676
- [locale, resolvedBillingUrl]
7388
+ [locale, resolvedBillingUrl, telemetry, telemetryCheckoutContext]
6677
7389
  );
6678
- const handleInlineSessionPatch = useCallback3(async (patch) => {
7390
+ const handleInlineSessionPatch = useCallback4(async (patch) => {
6679
7391
  if (!hasInlineSessionPatchData(patch) || cardBootstrapPending) {
6680
7392
  return {
6681
7393
  sessionId: resolvedSessionId,
@@ -6752,7 +7464,9 @@ function FloPayCheckout({
6752
7464
  setModeOverlayStatus("success");
6753
7465
  await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
6754
7466
  if (!cancelled) {
6755
- onCompleteRef.current?.({ status: "succeeded" });
7467
+ invokeMerchantCallback(() => {
7468
+ onCompleteRef.current?.({ status: "succeeded" });
7469
+ });
6756
7470
  setModeOverlayStatus(null);
6757
7471
  setModeOverlayError(null);
6758
7472
  }
@@ -6772,8 +7486,9 @@ function FloPayCheckout({
6772
7486
  }
6773
7487
  setIsLoading(true);
6774
7488
  async function init() {
7489
+ let sessionReadCompleted = false;
6775
7490
  try {
6776
- const api = new PaymentAPI4(resolvedBillingUrl);
7491
+ const api = new PaymentAPI4(resolvedBillingUrl, { telemetry: false });
6777
7492
  const result = await api.getUnifiedCheckoutSession(activeSessionId, nonceProp);
6778
7493
  if (cancelled) return;
6779
7494
  setUnified(result);
@@ -6784,7 +7499,9 @@ function FloPayCheckout({
6784
7499
  }
6785
7500
  if (sess.status === "complete") {
6786
7501
  setIsLoading(false);
6787
- onSessionCompletedRef.current?.(sess.successUrl ?? "");
7502
+ invokeMerchantCallback(() => {
7503
+ onSessionCompletedRef.current?.(sess.successUrl ?? "");
7504
+ });
6788
7505
  return;
6789
7506
  }
6790
7507
  if (sess.status === "expired") {
@@ -6792,6 +7509,7 @@ function FloPayCheckout({
6792
7509
  code: "checkout_session_expired"
6793
7510
  });
6794
7511
  }
7512
+ sessionReadCompleted = true;
6795
7513
  const effectiveMode = checkoutModeProp ?? sess.checkoutMode ?? "full";
6796
7514
  setCurrentMode(effectiveMode);
6797
7515
  const hasPayPalRedirectParams = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("payment_intent");
@@ -6823,6 +7541,23 @@ function FloPayCheckout({
6823
7541
  if (!cancelled) setIsLoading(false);
6824
7542
  } catch (err) {
6825
7543
  if (cancelled) return;
7544
+ if (!(err instanceof FloPayError5)) {
7545
+ reportStandaloneTelemetryError(
7546
+ resolvedBillingUrl,
7547
+ telemetry,
7548
+ telemetryCheckoutContext,
7549
+ "INTERNAL_SDK_ERROR",
7550
+ "checkout_mount"
7551
+ );
7552
+ } else if (!sessionReadCompleted && !isExpectedExistingSessionError(err)) {
7553
+ reportStandaloneTelemetryError(
7554
+ resolvedBillingUrl,
7555
+ telemetry,
7556
+ telemetryCheckoutContext,
7557
+ "NETWORK_REQUEST_FAILED",
7558
+ "session_read"
7559
+ );
7560
+ }
6826
7561
  const floPayErr = err instanceof FloPayError5 ? err : new FloPayError5(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
6827
7562
  setLoadError(floPayErr);
6828
7563
  setIsLoading(false);
@@ -6844,7 +7579,8 @@ function FloPayCheckout({
6844
7579
  publishableKey,
6845
7580
  paypalPublishableKey,
6846
7581
  billingApiUrl: resolvedBillingUrl,
6847
- locale
7582
+ locale,
7583
+ telemetry
6848
7584
  });
6849
7585
  flopayRef.current = instance;
6850
7586
  setFloPay(instance);
@@ -6861,10 +7597,11 @@ function FloPayCheckout({
6861
7597
  createSessionHash,
6862
7598
  effectiveCreateSessionMode,
6863
7599
  initSessionDependency,
7600
+ invokeMerchantCallback,
6864
7601
  nonceProp,
6865
7602
  runSavedPaymentFlow
6866
7603
  ]);
6867
- const handleConfirmCheckout = useCallback3(async () => {
7604
+ const handleConfirmCheckout = useCallback4(async () => {
6868
7605
  if (confirmProcessing || !session) return;
6869
7606
  setConfirmProcessing(true);
6870
7607
  setModeError(null);
@@ -7028,7 +7765,7 @@ function FloPayCheckout({
7028
7765
  }
7029
7766
  ),
7030
7767
  /* @__PURE__ */ jsx8(
7031
- DirectPayPalButton,
7768
+ InstrumentedDirectPayPalButton,
7032
7769
  {
7033
7770
  sessionId: activeSessionId,
7034
7771
  nonce: session.clientSecret || void 0,
@@ -7043,6 +7780,8 @@ function FloPayCheckout({
7043
7780
  onDecline,
7044
7781
  onButtonClick,
7045
7782
  session,
7783
+ telemetry,
7784
+ telemetryContext: telemetryCheckoutContext,
7046
7785
  debug
7047
7786
  }
7048
7787
  )
@@ -7431,7 +8170,7 @@ function InterimButtonsView({
7431
8170
  // src/checkout-form.tsx
7432
8171
  import { PaymentAPI as PaymentAPI5 } from "@flopay/js";
7433
8172
  import { FloPayError as FloPayError6 } from "@flopay/shared";
7434
- import { forwardRef as forwardRef2, useCallback as useCallback4, useEffect as useEffect7, useImperativeHandle as useImperativeHandle2, useState as useState5 } from "react";
8173
+ import { forwardRef as forwardRef2, useCallback as useCallback5, useEffect as useEffect7, useImperativeHandle as useImperativeHandle2, useState as useState5 } from "react";
7435
8174
  import { Fragment as Fragment4, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
7436
8175
  var WALLET_RESUME_KEY = "flopay_wallet_resume";
7437
8176
  var CheckoutForm = forwardRef2(
@@ -7473,20 +8212,20 @@ function CheckoutFormInner({
7473
8212
  const isSubmitting = externalProcessing ?? processing;
7474
8213
  const isSelfContained = !onTokenizedBody;
7475
8214
  const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
7476
- const updateError = useCallback4(
8215
+ const updateError = useCallback5(
7477
8216
  (err) => {
7478
8217
  setError(err);
7479
8218
  onErrorChange?.(err);
7480
8219
  },
7481
8220
  [onErrorChange]
7482
8221
  );
7483
- const emitDecline = useCallback4(
8222
+ const emitDecline = useCallback5(
7484
8223
  (input, overrides) => {
7485
8224
  onDecline?.(buildDeclineEvent("card", input, overrides));
7486
8225
  },
7487
8226
  [onDecline]
7488
8227
  );
7489
- const processPaymentInternal = useCallback4(
8228
+ const processPaymentInternal = useCallback5(
7490
8229
  async (tokenizedBody, completionPaymentMethodId) => {
7491
8230
  setProcessing(true);
7492
8231
  updateError(null);
@@ -7598,7 +8337,7 @@ function CheckoutFormInner({
7598
8337
  },
7599
8338
  [baseUrl, sessionId, nonce, userId, email, firstName, lastName, chv, flopay, paypalFlopay, onComplete, onError, onDecline, updateError, emitDecline]
7600
8339
  );
7601
- const dispatchTokenizedBody = useCallback4(
8340
+ const dispatchTokenizedBody = useCallback5(
7602
8341
  (tokenizedBody) => {
7603
8342
  if (onTokenizedBody) {
7604
8343
  onTokenizedBody(tokenizedBody);
@@ -7653,7 +8392,7 @@ function CheckoutFormInner({
7653
8392
  localStorage.removeItem(WALLET_RESUME_KEY);
7654
8393
  }
7655
8394
  }, [sessionId, dispatchTokenizedBody]);
7656
- const handleSubmit = useCallback4(
8395
+ const handleSubmit = useCallback5(
7657
8396
  async (e) => {
7658
8397
  e.preventDefault();
7659
8398
  if (!flopay || !elements || isSubmitting) return;
@@ -7776,7 +8515,7 @@ function CheckoutFormInner({
7776
8515
  // src/paypal-button.tsx
7777
8516
  import { PaymentAPI as PaymentAPI6 } from "@flopay/js";
7778
8517
  import { FloPayError as FloPayError7 } from "@flopay/shared";
7779
- import { useCallback as useCallback5, useEffect as useEffect8, useRef as useRef6, useState as useState6 } from "react";
8518
+ import { useCallback as useCallback6, useEffect as useEffect8, useRef as useRef7, useState as useState6 } from "react";
7780
8519
  import { Fragment as Fragment5, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
7781
8520
  function PayPalButton({
7782
8521
  sessionId,
@@ -7797,9 +8536,9 @@ function PayPalButton({
7797
8536
  const contextBillingUrl = useBillingApiUrl();
7798
8537
  const [ready, setReady] = useState6(false);
7799
8538
  const [submitting, setSubmitting] = useState6(false);
7800
- const paypalResumeAttempted = useRef6(false);
8539
+ const paypalResumeAttempted = useRef7(false);
7801
8540
  const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
7802
- const processPaymentInternal = useCallback5(
8541
+ const processPaymentInternal = useCallback6(
7803
8542
  async (tokenizedBody) => {
7804
8543
  try {
7805
8544
  const api = new PaymentAPI6(baseUrl);
@@ -7827,7 +8566,7 @@ function PayPalButton({
7827
8566
  },
7828
8567
  [baseUrl, sessionId, nonce, userId, email, firstName, lastName, chv, onComplete, onErrorChange]
7829
8568
  );
7830
- const dispatchTokenizedBody = useCallback5(
8569
+ const dispatchTokenizedBody = useCallback6(
7831
8570
  (body) => {
7832
8571
  if (onTokenizedBody) {
7833
8572
  onTokenizedBody(body);
@@ -7885,7 +8624,7 @@ function PayPalButton({
7885
8624
  }
7886
8625
  })();
7887
8626
  }, [flopay, dispatchTokenizedBody, onErrorChange]);
7888
- const handlePayPalConfirm = useCallback5(async () => {
8627
+ const handlePayPalConfirm = useCallback6(async () => {
7889
8628
  if (!flopay || !elements) return;
7890
8629
  try {
7891
8630
  setSubmitting(true);
@@ -7965,7 +8704,7 @@ function PayPalButton({
7965
8704
  }
7966
8705
 
7967
8706
  // src/automatic-payment-button.tsx
7968
- import { useCallback as useCallback6, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef7, useState as useState7 } from "react";
8707
+ import { useCallback as useCallback7, useEffect as useEffect9, useMemo as useMemo5, useRef as useRef8, useState as useState7 } from "react";
7969
8708
  import { PaymentAPI as PaymentAPI7 } from "@flopay/js";
7970
8709
  import { FloPayError as FloPayError8, resolveBillingApiUrl as resolveBillingApiUrl4, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme3, resolveTheme as resolveTheme3 } from "@flopay/shared";
7971
8710
  import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -8084,12 +8823,12 @@ function FloPayAutomaticPaymentButton({
8084
8823
  const [overlayStatus, setOverlayStatus] = useState7(null);
8085
8824
  const [overlayError, setOverlayError] = useState7(null);
8086
8825
  const [fallbackSession, setFallbackSession] = useState7(null);
8087
- const isMountedRef = useRef7(true);
8088
- const threeDsAbortRef = useRef7(null);
8089
- const fallbackSessionRef = useRef7(fallbackSession);
8090
- const onSuccessRef = useRef7(onSuccess);
8091
- const onErrorRef = useRef7(onError);
8092
- const onDeclineRef = useRef7(onDecline);
8826
+ const isMountedRef = useRef8(true);
8827
+ const threeDsAbortRef = useRef8(null);
8828
+ const fallbackSessionRef = useRef8(fallbackSession);
8829
+ const onSuccessRef = useRef8(onSuccess);
8830
+ const onErrorRef = useRef8(onError);
8831
+ const onDeclineRef = useRef8(onDecline);
8093
8832
  useEffect9(() => {
8094
8833
  fallbackSessionRef.current = fallbackSession;
8095
8834
  }, [fallbackSession]);
@@ -8110,7 +8849,7 @@ function FloPayAutomaticPaymentButton({
8110
8849
  };
8111
8850
  }, []);
8112
8851
  useEffect9(() => {
8113
- if (!fallbackSession || typeof window === "undefined") {
8852
+ if (typeof window === "undefined") {
8114
8853
  return;
8115
8854
  }
8116
8855
  const handleKeyDown = (event) => {
@@ -8122,14 +8861,14 @@ function FloPayAutomaticPaymentButton({
8122
8861
  return () => {
8123
8862
  window.removeEventListener("keydown", handleKeyDown);
8124
8863
  };
8125
- }, [fallbackSession]);
8126
- const emitDecline = useCallback6((error, method = DEFAULT_SAVED_PAYMENT_DECLINE_METHOD) => {
8864
+ }, []);
8865
+ const emitDecline = useCallback7((error, method = DEFAULT_SAVED_PAYMENT_DECLINE_METHOD) => {
8127
8866
  onDeclineRef.current?.(buildDeclineEvent(method, error, {
8128
8867
  code: error.code,
8129
8868
  declineCode: error.declineCode
8130
8869
  }));
8131
8870
  }, []);
8132
- const showSuccess = useCallback6(async (event) => {
8871
+ const showSuccess = useCallback7(async (event) => {
8133
8872
  if (!isMountedRef.current) return;
8134
8873
  setOverlayError(null);
8135
8874
  setOverlayStatus("success");
@@ -8137,7 +8876,7 @@ function FloPayAutomaticPaymentButton({
8137
8876
  if (!isMountedRef.current) return;
8138
8877
  onSuccessRef.current?.(event);
8139
8878
  }, []);
8140
- const showError = useCallback6(async (error, options) => {
8879
+ const showError = useCallback7(async (error, options) => {
8141
8880
  if (!isMountedRef.current) return;
8142
8881
  onErrorRef.current?.(error);
8143
8882
  if (options?.emitDecline) {
@@ -8147,7 +8886,7 @@ function FloPayAutomaticPaymentButton({
8147
8886
  setOverlayStatus("error");
8148
8887
  await sleep2(PROCESSING_OVERLAY_ERROR_DELAY_MS);
8149
8888
  }, [emitDecline]);
8150
- const processResolvedSession = useCallback6(async (apiResult, resolvedSessionId, options) => {
8889
+ const processResolvedSession = useCallback7(async (apiResult, resolvedSessionId, options) => {
8151
8890
  const session = apiResult.data.session ?? null;
8152
8891
  if (!session) {
8153
8892
  throw new FloPayError8("No session data returned", "api_error");
@@ -8303,7 +9042,7 @@ function FloPayAutomaticPaymentButton({
8303
9042
  showError,
8304
9043
  showSuccess
8305
9044
  ]);
8306
- const handleButtonClick = useCallback6(async (event) => {
9045
+ const handleButtonClick = useCallback7(async (event) => {
8307
9046
  buttonProps.onClick?.(event);
8308
9047
  if (event.defaultPrevented || disabled || isProcessing) {
8309
9048
  return;
@@ -8385,7 +9124,7 @@ function FloPayAutomaticPaymentButton({
8385
9124
  showError,
8386
9125
  showSuccess
8387
9126
  ]);
8388
- const handleFallbackComplete = useCallback6((result) => {
9127
+ const handleFallbackComplete = useCallback7((result) => {
8389
9128
  const activeFallback = fallbackSessionRef.current;
8390
9129
  setFallbackSession(null);
8391
9130
  onSuccessRef.current?.({
@@ -8395,10 +9134,10 @@ function FloPayAutomaticPaymentButton({
8395
9134
  autoCompleted: false
8396
9135
  });
8397
9136
  }, []);
8398
- const handleFallbackError = useCallback6((error) => {
9137
+ const handleFallbackError = useCallback7((error) => {
8399
9138
  onErrorRef.current?.(error);
8400
9139
  }, []);
8401
- const handleFallbackDecline = useCallback6((decline) => {
9140
+ const handleFallbackDecline = useCallback7((decline) => {
8402
9141
  onDeclineRef.current?.(decline);
8403
9142
  }, []);
8404
9143
  const themeBundle = useMemo5(() => resolveTheme3(theme), [theme]);