@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.cjs CHANGED
@@ -69,6 +69,59 @@ var CheckoutContext = (0, import_react.createContext)({
69
69
  error: null
70
70
  });
71
71
 
72
+ // src/telemetry-bridge.ts
73
+ var FLOPAY_TELEMETRY_BRIDGE = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.bridge.v1");
74
+ var TELEMETRY_REPORTER_FACTORY = /* @__PURE__ */ Symbol.for("@flopay/js.telemetry.reporter-factory.v1");
75
+ function noopTelemetryBridge() {
76
+ return {
77
+ error: () => {
78
+ },
79
+ log: () => {
80
+ },
81
+ performance: () => {
82
+ },
83
+ terminal: () => {
84
+ },
85
+ now: () => globalThis.performance?.now() ?? 0,
86
+ elapsed: (startedAt) => Math.max(0, (globalThis.performance?.now() ?? startedAt) - startedAt),
87
+ setCheckoutContext: () => {
88
+ },
89
+ beginCheckout: () => globalThis.performance?.now() ?? 0,
90
+ disable: () => {
91
+ },
92
+ flush: async () => {
93
+ },
94
+ destroy: () => {
95
+ }
96
+ };
97
+ }
98
+ function normalizeTelemetryBridge(source) {
99
+ const fallback = noopTelemetryBridge();
100
+ const bind = (candidate, defaultValue) => candidate ? candidate.bind(source) : defaultValue;
101
+ const now = bind(source.now, fallback.now);
102
+ return {
103
+ error: bind(source.error, fallback.error),
104
+ log: bind(source.log, fallback.log),
105
+ performance: bind(source.performance, fallback.performance),
106
+ terminal: bind(source.terminal, fallback.terminal),
107
+ now,
108
+ elapsed: source.elapsed ? source.elapsed.bind(source) : (startedAt) => Math.max(0, now() - startedAt),
109
+ setCheckoutContext: bind(source.setCheckoutContext, fallback.setCheckoutContext),
110
+ beginCheckout: bind(source.beginCheckout, fallback.beginCheckout),
111
+ disable: bind(source.disable, fallback.disable),
112
+ flush: bind(source.flush, fallback.flush),
113
+ destroy: bind(source.destroy, fallback.destroy)
114
+ };
115
+ }
116
+ function createTelemetryBridge(options) {
117
+ const factory = globalThis[TELEMETRY_REPORTER_FACTORY];
118
+ return normalizeTelemetryBridge(factory?.(options) ?? {});
119
+ }
120
+ function getFloPayTelemetryBridge(floPay) {
121
+ if (!floPay) return void 0;
122
+ return floPay[FLOPAY_TELEMETRY_BRIDGE];
123
+ }
124
+
72
125
  // src/provider.tsx
73
126
  var import_jsx_runtime = require("react/jsx-runtime");
74
127
  function FloPayProvider({
@@ -84,6 +137,18 @@ function FloPayProvider({
84
137
  paypalFloPayProp instanceof Promise || !paypalFloPayProp ? null : paypalFloPayProp
85
138
  );
86
139
  const [elements, setElements] = (0, import_react2.useState)(null);
140
+ const mountedAt = (0, import_react2.useRef)(null);
141
+ const renderedElements = (0, import_react2.useRef)(null);
142
+ const interactiveElements = (0, import_react2.useRef)(null);
143
+ (0, import_react2.useEffect)(() => {
144
+ if (!flopay) return;
145
+ const telemetry = getFloPayTelemetryBridge(flopay);
146
+ mountedAt.current = telemetry?.beginCheckout() ?? telemetry?.now() ?? 0;
147
+ telemetry?.log({ name: "checkout.mount", stage: "checkout_mount" });
148
+ return () => {
149
+ telemetry?.log({ name: "checkout.unmount", stage: "unmount" });
150
+ };
151
+ }, [flopay]);
87
152
  (0, import_react2.useEffect)(() => {
88
153
  let cancelled = false;
89
154
  if (floPayProp instanceof Promise) {
@@ -144,10 +209,38 @@ function FloPayProvider({
144
209
  options?.paymentMethodCreation,
145
210
  options?.setupFutureUsage
146
211
  ]);
212
+ (0, import_react2.useEffect)(() => {
213
+ if (!flopay || !elements || renderedElements.current === elements) return;
214
+ renderedElements.current = elements;
215
+ const telemetry = getFloPayTelemetryBridge(flopay);
216
+ telemetry?.log({ name: "checkout.rendered", stage: "checkout_render" });
217
+ telemetry?.performance({
218
+ stage: "checkout_render",
219
+ durationMs: telemetry.elapsed(mountedAt.current ?? 0),
220
+ durationMode: "machine"
221
+ });
222
+ }, [elements, flopay]);
223
+ const reportInteractive = (0, import_react2.useCallback)(() => {
224
+ if (!flopay || !elements || interactiveElements.current === elements) return;
225
+ interactiveElements.current = elements;
226
+ const telemetry = getFloPayTelemetryBridge(flopay);
227
+ telemetry?.log({ name: "checkout.interactive", stage: "checkout_interactive" });
228
+ telemetry?.performance({
229
+ stage: "checkout_interactive",
230
+ durationMs: telemetry.elapsed(mountedAt.current ?? 0),
231
+ durationMode: "machine"
232
+ });
233
+ }, [elements, flopay]);
147
234
  const resolvedBillingApiUrl = (0, import_shared.resolveBillingApiUrl)(options?.billingApiUrl);
148
235
  const value = (0, import_react2.useMemo)(
149
- () => ({ flopay, paypalFlopay, elements, billingApiUrl: resolvedBillingApiUrl }),
150
- [flopay, paypalFlopay, elements, resolvedBillingApiUrl]
236
+ () => ({
237
+ flopay,
238
+ paypalFlopay,
239
+ elements,
240
+ billingApiUrl: resolvedBillingApiUrl,
241
+ reportInteractive
242
+ }),
243
+ [flopay, paypalFlopay, elements, resolvedBillingApiUrl, reportInteractive]
151
244
  );
152
245
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FloPayContext.Provider, { value, children });
153
246
  }
@@ -241,7 +334,7 @@ function createElementComponent(elementType, displayName) {
241
334
  }) {
242
335
  const containerRef = (0, import_react4.useRef)(null);
243
336
  const elementRef = (0, import_react4.useRef)(null);
244
- const { elements } = (0, import_react4.useContext)(FloPayContext);
337
+ const { elements, reportInteractive } = (0, import_react4.useContext)(FloPayContext);
245
338
  (0, import_react4.useEffect)(() => {
246
339
  if (!elements || !containerRef.current) return;
247
340
  let mounted = true;
@@ -256,7 +349,10 @@ function createElementComponent(elementType, displayName) {
256
349
  element.mount(containerRef.current);
257
350
  elementRef.current = element;
258
351
  if (onChange) element.on("change", onChange);
259
- if (onReady) element.on("ready", onReady);
352
+ element.on("ready", () => {
353
+ reportInteractive?.();
354
+ onReady?.();
355
+ });
260
356
  if (onFocus) element.on("focus", onFocus);
261
357
  if (onBlur) element.on("blur", onBlur);
262
358
  if (onEscape) element.on("escape", onEscape);
@@ -271,7 +367,7 @@ function createElementComponent(elementType, displayName) {
271
367
  elementRef.current = null;
272
368
  }
273
369
  };
274
- }, [elements]);
370
+ }, [elements, reportInteractive]);
275
371
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: containerRef, className, id, style });
276
372
  }
277
373
  ElementComponent.displayName = displayName;
@@ -803,6 +899,19 @@ var import_paypal_js = require("@paypal/paypal-js");
803
899
  var import_js = require("@flopay/js");
804
900
  var import_shared4 = require("@flopay/shared");
805
901
 
902
+ // src/merchant-callback.ts
903
+ function invokeMerchantCallback(callback) {
904
+ if (!callback) return;
905
+ const reportFailure = (error) => {
906
+ console.error("[FloPay] Merchant callback failed; checkout continued.", error);
907
+ };
908
+ try {
909
+ void Promise.resolve(callback()).catch(reportFailure);
910
+ } catch (error) {
911
+ reportFailure(error);
912
+ }
913
+ }
914
+
806
915
  // src/external-method-recovery.ts
807
916
  var EXTERNAL_METHOD_CALLBACK_GRACE_MS = 600;
808
917
  function getProviderErrorMessage(err) {
@@ -859,7 +968,7 @@ var DIRECT_PAYPAL_RECOVERY_ACTION_STYLE = {
859
968
  function buildDirectPayPalRecoveryMessage(popupBlocked) {
860
969
  return popupBlocked ? `${DIRECT_PAYPAL_RECOVERY_MESSAGE} Allow pop-ups for this site, then try again.` : DIRECT_PAYPAL_RECOVERY_MESSAGE;
861
970
  }
862
- function DirectPayPalButton({
971
+ function DirectPayPalButtonImplementation({
863
972
  sessionId,
864
973
  nonce,
865
974
  billingApiUrl,
@@ -879,9 +988,57 @@ function DirectPayPalButton({
879
988
  runBeforeButtonClick,
880
989
  session,
881
990
  existingOrderId,
991
+ telemetry,
992
+ telemetryContext,
882
993
  debug = false
883
994
  }) {
995
+ const flopay = useFloPay();
996
+ const standaloneTelemetry = (0, import_react8.useMemo)(() => {
997
+ if (flopay) return null;
998
+ const reporter = createTelemetryBridge({
999
+ billingApiUrl,
1000
+ sdkPackage: "@flopay/react",
1001
+ sdkVersion: import_shared4.SDK_VERSION,
1002
+ enabled: telemetry !== false
1003
+ });
1004
+ reporter.setCheckoutContext(telemetryContext ?? {});
1005
+ reporter.beginCheckout(telemetryContext ?? {});
1006
+ return reporter;
1007
+ }, [
1008
+ billingApiUrl,
1009
+ flopay,
1010
+ telemetry,
1011
+ telemetryContext?.checkoutMode,
1012
+ telemetryContext?.layout
1013
+ ]);
1014
+ const floPayTelemetry = (0, import_react8.useMemo)(() => getFloPayTelemetryBridge(flopay), [flopay]);
1015
+ (0, import_react8.useEffect)(() => () => {
1016
+ if (!standaloneTelemetry) return;
1017
+ void standaloneTelemetry.flush().catch(() => {
1018
+ }).finally(() => standaloneTelemetry.destroy());
1019
+ }, [standaloneTelemetry]);
1020
+ const telemetrySource = (0, import_react8.useMemo)(() => ({
1021
+ error: (input) => {
1022
+ if (floPayTelemetry) floPayTelemetry.error(input);
1023
+ else standaloneTelemetry?.error(input);
1024
+ },
1025
+ log: (input) => {
1026
+ if (floPayTelemetry) floPayTelemetry.log(input);
1027
+ else standaloneTelemetry?.log(input);
1028
+ },
1029
+ performance: (input) => {
1030
+ if (floPayTelemetry) floPayTelemetry.performance(input);
1031
+ else standaloneTelemetry?.performance(input);
1032
+ },
1033
+ terminal: (input) => {
1034
+ if (floPayTelemetry) floPayTelemetry.terminal(input);
1035
+ else standaloneTelemetry?.terminal(input);
1036
+ },
1037
+ startTiming: () => floPayTelemetry?.now() ?? standaloneTelemetry?.now() ?? 0,
1038
+ elapsed: (startedAt) => floPayTelemetry?.elapsed(startedAt) ?? Math.max(0, (standaloneTelemetry?.now() ?? startedAt) - startedAt)
1039
+ }), [floPayTelemetry, standaloneTelemetry]);
884
1040
  const containerRef = (0, import_react8.useRef)(null);
1041
+ const providerStartedAt = (0, import_react8.useRef)(0);
885
1042
  const focusTargetRef = (0, import_react8.useRef)(null);
886
1043
  const pendingProviderFocusRef = (0, import_react8.useRef)(false);
887
1044
  const [ready, setReady] = (0, import_react8.useState)(false);
@@ -1070,7 +1227,7 @@ function DirectPayPalButton({
1070
1227
  };
1071
1228
  }, []);
1072
1229
  (0, import_react8.useEffect)(() => {
1073
- onLoadStateChangeRef.current?.(ready && !failed);
1230
+ invokeMerchantCallback(() => onLoadStateChangeRef.current?.(ready && !failed));
1074
1231
  }, [ready, failed]);
1075
1232
  const normalizedEnv = (0, import_shared4.normalizeGatewayEnvironment)(environment);
1076
1233
  (0, import_react8.useEffect)(() => {
@@ -1081,6 +1238,13 @@ function DirectPayPalButton({
1081
1238
  if (!clientId) {
1082
1239
  appendDebug("FAIL: clientId empty \u2014 gateway misconfigured");
1083
1240
  setFailed(true);
1241
+ telemetrySource.error({
1242
+ errorCode: "CONFIGURATION_INVALID",
1243
+ stage: "provider_load",
1244
+ provider: "paypal",
1245
+ paymentMethodCategory: "paypal",
1246
+ requestCategory: "provider_sdk"
1247
+ });
1084
1248
  console.error("[FloPay] DirectPayPal: clientId empty \u2014 gateway misconfigured");
1085
1249
  return;
1086
1250
  }
@@ -1088,8 +1252,33 @@ function DirectPayPalButton({
1088
1252
  appendDebug("FAIL: containerRef not attached");
1089
1253
  return;
1090
1254
  }
1255
+ providerStartedAt.current = telemetrySource.startTiming();
1256
+ telemetrySource.log({
1257
+ name: "provider.load.started",
1258
+ stage: "provider_load",
1259
+ provider: "paypal",
1260
+ paymentMethodCategory: "paypal"
1261
+ });
1091
1262
  let cancelled = false;
1092
1263
  let activeButtons = null;
1264
+ let overlayStartedAt = null;
1265
+ const finishOverlay = () => {
1266
+ if (overlayStartedAt === null) return;
1267
+ telemetrySource.log({
1268
+ name: "provider.overlay.returned",
1269
+ stage: "overlay_return",
1270
+ provider: "paypal",
1271
+ paymentMethodCategory: "paypal"
1272
+ });
1273
+ telemetrySource.performance({
1274
+ stage: "overlay_return",
1275
+ durationMs: telemetrySource.elapsed(overlayStartedAt),
1276
+ durationMode: "buyer",
1277
+ provider: "paypal",
1278
+ paymentMethodCategory: "paypal"
1279
+ });
1280
+ overlayStartedAt = null;
1281
+ };
1093
1282
  let rendered = false;
1094
1283
  const container = containerRef.current;
1095
1284
  let containerObserver = null;
@@ -1193,7 +1382,7 @@ function DirectPayPalButton({
1193
1382
  if (cancelled) return;
1194
1383
  if (isZoidLifecycleMessage(message)) return;
1195
1384
  const friendly = applyFriendlyMessageOverride(message) ?? message;
1196
- onErrorChangeRef.current?.(friendly);
1385
+ invokeMerchantCallback(() => onErrorChangeRef.current?.(friendly));
1197
1386
  };
1198
1387
  const markRenderFailed = (message) => {
1199
1388
  if (cancelled) return;
@@ -1202,24 +1391,55 @@ function DirectPayPalButton({
1202
1391
  return;
1203
1392
  }
1204
1393
  setFailed(true);
1394
+ if (!message.includes("paypal_ineligible")) {
1395
+ telemetrySource.error({
1396
+ errorCode: "PROVIDER_LOAD_FAILED",
1397
+ stage: "provider_load",
1398
+ provider: "paypal",
1399
+ paymentMethodCategory: "paypal",
1400
+ requestCategory: "provider_sdk"
1401
+ });
1402
+ }
1205
1403
  console.error("[FloPay] DirectPayPal load/render failure:", message);
1206
1404
  };
1207
1405
  setFailed(false);
1208
1406
  const dispatchTokenizedBody = async (body, prepared = beforeClickRef.current) => {
1209
1407
  const effectiveSessionId = prepared?.sessionId ?? sessionId;
1210
1408
  if (onTokenizedBodyRef.current) {
1211
- onTokenizedBodyRef.current(body, {
1409
+ await onTokenizedBodyRef.current(body, {
1212
1410
  sessionId: effectiveSessionId,
1213
1411
  accountPatch: prepared?.accountPatch,
1214
1412
  nonce: prepared?.nonce
1215
1413
  });
1216
1414
  return;
1217
1415
  }
1416
+ const processingStartedAt = telemetrySource.startTiming();
1417
+ telemetrySource.log({
1418
+ name: "payment.processing.started",
1419
+ stage: "processing",
1420
+ provider: "paypal",
1421
+ paymentMethodCategory: "paypal"
1422
+ });
1423
+ const finishProcessing = () => {
1424
+ telemetrySource.log({
1425
+ name: "payment.processing.completed",
1426
+ stage: "processing",
1427
+ provider: "paypal",
1428
+ paymentMethodCategory: "paypal"
1429
+ });
1430
+ telemetrySource.performance({
1431
+ stage: "processing",
1432
+ durationMs: telemetrySource.elapsed(processingStartedAt),
1433
+ durationMode: "machine",
1434
+ provider: "paypal",
1435
+ paymentMethodCategory: "paypal"
1436
+ });
1437
+ };
1218
1438
  try {
1219
1439
  const currentSession = sessionRef.current;
1220
1440
  const currentEmail = prepared?.accountPatch?.email ?? emailRef.current;
1221
1441
  const effectiveUserId = prepared?.accountPatch?.userId ?? currentSession?.customer?.id ?? currentSession?.accountData?.userId ?? "";
1222
- const api = new import_js.PaymentAPI(baseUrl);
1442
+ const api = new import_js.PaymentAPI(baseUrl, { telemetry: false });
1223
1443
  const response = await api.processPayment(
1224
1444
  effectiveUserId,
1225
1445
  {
@@ -1237,23 +1457,57 @@ function DirectPayPalButton({
1237
1457
  }
1238
1458
  );
1239
1459
  if (response.ok) {
1240
- onCompleteRef.current?.({ status: "succeeded", checkoutMethod: "paypal" });
1460
+ finishProcessing();
1461
+ telemetrySource.terminal({
1462
+ outcome: "payment_succeeded",
1463
+ provider: "paypal",
1464
+ paymentMethodCategory: "paypal"
1465
+ });
1466
+ } else {
1467
+ const json = await response.json().catch(() => null);
1468
+ const rawMessage = json?.["message"] ?? "PayPal payment failed.";
1469
+ const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
1470
+ finishProcessing();
1471
+ if (typeof json?.["declineCode"] === "string") {
1472
+ telemetrySource.terminal({
1473
+ outcome: "payment_declined",
1474
+ provider: "paypal",
1475
+ paymentMethodCategory: "paypal"
1476
+ });
1477
+ } else {
1478
+ telemetrySource.error({
1479
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1480
+ stage: "processing",
1481
+ provider: "paypal",
1482
+ paymentMethodCategory: "paypal",
1483
+ requestCategory: "process_payment",
1484
+ statusClass: response.status >= 500 ? "5xx" : response.status >= 400 ? "4xx" : response.status >= 300 ? "3xx" : "unknown"
1485
+ });
1486
+ }
1487
+ forwardError(message);
1488
+ invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
1489
+ code: json?.["code"],
1490
+ declineCode: json?.["declineCode"]
1491
+ })));
1241
1492
  return;
1242
1493
  }
1243
- const json = await response.json().catch(() => null);
1244
- const rawMessage = json?.["message"] ?? "PayPal payment failed.";
1245
- const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
1246
- forwardError(message);
1247
- onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
1248
- code: json?.["code"],
1249
- declineCode: json?.["declineCode"]
1250
- }));
1251
1494
  } catch (err) {
1495
+ finishProcessing();
1496
+ telemetrySource.error({
1497
+ errorCode: "NETWORK_REQUEST_FAILED",
1498
+ stage: "processing",
1499
+ provider: "paypal",
1500
+ paymentMethodCategory: "paypal",
1501
+ requestCategory: "process_payment",
1502
+ statusClass: "network_error"
1503
+ });
1252
1504
  const rawMessage = err instanceof Error ? err.message : "PayPal payment failed.";
1253
1505
  const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
1254
1506
  forwardError(message);
1255
- onDeclineRef.current?.(buildDeclineEvent("paypal", message));
1507
+ invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message)));
1508
+ return;
1256
1509
  }
1510
+ invokeMerchantCallback(() => onCompleteRef.current?.({ status: "succeeded", checkoutMethod: "paypal" }));
1257
1511
  };
1258
1512
  const createPaypalIntent = async (fallbackMessage) => {
1259
1513
  const prepared = beforeClickRef.current;
@@ -1262,6 +1516,13 @@ function DirectPayPalButton({
1262
1516
  const intentHeaders = { "Content-Type": "application/json" };
1263
1517
  const currentNonce = prepared?.nonce ?? nonceRef.current;
1264
1518
  if (currentNonce) intentHeaders["x-checkout-session-token"] = currentNonce;
1519
+ telemetrySource.log({
1520
+ name: "payment.intent.started",
1521
+ stage: "processing",
1522
+ provider: "paypal",
1523
+ paymentMethodCategory: "paypal",
1524
+ requestCategory: "intent_create"
1525
+ });
1265
1526
  const response = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
1266
1527
  method: "POST",
1267
1528
  headers: intentHeaders,
@@ -1280,6 +1541,14 @@ function DirectPayPalButton({
1280
1541
  if (!id) {
1281
1542
  throw new Error(fallbackMessage);
1282
1543
  }
1544
+ telemetrySource.log({
1545
+ name: "payment.intent.completed",
1546
+ stage: "processing",
1547
+ provider: "paypal",
1548
+ paymentMethodCategory: "paypal",
1549
+ requestCategory: "intent_create",
1550
+ statusClass: "2xx"
1551
+ });
1283
1552
  return id;
1284
1553
  };
1285
1554
  const effectRenderGeneration = renderGeneration;
@@ -1314,6 +1583,12 @@ function DirectPayPalButton({
1314
1583
  if (!cancelled && !paypal?.Buttons) appendDebug("FAIL: namespace missing Buttons factory");
1315
1584
  return;
1316
1585
  }
1586
+ telemetrySource.log({
1587
+ name: "provider.availability.checked",
1588
+ stage: "provider_ready",
1589
+ provider: "paypal",
1590
+ paymentMethodCategory: "paypal"
1591
+ });
1317
1592
  const handleApprove = async (data) => {
1318
1593
  if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
1319
1594
  const token = data.subscriptionID ?? data.orderID ?? "";
@@ -1322,9 +1597,10 @@ function DirectPayPalButton({
1322
1597
  if (!finishAttempt(context.generation)) return;
1323
1598
  approvalContextByTokenRef.current.delete(token);
1324
1599
  attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1600
+ finishOverlay();
1325
1601
  try {
1326
1602
  setSubmitting(true);
1327
- onErrorChangeRef.current?.(null);
1603
+ invokeMerchantCallback(() => onErrorChangeRef.current?.(null));
1328
1604
  if (!token) {
1329
1605
  throw new import_shared4.FloPayError(
1330
1606
  "PayPal did not return an approval token.",
@@ -1374,7 +1650,7 @@ function DirectPayPalButton({
1374
1650
  return;
1375
1651
  }
1376
1652
  }
1377
- onButtonClickRef.current?.("paypal");
1653
+ invokeMerchantCallback(() => onButtonClickRef.current?.("paypal"));
1378
1654
  const generation = startAttempt();
1379
1655
  const context = {
1380
1656
  generation,
@@ -1384,6 +1660,25 @@ function DirectPayPalButton({
1384
1660
  beforeClickRef.current = context;
1385
1661
  attemptContextBySurfaceRef.current.set(effectRenderGeneration, context);
1386
1662
  await actions.resolve();
1663
+ telemetrySource.log({
1664
+ name: "payment.method.selected",
1665
+ stage: "processing",
1666
+ provider: "paypal",
1667
+ paymentMethodCategory: "paypal"
1668
+ });
1669
+ telemetrySource.log({
1670
+ name: "provider.popup.opened",
1671
+ stage: "overlay_open",
1672
+ provider: "paypal",
1673
+ paymentMethodCategory: "paypal"
1674
+ });
1675
+ telemetrySource.log({
1676
+ name: "provider.overlay.opened",
1677
+ stage: "overlay_open",
1678
+ provider: "paypal",
1679
+ paymentMethodCategory: "paypal"
1680
+ });
1681
+ overlayStartedAt = telemetrySource.startTiming();
1387
1682
  },
1388
1683
  // When the SDK is already holding an order/subscription id from a
1389
1684
  // prior backend round-trip (the `paypal_direct_required` retry
@@ -1396,6 +1691,12 @@ function DirectPayPalButton({
1396
1691
  const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
1397
1692
  if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1398
1693
  beforeClickRef.current = null;
1694
+ finishOverlay();
1695
+ telemetrySource.terminal({
1696
+ outcome: "payment_cancelled",
1697
+ provider: "paypal",
1698
+ paymentMethodCategory: "paypal"
1699
+ });
1399
1700
  pendingProviderFocusRef.current = true;
1400
1701
  invalidateAttempt({ generation: context?.generation, remount: true, focus: false });
1401
1702
  },
@@ -1411,6 +1712,15 @@ function DirectPayPalButton({
1411
1712
  finishAttempt(context?.generation);
1412
1713
  return;
1413
1714
  }
1715
+ finishOverlay();
1716
+ const lower = message.toLowerCase();
1717
+ telemetrySource.error({
1718
+ errorCode: lower.includes("popup") && lower.includes("block") ? "POPUP_BLOCKED" : "PROVIDER_RUNTIME_FAILED",
1719
+ stage: "provider_ready",
1720
+ provider: "paypal",
1721
+ paymentMethodCategory: "paypal",
1722
+ requestCategory: "provider_sdk"
1723
+ });
1414
1724
  if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1415
1725
  beforeClickRef.current = null;
1416
1726
  invalidateAttempt({ generation: context?.generation, remount: true, showRetry: true, focus: true });
@@ -1422,6 +1732,12 @@ function DirectPayPalButton({
1422
1732
  });
1423
1733
  const eligible = buttons.isEligible();
1424
1734
  appendDebug(`isEligible=${eligible}`);
1735
+ telemetrySource.log({
1736
+ name: "provider.eligibility.checked",
1737
+ stage: "provider_ready",
1738
+ provider: "paypal",
1739
+ paymentMethodCategory: "paypal"
1740
+ });
1425
1741
  if (!eligible) {
1426
1742
  setReady(false);
1427
1743
  markRenderFailed(
@@ -1493,6 +1809,19 @@ function DirectPayPalButton({
1493
1809
  activeButtons = typedButtons;
1494
1810
  rendered = true;
1495
1811
  setReady(true);
1812
+ telemetrySource.log({
1813
+ name: "provider.ready",
1814
+ stage: "provider_ready",
1815
+ provider: "paypal",
1816
+ paymentMethodCategory: "paypal"
1817
+ });
1818
+ telemetrySource.performance({
1819
+ stage: "provider_ready",
1820
+ durationMs: telemetrySource.elapsed(providerStartedAt.current),
1821
+ durationMode: "machine",
1822
+ provider: "paypal",
1823
+ paymentMethodCategory: "paypal"
1824
+ });
1496
1825
  }).catch((err) => {
1497
1826
  const message = err instanceof Error ? err.message : "PayPal failed to render.";
1498
1827
  appendDebug(`render:rejected msg=${message.slice(0, 120)}`);
@@ -1514,7 +1843,7 @@ function DirectPayPalButton({
1514
1843
  });
1515
1844
  }
1516
1845
  };
1517
- }, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration]);
1846
+ }, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration, telemetrySource]);
1518
1847
  const debugPanel = debug ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1519
1848
  "pre",
1520
1849
  {
@@ -1595,6 +1924,12 @@ ${debugLines.join("\n")}`
1595
1924
  ] })
1596
1925
  );
1597
1926
  }
1927
+ function DirectPayPalButton(props) {
1928
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DirectPayPalButtonImplementation, { ...props });
1929
+ }
1930
+ function InstrumentedDirectPayPalButton(props) {
1931
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DirectPayPalButtonImplementation, { ...props });
1932
+ }
1598
1933
 
1599
1934
  // src/split-card-form.tsx
1600
1935
  var import_shared6 = require("@flopay/shared");
@@ -1911,6 +2246,7 @@ function PayPalButtonInner({
1911
2246
  onLoadStateChange,
1912
2247
  placeholderBorderRadius
1913
2248
  }) {
2249
+ const flopay = useFloPay();
1914
2250
  const stripe = (0, import_react_stripe_js.useStripe)();
1915
2251
  const elements = (0, import_react_stripe_js.useElements)();
1916
2252
  const [loadState, setLoadState] = (0, import_react9.useState)("loading");
@@ -2225,6 +2561,11 @@ function PayPalButtonInner({
2225
2561
  onCancel: () => {
2226
2562
  const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
2227
2563
  attemptContextBySurfaceRef.current.delete(surfaceKey);
2564
+ getFloPayTelemetryBridge(flopay)?.terminal({
2565
+ outcome: "payment_cancelled",
2566
+ provider: "paypal",
2567
+ paymentMethodCategory: "paypal"
2568
+ });
2228
2569
  invalidateAttempt(attemptContext?.generation);
2229
2570
  setShowRecoveryAction(false);
2230
2571
  pendingProviderFocusRef.current = true;
@@ -2268,6 +2609,7 @@ function WalletButtonInner({
2268
2609
  onLoadStateChange,
2269
2610
  placeholderBorderRadius
2270
2611
  }) {
2612
+ const flopay = useFloPay();
2271
2613
  const stripe = (0, import_react_stripe_js.useStripe)();
2272
2614
  const elements = (0, import_react_stripe_js.useElements)();
2273
2615
  const [loadState, setLoadState] = (0, import_react9.useState)("loading");
@@ -2489,6 +2831,13 @@ function WalletButtonInner({
2489
2831
  },
2490
2832
  onLoadError: (_event) => {
2491
2833
  setLoadState("load_error");
2834
+ getFloPayTelemetryBridge(flopay)?.error({
2835
+ errorCode: "PROVIDER_LOAD_FAILED",
2836
+ stage: "provider_load",
2837
+ provider: "stripe",
2838
+ paymentMethodCategory: "wallet",
2839
+ requestCategory: "provider_sdk"
2840
+ });
2492
2841
  },
2493
2842
  onClick: async (event) => {
2494
2843
  lastWalletProviderMethodRef.current = event.expressPaymentType;
@@ -2515,6 +2864,11 @@ function WalletButtonInner({
2515
2864
  onCancel: () => {
2516
2865
  const attemptContext = attemptContextBySurfaceRef.current.get(surfaceKey);
2517
2866
  attemptContextBySurfaceRef.current.delete(surfaceKey);
2867
+ getFloPayTelemetryBridge(flopay)?.terminal({
2868
+ outcome: "payment_cancelled",
2869
+ provider: "stripe",
2870
+ paymentMethodCategory: "wallet"
2871
+ });
2518
2872
  invalidateAttempt(attemptContext?.generation);
2519
2873
  setShowRecoveryAction(false);
2520
2874
  pendingProviderFocusRef.current = true;
@@ -2685,6 +3039,7 @@ function StripeMethodInlineForm({
2685
3039
  submitButtonStyle,
2686
3040
  errorText
2687
3041
  }) {
3042
+ const flopay = useFloPay();
2688
3043
  const stripe = (0, import_react_stripe_js.useStripe)();
2689
3044
  const elements = (0, import_react_stripe_js.useElements)();
2690
3045
  const [submitting, setSubmitting] = (0, import_react9.useState)(false);
@@ -2837,7 +3192,16 @@ function StripeMethodInlineForm({
2837
3192
  import_react_stripe_js.PaymentElement,
2838
3193
  {
2839
3194
  onReady: () => setLoadState("ready"),
2840
- onLoadError: () => setLoadState("load_error"),
3195
+ onLoadError: () => {
3196
+ setLoadState("load_error");
3197
+ getFloPayTelemetryBridge(flopay)?.error({
3198
+ errorCode: "PROVIDER_LOAD_FAILED",
3199
+ stage: "provider_load",
3200
+ provider: "stripe",
3201
+ paymentMethodCategory: "apm",
3202
+ requestCategory: "provider_sdk"
3203
+ });
3204
+ },
2841
3205
  onChange: (event) => {
2842
3206
  const evRecord = event;
2843
3207
  setIsMethodComplete(!!evRecord.complete);
@@ -5753,7 +6117,7 @@ async function recover3DSRedirectResult({
5753
6117
  return null;
5754
6118
  }
5755
6119
  try {
5756
- const api = new import_js3.PaymentAPI(billingApiUrl);
6120
+ const api = new import_js3.PaymentAPI(billingApiUrl, { telemetry: false });
5757
6121
  const unified = await api.getUnifiedCheckoutSession(sessionId, nonce);
5758
6122
  const refreshedToken = unified.data.stripe?.clientSecret;
5759
6123
  if (isStripePaymentIntentClientSecret(refreshedToken)) {
@@ -5772,7 +6136,8 @@ async function processSavedPaymentForMode({
5772
6136
  session,
5773
6137
  nonce,
5774
6138
  tokenizedData,
5775
- returnUrl
6139
+ returnUrl,
6140
+ telemetry
5776
6141
  }) {
5777
6142
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
5778
6143
  const resolvedSessionId = sessionId ?? session.id;
@@ -5783,7 +6148,10 @@ async function processSavedPaymentForMode({
5783
6148
  const lastName = session.customer?.lastName ?? session.accountData?.lastName ?? "";
5784
6149
  const country = session.customer?.country ?? session.accountData?.country ?? void 0;
5785
6150
  const zip = session.customer?.zip ?? session.accountData?.zip ?? void 0;
5786
- const api = new import_js3.PaymentAPI(baseUrl);
6151
+ const api = new import_js3.PaymentAPI(
6152
+ baseUrl,
6153
+ telemetry === false ? { telemetry: false } : void 0
6154
+ );
5787
6155
  const response = await retryOnceOnFetchFailure(() => api.processPayment(customerId, {
5788
6156
  sessionId: resolvedSessionId,
5789
6157
  nonce: resolvedNonce,
@@ -5860,7 +6228,8 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
5860
6228
  billingApiUrl,
5861
6229
  sessionId,
5862
6230
  session,
5863
- returnUrl
6231
+ returnUrl,
6232
+ telemetry
5864
6233
  }) {
5865
6234
  const stripe = flopay?.getRawProvider();
5866
6235
  if (!stripe) {
@@ -5943,6 +6312,7 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
5943
6312
  billingApiUrl,
5944
6313
  sessionId,
5945
6314
  session,
6315
+ telemetry,
5946
6316
  tokenizedData: {
5947
6317
  id: paymentIntent.id,
5948
6318
  type: "card",
@@ -5964,7 +6334,8 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
5964
6334
  billingApiUrl,
5965
6335
  sessionId,
5966
6336
  session,
5967
- returnUrl
6337
+ returnUrl,
6338
+ telemetry
5968
6339
  });
5969
6340
  }
5970
6341
  throw Object.assign(
@@ -6070,7 +6441,8 @@ async function loadSavedPaymentProviders({
6070
6441
  publishableKey,
6071
6442
  paypalPublishableKey,
6072
6443
  billingApiUrl,
6073
- locale
6444
+ locale,
6445
+ telemetry
6074
6446
  }) {
6075
6447
  if (!publishableKey) {
6076
6448
  return { flopay: null, paypalFlopay: null };
@@ -6079,11 +6451,13 @@ async function loadSavedPaymentProviders({
6079
6451
  const [instance, paypalInstanceOrError] = await Promise.all([
6080
6452
  (0, import_js3.loadFloPay)(publishableKey, {
6081
6453
  billingApiUrl,
6082
- locale
6454
+ locale,
6455
+ telemetry
6083
6456
  }),
6084
6457
  needsSeparatePaypal ? (0, import_js3.loadFloPay)(paypalPublishableKey, {
6085
6458
  billingApiUrl,
6086
- locale
6459
+ locale,
6460
+ telemetry
6087
6461
  }).catch((err) => {
6088
6462
  console.warn("[FloPay] Failed to load PayPal Stripe instance:", err);
6089
6463
  return null;
@@ -6103,6 +6477,33 @@ var sessionInflightMap = /* @__PURE__ */ new Map();
6103
6477
  function sleep(ms) {
6104
6478
  return new Promise((resolve) => setTimeout(resolve, ms));
6105
6479
  }
6480
+ function createStandaloneTelemetryReporter(billingApiUrl, enabled, context) {
6481
+ const reporter = createTelemetryBridge({
6482
+ billingApiUrl,
6483
+ sdkPackage: "@flopay/react",
6484
+ sdkVersion: import_shared8.SDK_VERSION,
6485
+ enabled: enabled !== false
6486
+ });
6487
+ reporter.beginCheckout(context);
6488
+ return reporter;
6489
+ }
6490
+ function finishStandaloneTelemetry(reporter) {
6491
+ void reporter.flush().catch(() => {
6492
+ }).finally(() => reporter.destroy());
6493
+ }
6494
+ function reportStandaloneTelemetryError(billingApiUrl, enabled, context, errorCode, stage) {
6495
+ const reporter = createStandaloneTelemetryReporter(billingApiUrl, enabled, context);
6496
+ reporter.error({
6497
+ errorCode,
6498
+ stage,
6499
+ paymentMethodCategory: "unknown",
6500
+ ...stage === "session_read" ? { requestCategory: "session_read" } : {}
6501
+ });
6502
+ finishStandaloneTelemetry(reporter);
6503
+ }
6504
+ function isExpectedExistingSessionError(error) {
6505
+ return error.type === "validation_error" || error.code === "checkout_session_not_found" || error.code === "checkout_session_expired" || error.code === "checkout_session_completed" || error.code === "session_auto_completed";
6506
+ }
6106
6507
  function resolveDirectPaypalConfig(unified) {
6107
6508
  const clientId = unified?.data.paypal?.publishableKey;
6108
6509
  if (!clientId) return void 0;
@@ -6207,6 +6608,7 @@ function FloPayCheckout({
6207
6608
  nonce: nonceProp,
6208
6609
  createSession: createSessionParams,
6209
6610
  billingApiUrl,
6611
+ telemetry,
6210
6612
  appearance: appearanceOverride,
6211
6613
  locale,
6212
6614
  loading: loadingNode,
@@ -6272,12 +6674,22 @@ function FloPayCheckout({
6272
6674
  const autoCheckoutAttempted = (0, import_react10.useRef)(false);
6273
6675
  const paypalResumeAttempted = (0, import_react10.useRef)(false);
6274
6676
  const savedPaymentKeysRef = (0, import_react10.useRef)(null);
6677
+ const telemetryCheckoutContext = (0, import_react10.useMemo)(() => ({
6678
+ checkoutMode: checkoutModeProp ?? currentMode,
6679
+ layout: children ? "unknown" : layout === "buttons" ? "buttons" : "embedded"
6680
+ }), [checkoutModeProp, children, currentMode, layout]);
6275
6681
  const onCompleteRef = (0, import_react10.useRef)(onComplete);
6276
6682
  onCompleteRef.current = onComplete;
6277
6683
  const onErrorRef = (0, import_react10.useRef)(onError);
6278
6684
  onErrorRef.current = onError;
6279
6685
  const onDeclineRef = (0, import_react10.useRef)(onDecline);
6280
6686
  onDeclineRef.current = onDecline;
6687
+ (0, import_react10.useEffect)(() => {
6688
+ getFloPayTelemetryBridge(flopay)?.setCheckoutContext(telemetryCheckoutContext);
6689
+ if (paypalFlopay && paypalFlopay !== flopay) {
6690
+ getFloPayTelemetryBridge(paypalFlopay)?.setCheckoutContext(telemetryCheckoutContext);
6691
+ }
6692
+ }, [flopay, paypalFlopay, telemetryCheckoutContext]);
6281
6693
  const onSessionCompletedRef = (0, import_react10.useRef)(onSessionCompleted);
6282
6694
  onSessionCompletedRef.current = onSessionCompleted;
6283
6695
  (0, import_react10.useEffect)(() => {
@@ -6317,9 +6729,11 @@ function FloPayCheckout({
6317
6729
  }, [initialErrorMessage]);
6318
6730
  const emitDecline = (0, import_react10.useCallback)(
6319
6731
  (method, input, overrides) => {
6320
- onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
6732
+ invokeMerchantCallback(() => {
6733
+ onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
6734
+ });
6321
6735
  },
6322
- []
6736
+ [invokeMerchantCallback]
6323
6737
  );
6324
6738
  const runSavedPaymentFlow = (0, import_react10.useCallback)(
6325
6739
  async (sess, options) => {
@@ -6327,10 +6741,98 @@ function FloPayCheckout({
6327
6741
  setModeOverlayError(null);
6328
6742
  setModeOverlayStatus("processing");
6329
6743
  const activeSessionId2 = options?.sessionId ?? sess.id;
6744
+ const activeFloPay = flopayRef.current ?? paypalFlopayRef.current;
6745
+ const activeTelemetry = getFloPayTelemetryBridge(activeFloPay);
6746
+ const standaloneTelemetry = activeFloPay ? null : createStandaloneTelemetryReporter(
6747
+ resolvedBillingUrl,
6748
+ telemetry,
6749
+ telemetryCheckoutContext
6750
+ );
6751
+ const telemetrySource = {
6752
+ log: (input) => {
6753
+ if (activeTelemetry) activeTelemetry.log(input);
6754
+ else standaloneTelemetry?.log(input);
6755
+ },
6756
+ error: (input) => {
6757
+ if (activeTelemetry) activeTelemetry.error(input);
6758
+ else standaloneTelemetry?.error(input);
6759
+ },
6760
+ terminal: (input) => {
6761
+ if (activeTelemetry) activeTelemetry.terminal(input);
6762
+ else standaloneTelemetry?.terminal(input);
6763
+ },
6764
+ performance: (input) => {
6765
+ if (activeTelemetry) activeTelemetry.performance(input);
6766
+ else standaloneTelemetry?.performance(input);
6767
+ },
6768
+ now: () => activeTelemetry?.now() ?? standaloneTelemetry?.now() ?? 0,
6769
+ elapsed: (startedAt) => activeTelemetry?.elapsed(startedAt) ?? Math.max(0, (standaloneTelemetry?.now() ?? startedAt) - startedAt)
6770
+ };
6771
+ const processingStartedAt = telemetrySource.now();
6772
+ let recoveryFlow = Boolean(
6773
+ options?.initialAutoProcessingError || options?.initialAutoProcessingPending
6774
+ );
6775
+ let recoveryStarted = false;
6776
+ let recoveryStartedAt;
6777
+ const startRecovery = () => {
6778
+ if (recoveryStarted) return;
6779
+ recoveryStarted = true;
6780
+ recoveryFlow = true;
6781
+ recoveryStartedAt = telemetrySource.now();
6782
+ telemetrySource.log({
6783
+ name: "checkout.recovery.started",
6784
+ stage: "recovery",
6785
+ paymentMethodCategory: "saved"
6786
+ });
6787
+ telemetrySource.log({
6788
+ name: "operation.recovery.started",
6789
+ stage: "recovery",
6790
+ paymentMethodCategory: "saved"
6791
+ });
6792
+ };
6793
+ let processingFinished = false;
6794
+ const finishProcessing = () => {
6795
+ if (processingFinished) return;
6796
+ processingFinished = true;
6797
+ telemetrySource.log({
6798
+ name: "payment.processing.completed",
6799
+ stage: "processing",
6800
+ paymentMethodCategory: "saved"
6801
+ });
6802
+ telemetrySource.performance({
6803
+ stage: "processing",
6804
+ durationMs: telemetrySource.elapsed(processingStartedAt),
6805
+ durationMode: "machine",
6806
+ paymentMethodCategory: "saved"
6807
+ });
6808
+ if (recoveryStartedAt !== void 0) {
6809
+ telemetrySource.performance({
6810
+ stage: "recovery",
6811
+ durationMs: telemetrySource.elapsed(recoveryStartedAt),
6812
+ durationMode: "machine",
6813
+ paymentMethodCategory: "saved"
6814
+ });
6815
+ }
6816
+ };
6817
+ telemetrySource.log({
6818
+ name: "payment.method.selected",
6819
+ stage: "processing",
6820
+ paymentMethodCategory: "saved"
6821
+ });
6822
+ telemetrySource.log({
6823
+ name: "payment.processing.started",
6824
+ stage: "processing",
6825
+ paymentMethodCategory: "saved"
6826
+ });
6827
+ if (recoveryFlow) {
6828
+ startRecovery();
6829
+ }
6830
+ let completionCallback;
6330
6831
  try {
6331
6832
  const redirectResult = getRedirectResultFromCheckoutProcessError(options?.initialAutoProcessingError);
6332
6833
  let paymentResult;
6333
6834
  if (redirectResult) {
6835
+ startRecovery();
6334
6836
  if (redirectResult.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
6335
6837
  persistPayPalResumeState({
6336
6838
  sessionId: activeSessionId2,
@@ -6344,13 +6846,14 @@ function FloPayCheckout({
6344
6846
  attempt3DS: options?.attempt3DS,
6345
6847
  billingApiUrl: resolvedBillingUrl,
6346
6848
  sessionId: activeSessionId2,
6347
- session: sess
6849
+ session: sess,
6850
+ telemetry: false
6348
6851
  });
6349
6852
  if (redirectResult.type === "paypal_redirect_required") {
6350
6853
  clearPayPalResumeState();
6351
6854
  }
6352
6855
  } else if (options?.initialAutoProcessingPending) {
6353
- const api = new import_js4.PaymentAPI(resolvedBillingUrl);
6856
+ const api = new import_js4.PaymentAPI(resolvedBillingUrl, { telemetry: false });
6354
6857
  const completed = await api.waitForCheckoutSessionCompletion(options.initialAutoProcessingPending.sessionId, {
6355
6858
  initialDelayMs: options.initialAutoProcessingPending.retryAfterMs
6356
6859
  });
@@ -6378,11 +6881,13 @@ function FloPayCheckout({
6378
6881
  const result = await processSavedPaymentForMode({
6379
6882
  billingApiUrl: resolvedBillingUrl,
6380
6883
  sessionId: activeSessionId2,
6381
- session: sess
6884
+ session: sess,
6885
+ telemetry: false
6382
6886
  });
6383
6887
  if (result.type === "success") {
6384
6888
  paymentResult = result.result;
6385
6889
  } else {
6890
+ startRecovery();
6386
6891
  if (result.type === "paypal_redirect_required" && savedPaymentKeysRef.current?.publishableKey) {
6387
6892
  persistPayPalResumeState({
6388
6893
  sessionId: activeSessionId2,
@@ -6396,7 +6901,8 @@ function FloPayCheckout({
6396
6901
  attempt3DS: options?.attempt3DS,
6397
6902
  billingApiUrl: resolvedBillingUrl,
6398
6903
  sessionId: activeSessionId2,
6399
- session: sess
6904
+ session: sess,
6905
+ telemetry: false
6400
6906
  });
6401
6907
  if (result.type === "paypal_redirect_required") {
6402
6908
  clearPayPalResumeState();
@@ -6404,20 +6910,57 @@ function FloPayCheckout({
6404
6910
  }
6405
6911
  }
6406
6912
  if (activeSessionId2) markSessionRecentlyCompleted(activeSessionId2);
6913
+ finishProcessing();
6914
+ if (recoveryFlow) {
6915
+ telemetrySource.log({
6916
+ name: "checkout.recovery.completed",
6917
+ stage: "recovery",
6918
+ paymentMethodCategory: "saved"
6919
+ });
6920
+ telemetrySource.log({
6921
+ name: "operation.recovery.completed",
6922
+ stage: "recovery",
6923
+ paymentMethodCategory: "saved"
6924
+ });
6925
+ }
6926
+ telemetrySource.terminal({
6927
+ outcome: "payment_succeeded",
6928
+ paymentMethodCategory: "saved"
6929
+ });
6407
6930
  setModeOverlayStatus("success");
6408
6931
  await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
6409
- onCompleteRef.current?.(paymentResult);
6410
- return true;
6932
+ completionCallback = () => onCompleteRef.current?.(paymentResult);
6411
6933
  } catch (err) {
6412
6934
  const floPayErr = normalizeSavedPaymentError(err);
6413
6935
  const method = floPayErr.checkoutMethod ?? DEFAULT_SAVED_PAYMENT_DECLINE_METHOD;
6414
6936
  setModeError(floPayErr.message);
6415
6937
  setModeOverlayError(floPayErr.message);
6416
6938
  if (options?.fallbackToFull) {
6939
+ telemetrySource.log({
6940
+ name: "operation.fallback",
6941
+ stage: "recovery",
6942
+ paymentMethodCategory: "saved"
6943
+ });
6417
6944
  setCurrentMode("full");
6418
6945
  replaceCheckoutModeQueryParam("full");
6419
6946
  }
6420
- onErrorRef.current?.(floPayErr);
6947
+ finishProcessing();
6948
+ const expectedDecline = Boolean(
6949
+ floPayErr.declineCode || floPayErr.code?.toLowerCase().includes("declin")
6950
+ );
6951
+ if (expectedDecline) {
6952
+ telemetrySource.terminal({
6953
+ outcome: "payment_declined",
6954
+ paymentMethodCategory: "saved"
6955
+ });
6956
+ } else {
6957
+ telemetrySource.error({
6958
+ errorCode: recoveryFlow ? "RECOVERY_FAILED" : "PAYMENT_PROCESSING_FAILED",
6959
+ stage: recoveryFlow ? "recovery" : "processing",
6960
+ paymentMethodCategory: "saved"
6961
+ });
6962
+ }
6963
+ invokeMerchantCallback(() => onErrorRef.current?.(floPayErr));
6421
6964
  emitDecline(method, floPayErr, {
6422
6965
  code: floPayErr.code,
6423
6966
  declineCode: floPayErr.declineCode
@@ -6427,16 +6970,35 @@ function FloPayCheckout({
6427
6970
  sleep(PROCESSING_OVERLAY_ERROR_DELAY_MS),
6428
6971
  options?.ensureProvidersReady ? options.ensureProvidersReady() : Promise.resolve()
6429
6972
  ]);
6973
+ if (recoveryFlow) {
6974
+ telemetrySource.log({
6975
+ name: "checkout.recovery.completed",
6976
+ stage: "recovery",
6977
+ paymentMethodCategory: "saved"
6978
+ });
6979
+ telemetrySource.log({
6980
+ name: "operation.recovery.completed",
6981
+ stage: "recovery",
6982
+ paymentMethodCategory: "saved"
6983
+ });
6984
+ }
6430
6985
  return false;
6431
6986
  } finally {
6987
+ finishProcessing();
6988
+ if (standaloneTelemetry) finishStandaloneTelemetry(standaloneTelemetry);
6432
6989
  setModeOverlayStatus(null);
6433
6990
  setModeOverlayError(null);
6434
6991
  }
6992
+ invokeMerchantCallback(completionCallback);
6993
+ return true;
6435
6994
  },
6436
6995
  [
6437
6996
  emitDecline,
6997
+ invokeMerchantCallback,
6438
6998
  normalizeSavedPaymentError,
6439
- resolvedBillingUrl
6999
+ resolvedBillingUrl,
7000
+ telemetry,
7001
+ telemetryCheckoutContext
6440
7002
  ]
6441
7003
  );
6442
7004
  (0, import_react10.useEffect)(() => {
@@ -6454,6 +7016,8 @@ function FloPayCheckout({
6454
7016
  }
6455
7017
  paypalResumeAttempted.current = true;
6456
7018
  void (async () => {
7019
+ let resumeTelemetry;
7020
+ let redirectResumeStartedAt = 0;
6457
7021
  setModeError(null);
6458
7022
  setModeOverlayError(null);
6459
7023
  setModeOverlayStatus("processing");
@@ -6461,7 +7025,11 @@ function FloPayCheckout({
6461
7025
  try {
6462
7026
  if (params.get("redirect_status") === "failed") {
6463
7027
  throw Object.assign(
6464
- new import_shared8.FloPayError("PayPal payment was declined. Please try again.", "api_error"),
7028
+ new import_shared8.FloPayError(
7029
+ "PayPal payment was declined. Please try again.",
7030
+ "api_error",
7031
+ { declineCode: "paypal_redirect_failed" }
7032
+ ),
6465
7033
  { checkoutMethod: "paypal" }
6466
7034
  );
6467
7035
  }
@@ -6472,7 +7040,22 @@ function FloPayCheckout({
6472
7040
  publishableKey: resumeState.publishableKey,
6473
7041
  paypalPublishableKey: resumeState.paypalPublishableKey,
6474
7042
  billingApiUrl: resolvedBillingUrl,
6475
- locale
7043
+ locale,
7044
+ telemetry
7045
+ });
7046
+ resumeTelemetry = getFloPayTelemetryBridge(resumePaypalFlopay ?? resumeFlopay);
7047
+ redirectResumeStartedAt = resumeTelemetry?.now() ?? 0;
7048
+ resumeTelemetry?.log({
7049
+ name: "provider.redirect.resumed",
7050
+ stage: "redirect_resume",
7051
+ provider: "paypal",
7052
+ paymentMethodCategory: "paypal"
7053
+ });
7054
+ resumeTelemetry?.log({
7055
+ name: "operation.recovery.started",
7056
+ stage: "recovery",
7057
+ provider: "paypal",
7058
+ paymentMethodCategory: "paypal"
6476
7059
  });
6477
7060
  const paypalStripe = (resumePaypalFlopay ?? resumeFlopay)?.getRawProvider();
6478
7061
  if (!paypalStripe) {
@@ -6502,7 +7085,7 @@ function FloPayCheckout({
6502
7085
  const paymentMethodId = typeof paymentIntent.payment_method === "string" ? paymentIntent.payment_method : paymentIntent.payment_method?.id;
6503
7086
  let finalResultStatus = resultStatus;
6504
7087
  if (resumeState.sessionId) {
6505
- const resumeApi = new import_js4.PaymentAPI(resolvedBillingUrl);
7088
+ const resumeApi = new import_js4.PaymentAPI(resolvedBillingUrl, { telemetry: false });
6506
7089
  const resumeSessionResult = await resumeApi.getUnifiedCheckoutSession(resumeState.sessionId);
6507
7090
  const resumeSession = resumeSessionResult.data.session;
6508
7091
  if (resumeSession && resumeSession.status !== "complete") {
@@ -6510,6 +7093,7 @@ function FloPayCheckout({
6510
7093
  billingApiUrl: resolvedBillingUrl,
6511
7094
  sessionId: resumeState.sessionId,
6512
7095
  session: resumeSession,
7096
+ telemetry: false,
6513
7097
  tokenizedData: {
6514
7098
  id: paymentMethodId ?? paymentIntent.id,
6515
7099
  type: "card",
@@ -6527,20 +7111,84 @@ function FloPayCheckout({
6527
7111
  }
6528
7112
  }
6529
7113
  if (resumeState.sessionId) markSessionRecentlyCompleted(resumeState.sessionId);
7114
+ resumeTelemetry?.log({
7115
+ name: "operation.recovery.completed",
7116
+ stage: "recovery",
7117
+ provider: "paypal",
7118
+ paymentMethodCategory: "paypal"
7119
+ });
7120
+ resumeTelemetry?.performance({
7121
+ stage: "redirect_resume",
7122
+ durationMs: resumeTelemetry.elapsed(redirectResumeStartedAt),
7123
+ durationMode: "machine",
7124
+ provider: "paypal",
7125
+ paymentMethodCategory: "paypal"
7126
+ });
7127
+ resumeTelemetry?.terminal({
7128
+ outcome: "payment_succeeded",
7129
+ provider: "paypal",
7130
+ paymentMethodCategory: "paypal"
7131
+ });
6530
7132
  setModeOverlayStatus("success");
6531
7133
  await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
6532
- onCompleteRef.current?.({
7134
+ invokeMerchantCallback(() => onCompleteRef.current?.({
6533
7135
  status: finalResultStatus,
6534
7136
  paymentIntentId: paymentIntent.id,
6535
7137
  paymentMethodId,
6536
7138
  checkoutMethod: "paypal"
6537
- });
7139
+ }));
6538
7140
  } catch (err) {
6539
7141
  const floPayErr = normalizeSavedPaymentError(err);
6540
7142
  const method = floPayErr.checkoutMethod ?? "paypal";
7143
+ const expectedDecline = Boolean(
7144
+ floPayErr.declineCode || floPayErr.code?.toLowerCase().includes("declin")
7145
+ );
7146
+ if (resumeTelemetry) {
7147
+ resumeTelemetry.performance({
7148
+ stage: "redirect_resume",
7149
+ durationMs: resumeTelemetry.elapsed(redirectResumeStartedAt),
7150
+ durationMode: "machine",
7151
+ provider: "paypal",
7152
+ paymentMethodCategory: "paypal"
7153
+ });
7154
+ if (expectedDecline) {
7155
+ resumeTelemetry.terminal({
7156
+ outcome: "payment_declined",
7157
+ provider: "paypal",
7158
+ paymentMethodCategory: "paypal"
7159
+ });
7160
+ } else {
7161
+ resumeTelemetry.error({
7162
+ errorCode: "REDIRECT_RESUME_FAILED",
7163
+ stage: "redirect_resume",
7164
+ provider: "paypal",
7165
+ paymentMethodCategory: "paypal"
7166
+ });
7167
+ }
7168
+ } else if (expectedDecline) {
7169
+ const reporter = createStandaloneTelemetryReporter(
7170
+ resolvedBillingUrl,
7171
+ telemetry,
7172
+ telemetryCheckoutContext
7173
+ );
7174
+ reporter.terminal({
7175
+ outcome: "payment_declined",
7176
+ provider: "paypal",
7177
+ paymentMethodCategory: "paypal"
7178
+ });
7179
+ finishStandaloneTelemetry(reporter);
7180
+ } else {
7181
+ reportStandaloneTelemetryError(
7182
+ resolvedBillingUrl,
7183
+ telemetry,
7184
+ telemetryCheckoutContext,
7185
+ "REDIRECT_RESUME_FAILED",
7186
+ "redirect_resume"
7187
+ );
7188
+ }
6541
7189
  setModeError(floPayErr.message);
6542
7190
  setModeOverlayError(floPayErr.message);
6543
- onErrorRef.current?.(floPayErr);
7191
+ invokeMerchantCallback(() => onErrorRef.current?.(floPayErr));
6544
7192
  emitDecline(method, floPayErr, {
6545
7193
  code: floPayErr.code,
6546
7194
  declineCode: floPayErr.declineCode
@@ -6555,7 +7203,7 @@ function FloPayCheckout({
6555
7203
  setConfirmProcessing(false);
6556
7204
  }
6557
7205
  })();
6558
- }, [emitDecline, locale, normalizeSavedPaymentError, resolvedBillingUrl]);
7206
+ }, [emitDecline, invokeMerchantCallback, locale, normalizeSavedPaymentError, resolvedBillingUrl, telemetry]);
6559
7207
  const initializedHashRef = (0, import_react10.useRef)(null);
6560
7208
  function hashCreateParams(params) {
6561
7209
  const key = JSON.stringify({
@@ -6605,42 +7253,105 @@ function FloPayCheckout({
6605
7253
  setModeOverlayStatus(null);
6606
7254
  }, [createSessionHash, initialErrorMessage, sessionIdProp]);
6607
7255
  async function resolveInlineSession(params, cacheKey) {
6608
- const api = new import_js4.PaymentAPI(resolvedBillingUrl);
6609
- const cached = readCachedInlineSession(cacheKey);
6610
- let sid = cached?.sid ?? null;
6611
- let realResult = null;
6612
- if (sid) {
6613
- try {
6614
- realResult = await api.getUnifiedCheckoutSession(sid, cached?.nonce);
6615
- const status = realResult.data.session?.status;
6616
- if (status === "complete") {
6617
- if (wasSessionRecentlyCompleted(sid)) {
6618
- return { sid, result: realResult };
7256
+ const reporter = createStandaloneTelemetryReporter(
7257
+ resolvedBillingUrl,
7258
+ telemetry,
7259
+ telemetryCheckoutContext
7260
+ );
7261
+ const api = new import_js4.PaymentAPI(resolvedBillingUrl, { telemetry: false });
7262
+ try {
7263
+ const cached = readCachedInlineSession(cacheKey);
7264
+ reporter.log({
7265
+ name: cached ? "operation.cache.hit" : "operation.cache.miss",
7266
+ stage: "session_create",
7267
+ requestCategory: "session_create"
7268
+ });
7269
+ let sid = cached?.sid ?? null;
7270
+ let realResult = null;
7271
+ if (sid) {
7272
+ try {
7273
+ realResult = await api.getUnifiedCheckoutSession(sid, cached?.nonce);
7274
+ const status = realResult.data.session?.status;
7275
+ if (status === "complete") {
7276
+ if (wasSessionRecentlyCompleted(sid)) {
7277
+ return { sid, result: realResult };
7278
+ }
7279
+ clearCachedInlineSession(cacheKey);
7280
+ sid = null;
7281
+ realResult = null;
6619
7282
  }
7283
+ } catch {
7284
+ reporter.log({
7285
+ name: "operation.fallback",
7286
+ stage: "session_read",
7287
+ requestCategory: "session_read"
7288
+ });
6620
7289
  clearCachedInlineSession(cacheKey);
6621
7290
  sid = null;
6622
- realResult = null;
6623
7291
  }
6624
- } catch {
6625
- clearCachedInlineSession(cacheKey);
6626
- sid = null;
6627
7292
  }
6628
- }
6629
- if (!sid) {
6630
- const paramsWithAnalytics = {
6631
- ...params,
6632
- avsCheck: !!enableAVS,
6633
- avsConfig: typeof enableAVS === "object" ? enableAVS : void 0,
6634
- checkoutType: "embedded_checkout",
6635
- checkoutLayout: children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout"
6636
- };
6637
- realResult = await api.createAndFetchSession(paramsWithAnalytics);
6638
- sid = realResult.data.session?.id ?? "";
6639
- if (sid) {
6640
- persistCachedInlineSession(cacheKey, sid, realResult.data.session?.clientSecret);
7293
+ if (!sid) {
7294
+ const sessionCreateStartedAt = reporter.now();
7295
+ reporter.log({
7296
+ name: "session.create.started",
7297
+ stage: "session_create",
7298
+ requestCategory: "session_create"
7299
+ });
7300
+ const paramsWithAnalytics = {
7301
+ ...params,
7302
+ avsCheck: !!enableAVS,
7303
+ avsConfig: typeof enableAVS === "object" ? enableAVS : void 0,
7304
+ checkoutType: "embedded_checkout",
7305
+ checkoutLayout: children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout"
7306
+ };
7307
+ try {
7308
+ realResult = await api.createAndFetchSession(paramsWithAnalytics);
7309
+ reporter.log({
7310
+ name: "session.request.completed",
7311
+ stage: "session_complete",
7312
+ requestCategory: "session_create",
7313
+ statusClass: "2xx"
7314
+ });
7315
+ reporter.performance({
7316
+ stage: "session_create",
7317
+ durationMs: reporter.now() - sessionCreateStartedAt,
7318
+ durationMode: "machine",
7319
+ requestCategory: "session_create",
7320
+ statusClass: "2xx"
7321
+ });
7322
+ } catch (error) {
7323
+ reporter.performance({
7324
+ stage: "session_create",
7325
+ durationMs: reporter.now() - sessionCreateStartedAt,
7326
+ durationMode: "machine",
7327
+ requestCategory: "session_create",
7328
+ statusClass: "network_error"
7329
+ });
7330
+ if (error instanceof import_shared8.FloPayError && error.type === "validation_error") {
7331
+ reporter.terminal({
7332
+ outcome: "validation_rejected",
7333
+ stage: "session_create",
7334
+ paymentMethodCategory: "unknown"
7335
+ });
7336
+ } else {
7337
+ reporter.error({
7338
+ errorCode: "CHECKOUT_SESSION_CREATE_FAILED",
7339
+ stage: "session_create",
7340
+ paymentMethodCategory: "unknown",
7341
+ requestCategory: "session_create"
7342
+ });
7343
+ }
7344
+ throw error;
7345
+ }
7346
+ sid = realResult.data.session?.id ?? "";
7347
+ if (sid) {
7348
+ persistCachedInlineSession(cacheKey, sid, realResult.data.session?.clientSecret);
7349
+ }
6641
7350
  }
7351
+ return { sid: sid ?? "", result: realResult };
7352
+ } finally {
7353
+ finishStandaloneTelemetry(reporter);
6642
7354
  }
6643
- return { sid: sid ?? "", result: realResult };
6644
7355
  }
6645
7356
  const bootstrapInlineSession = (0, import_react10.useCallback)(
6646
7357
  async (patch) => {
@@ -6686,7 +7397,8 @@ function FloPayCheckout({
6686
7397
  publishableKey,
6687
7398
  paypalPublishableKey,
6688
7399
  billingApiUrl: resolvedBillingUrl,
6689
- locale
7400
+ locale,
7401
+ telemetry
6690
7402
  });
6691
7403
  flopayRef.current = instance;
6692
7404
  setFloPay(instance);
@@ -6701,7 +7413,7 @@ function FloPayCheckout({
6701
7413
  }
6702
7414
  return resolved;
6703
7415
  },
6704
- [locale, resolvedBillingUrl]
7416
+ [locale, resolvedBillingUrl, telemetry, telemetryCheckoutContext]
6705
7417
  );
6706
7418
  const handleInlineSessionPatch = (0, import_react10.useCallback)(async (patch) => {
6707
7419
  if (!hasInlineSessionPatchData(patch) || cardBootstrapPending) {
@@ -6780,7 +7492,9 @@ function FloPayCheckout({
6780
7492
  setModeOverlayStatus("success");
6781
7493
  await sleep(PROCESSING_OVERLAY_SUCCESS_DELAY_MS);
6782
7494
  if (!cancelled) {
6783
- onCompleteRef.current?.({ status: "succeeded" });
7495
+ invokeMerchantCallback(() => {
7496
+ onCompleteRef.current?.({ status: "succeeded" });
7497
+ });
6784
7498
  setModeOverlayStatus(null);
6785
7499
  setModeOverlayError(null);
6786
7500
  }
@@ -6800,8 +7514,9 @@ function FloPayCheckout({
6800
7514
  }
6801
7515
  setIsLoading(true);
6802
7516
  async function init() {
7517
+ let sessionReadCompleted = false;
6803
7518
  try {
6804
- const api = new import_js4.PaymentAPI(resolvedBillingUrl);
7519
+ const api = new import_js4.PaymentAPI(resolvedBillingUrl, { telemetry: false });
6805
7520
  const result = await api.getUnifiedCheckoutSession(activeSessionId, nonceProp);
6806
7521
  if (cancelled) return;
6807
7522
  setUnified(result);
@@ -6812,7 +7527,9 @@ function FloPayCheckout({
6812
7527
  }
6813
7528
  if (sess.status === "complete") {
6814
7529
  setIsLoading(false);
6815
- onSessionCompletedRef.current?.(sess.successUrl ?? "");
7530
+ invokeMerchantCallback(() => {
7531
+ onSessionCompletedRef.current?.(sess.successUrl ?? "");
7532
+ });
6816
7533
  return;
6817
7534
  }
6818
7535
  if (sess.status === "expired") {
@@ -6820,6 +7537,7 @@ function FloPayCheckout({
6820
7537
  code: "checkout_session_expired"
6821
7538
  });
6822
7539
  }
7540
+ sessionReadCompleted = true;
6823
7541
  const effectiveMode = checkoutModeProp ?? sess.checkoutMode ?? "full";
6824
7542
  setCurrentMode(effectiveMode);
6825
7543
  const hasPayPalRedirectParams = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("payment_intent");
@@ -6851,6 +7569,23 @@ function FloPayCheckout({
6851
7569
  if (!cancelled) setIsLoading(false);
6852
7570
  } catch (err) {
6853
7571
  if (cancelled) return;
7572
+ if (!(err instanceof import_shared8.FloPayError)) {
7573
+ reportStandaloneTelemetryError(
7574
+ resolvedBillingUrl,
7575
+ telemetry,
7576
+ telemetryCheckoutContext,
7577
+ "INTERNAL_SDK_ERROR",
7578
+ "checkout_mount"
7579
+ );
7580
+ } else if (!sessionReadCompleted && !isExpectedExistingSessionError(err)) {
7581
+ reportStandaloneTelemetryError(
7582
+ resolvedBillingUrl,
7583
+ telemetry,
7584
+ telemetryCheckoutContext,
7585
+ "NETWORK_REQUEST_FAILED",
7586
+ "session_read"
7587
+ );
7588
+ }
6854
7589
  const floPayErr = err instanceof import_shared8.FloPayError ? err : new import_shared8.FloPayError(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
6855
7590
  setLoadError(floPayErr);
6856
7591
  setIsLoading(false);
@@ -6872,7 +7607,8 @@ function FloPayCheckout({
6872
7607
  publishableKey,
6873
7608
  paypalPublishableKey,
6874
7609
  billingApiUrl: resolvedBillingUrl,
6875
- locale
7610
+ locale,
7611
+ telemetry
6876
7612
  });
6877
7613
  flopayRef.current = instance;
6878
7614
  setFloPay(instance);
@@ -6889,6 +7625,7 @@ function FloPayCheckout({
6889
7625
  createSessionHash,
6890
7626
  effectiveCreateSessionMode,
6891
7627
  initSessionDependency,
7628
+ invokeMerchantCallback,
6892
7629
  nonceProp,
6893
7630
  runSavedPaymentFlow
6894
7631
  ]);
@@ -7056,7 +7793,7 @@ function FloPayCheckout({
7056
7793
  }
7057
7794
  ),
7058
7795
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
7059
- DirectPayPalButton,
7796
+ InstrumentedDirectPayPalButton,
7060
7797
  {
7061
7798
  sessionId: activeSessionId,
7062
7799
  nonce: session.clientSecret || void 0,
@@ -7071,6 +7808,8 @@ function FloPayCheckout({
7071
7808
  onDecline,
7072
7809
  onButtonClick,
7073
7810
  session,
7811
+ telemetry,
7812
+ telemetryContext: telemetryCheckoutContext,
7074
7813
  debug
7075
7814
  }
7076
7815
  )
@@ -8138,7 +8877,7 @@ function FloPayAutomaticPaymentButton({
8138
8877
  };
8139
8878
  }, []);
8140
8879
  (0, import_react13.useEffect)(() => {
8141
- if (!fallbackSession || typeof window === "undefined") {
8880
+ if (typeof window === "undefined") {
8142
8881
  return;
8143
8882
  }
8144
8883
  const handleKeyDown = (event) => {
@@ -8150,7 +8889,7 @@ function FloPayAutomaticPaymentButton({
8150
8889
  return () => {
8151
8890
  window.removeEventListener("keydown", handleKeyDown);
8152
8891
  };
8153
- }, [fallbackSession]);
8892
+ }, []);
8154
8893
  const emitDecline = (0, import_react13.useCallback)((error, method = DEFAULT_SAVED_PAYMENT_DECLINE_METHOD) => {
8155
8894
  onDeclineRef.current?.(buildDeclineEvent(method, error, {
8156
8895
  code: error.code,