@flopay/react 1.4.0 → 1.4.2

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
@@ -31,17 +31,12 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AddressElement: () => AddressElement,
34
- CardCvcElement: () => CardCvcElement,
35
- CardElement: () => CardElement,
36
- CardExpiryElement: () => CardExpiryElement,
37
- CardNumberElement: () => CardNumberElement,
38
- CheckoutForm: () => CheckoutForm,
39
34
  DirectPayPalButton: () => DirectPayPalButton,
40
35
  FloPayAutomaticPaymentButton: () => FloPayAutomaticPaymentButton,
41
36
  FloPayCheckout: () => FloPayCheckout,
42
37
  FloPayProvider: () => FloPayProvider,
43
38
  PayPalButton: () => PayPalButton,
44
- PaymentElement: () => PaymentElement,
39
+ PaymentElement: () => PaymentElement2,
45
40
  SplitCardForm: () => SplitCardForm,
46
41
  VaultCardFields: () => VaultCardFields,
47
42
  useCheckout: () => useCheckout,
@@ -51,6 +46,10 @@ __export(index_exports, {
51
46
  });
52
47
  module.exports = __toCommonJS(index_exports);
53
48
 
49
+ // ../../node_modules/.pnpm/tsup@8.5.1_jiti@2.7.0_postcss@8.5.18_typescript@6.0.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
50
+ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
51
+ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
52
+
54
53
  // src/provider.tsx
55
54
  var import_react2 = require("react");
56
55
  var import_shared = require("@flopay/shared");
@@ -317,77 +316,14 @@ function TitleContentSlot({
317
316
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: content === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(DefaultTitleContent, {}) : content });
318
317
  }
319
318
 
320
- // src/elements.tsx
321
- var import_react4 = require("react");
322
- var import_jsx_runtime3 = require("react/jsx-runtime");
323
- function createElementComponent(elementType, displayName) {
324
- function ElementComponent({
325
- className,
326
- id,
327
- style,
328
- options,
329
- onChange,
330
- onReady,
331
- onFocus,
332
- onBlur,
333
- onEscape
334
- }) {
335
- const containerRef = (0, import_react4.useRef)(null);
336
- const elementRef = (0, import_react4.useRef)(null);
337
- const { elements, reportInteractive } = (0, import_react4.useContext)(FloPayContext);
338
- (0, import_react4.useEffect)(() => {
339
- if (!elements || !containerRef.current) return;
340
- let mounted = true;
341
- (async () => {
342
- let element = elements.getElement(elementType);
343
- if (!element) {
344
- element = await elements.create(elementType, options);
345
- }
346
- if (!mounted || !containerRef.current) {
347
- return;
348
- }
349
- element.mount(containerRef.current);
350
- elementRef.current = element;
351
- if (onChange) element.on("change", onChange);
352
- element.on("ready", () => {
353
- reportInteractive?.();
354
- onReady?.();
355
- });
356
- if (onFocus) element.on("focus", onFocus);
357
- if (onBlur) element.on("blur", onBlur);
358
- if (onEscape) element.on("escape", onEscape);
359
- })();
360
- return () => {
361
- mounted = false;
362
- if (elementRef.current) {
363
- try {
364
- elementRef.current.unmount();
365
- } catch {
366
- }
367
- elementRef.current = null;
368
- }
369
- };
370
- }, [elements, reportInteractive]);
371
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: containerRef, className, id, style });
372
- }
373
- ElementComponent.displayName = displayName;
374
- return ElementComponent;
375
- }
376
- var PaymentElement = createElementComponent("payment", "PaymentElement");
377
- var CardElement = createElementComponent("card", "CardElement");
378
- var CardNumberElement = createElementComponent("cardNumber", "CardNumberElement");
379
- var CardExpiryElement = createElementComponent("cardExpiry", "CardExpiryElement");
380
- var CardCvcElement = createElementComponent("cardCvc", "CardCvcElement");
381
- var AddressElement = createElementComponent("address", "AddressElement");
382
-
383
319
  // src/split-card-form.tsx
384
320
  var import_react_stripe_js = require("@stripe/react-stripe-js");
385
321
  var import_shared5 = require("@flopay/shared");
386
322
  var import_js2 = require("@flopay/js");
387
323
 
388
324
  // src/vault-card-fields.tsx
389
- var import_react5 = require("react");
390
- var import_jsx_runtime4 = require("react/jsx-runtime");
325
+ var import_react4 = require("react");
326
+ var import_jsx_runtime3 = require("react/jsx-runtime");
391
327
  function VaultCardFields({
392
328
  capture,
393
329
  html,
@@ -399,16 +335,16 @@ function VaultCardFields({
399
335
  onError,
400
336
  onValidation
401
337
  }) {
402
- const containerRef = (0, import_react5.useRef)(null);
403
- const onReadyRef = (0, import_react5.useRef)(onReady);
404
- const onErrorRef = (0, import_react5.useRef)(onError);
405
- const onValidationRef = (0, import_react5.useRef)(onValidation);
338
+ const containerRef = (0, import_react4.useRef)(null);
339
+ const onReadyRef = (0, import_react4.useRef)(onReady);
340
+ const onErrorRef = (0, import_react4.useRef)(onError);
341
+ const onValidationRef = (0, import_react4.useRef)(onValidation);
406
342
  onReadyRef.current = onReady;
407
343
  onErrorRef.current = onError;
408
344
  onValidationRef.current = onValidation;
409
- const themeRef = (0, import_react5.useRef)(theme);
345
+ const themeRef = (0, import_react4.useRef)(theme);
410
346
  themeRef.current = theme;
411
- (0, import_react5.useEffect)(() => {
347
+ (0, import_react4.useEffect)(() => {
412
348
  const el = containerRef.current;
413
349
  if (!el) return;
414
350
  let active = true;
@@ -450,12 +386,69 @@ function VaultCardFields({
450
386
  capture.unmount();
451
387
  };
452
388
  }, [capture, html, messageToken, expectedOrigin]);
453
- (0, import_react5.useEffect)(() => {
389
+ (0, import_react4.useEffect)(() => {
454
390
  if (theme) capture.applyTheme?.(theme);
455
391
  }, [capture, theme]);
456
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { ref: containerRef, "data-testid": "flopay-vault-card-fields", style: containerStyle });
392
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: containerRef, "data-testid": "flopay-vault-card-fields", style: containerStyle });
457
393
  }
458
394
 
395
+ // src/error-banner.tsx
396
+ var import_react5 = require("react");
397
+ var import_jsx_runtime4 = require("react/jsx-runtime");
398
+ function ErrorBanner({
399
+ children,
400
+ margin,
401
+ icon = true,
402
+ styleOverride
403
+ }) {
404
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
405
+ "div",
406
+ {
407
+ role: "alert",
408
+ "data-testid": "flopay-error",
409
+ style: {
410
+ margin,
411
+ padding: "0.625rem 0.875rem",
412
+ background: "#FEF2F2",
413
+ border: "1px solid #FECACA",
414
+ borderRadius: "8px",
415
+ color: "#991B1B",
416
+ fontSize: "0.85rem",
417
+ fontWeight: 600,
418
+ ...icon ? { display: "flex", alignItems: "center", gap: "0.5rem" } : {},
419
+ ...styleOverride ?? {}
420
+ },
421
+ children: [
422
+ icon && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
423
+ "path",
424
+ {
425
+ d: "M12 9v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
426
+ stroke: "#DC2626",
427
+ strokeWidth: "2",
428
+ strokeLinecap: "round",
429
+ strokeLinejoin: "round"
430
+ }
431
+ ) }),
432
+ children
433
+ ]
434
+ }
435
+ );
436
+ }
437
+
438
+ // src/payment-logos.generated.ts
439
+ var PAYMENT_METHOD_LOGO_URLS = {
440
+ "alipay": new URL("./payment-logos/alipay.svg", importMetaUrl).href,
441
+ "bancontact": new URL("./payment-logos/bancontact.svg", importMetaUrl).href,
442
+ "blik": new URL("./payment-logos/blik.svg", importMetaUrl).href,
443
+ "eps": new URL("./payment-logos/eps.svg", importMetaUrl).href,
444
+ "giropay": new URL("./payment-logos/giropay.svg", importMetaUrl).href,
445
+ "ideal": new URL("./payment-logos/ideal.svg", importMetaUrl).href,
446
+ "klarna": new URL("./payment-logos/klarna.svg", importMetaUrl).href,
447
+ "p24": new URL("./payment-logos/p24.svg", importMetaUrl).href,
448
+ "sepa_debit": new URL("./payment-logos/sepa_debit.svg", importMetaUrl).href,
449
+ "wechat_pay": new URL("./payment-logos/wechat_pay.svg", importMetaUrl).href
450
+ };
451
+
459
452
  // src/split-card-form.tsx
460
453
  var import_react9 = __toESM(require("react"), 1);
461
454
 
@@ -745,13 +738,6 @@ function buildDeclineEvent(method, input, overrides) {
745
738
  ...declineCode ? { declineCode } : {}
746
739
  };
747
740
  }
748
- function isRecord(value) {
749
- return typeof value === "object" && value !== null;
750
- }
751
- function readString(payload, key) {
752
- const value = payload?.[key];
753
- return typeof value === "string" && value.trim() ? value : void 0;
754
- }
755
741
  var FRIENDLY_MESSAGE_OVERRIDES = [
756
742
  {
757
743
  match: /^paypal authorization required\.?$/i,
@@ -767,20 +753,8 @@ function applyFriendlyMessageOverride(message) {
767
753
  }
768
754
  return message;
769
755
  }
770
- function buildFloPayApiError(payload, fallbackMessage) {
771
- const nestedError = isRecord(payload?.error) ? payload.error : null;
772
- const rawMessage = readString(payload, "message") ?? readString(nestedError, "message") ?? fallbackMessage;
773
- const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
774
- const code = readString(payload, "code") ?? readString(payload, "gatewayErrorCode") ?? readString(nestedError, "code");
775
- const declineCode = readString(payload, "declineCode") ?? readString(payload, "gatewayDeclineReason") ?? readString(payload, "decline_code") ?? readString(nestedError, "decline_code");
776
- return new import_shared3.FloPayError(message, "api_error", {
777
- ...code ? { code } : {},
778
- ...declineCode ? { declineCode } : {}
779
- });
780
- }
781
- async function buildFloPayApiErrorFromResponse(response, fallbackMessage) {
782
- const payload = await response.json().catch(() => null);
783
- return buildFloPayApiError(payload, fallbackMessage);
756
+ function isSuccessfulPaymentIntentStatus(status) {
757
+ return status === "succeeded" || status === "requires_capture" || status === "processing";
784
758
  }
785
759
  function mapPayPalIntentStatusToPaymentResult(status) {
786
760
  if (status === "succeeded") {
@@ -791,41 +765,6 @@ function mapPayPalIntentStatusToPaymentResult(status) {
791
765
  }
792
766
  return "failed";
793
767
  }
794
- function resolvePaymentIntentPaymentMethodId(paymentIntent) {
795
- const paymentMethod = paymentIntent?.payment_method;
796
- if (typeof paymentMethod === "string" && paymentMethod.startsWith("pm_")) {
797
- return paymentMethod;
798
- }
799
- if (paymentMethod && typeof paymentMethod === "object" && typeof paymentMethod.id === "string") {
800
- return paymentMethod.id;
801
- }
802
- return void 0;
803
- }
804
- async function retrievePaymentIntentFromProvider(provider, clientSecret) {
805
- const retriever = provider;
806
- if ((0, import_shared3.isSetupIntentClientSecret)(clientSecret)) {
807
- if (!retriever?.retrieveSetupIntent) {
808
- return null;
809
- }
810
- const { setupIntent, error: error2 } = await retriever.retrieveSetupIntent(clientSecret);
811
- if (error2) {
812
- throw new import_shared3.FloPayError(error2.message ?? "Failed to retrieve setup intent.", "api_error", {
813
- ...error2.code ? { code: error2.code } : {}
814
- });
815
- }
816
- return setupIntent ?? null;
817
- }
818
- if (!retriever?.retrievePaymentIntent) {
819
- return null;
820
- }
821
- const { paymentIntent, error } = await retriever.retrievePaymentIntent(clientSecret);
822
- if (error) {
823
- throw new import_shared3.FloPayError(error.message ?? "Failed to retrieve payment intent.", "api_error", {
824
- ...error.code ? { code: error.code } : {}
825
- });
826
- }
827
- return paymentIntent ?? null;
828
- }
829
768
  function resolveWalletElementsMode(amountInMinorUnits) {
830
769
  if (typeof amountInMinorUnits !== "number" || !Number.isFinite(amountInMinorUnits) || amountInMinorUnits <= 0) {
831
770
  return { mode: "setup" };
@@ -951,6 +890,12 @@ function isPopupBlockedError(err) {
951
890
  // src/direct-paypal-button.tsx
952
891
  var import_jsx_runtime6 = require("react/jsx-runtime");
953
892
  var DEFAULT_BUTTON_HEIGHT = 45;
893
+ var PAYPAL_INIT_RETRY_DELAY_MS = 1e3;
894
+ var PAYPAL_RENDER_WATCHDOG_MS = 11e3;
895
+ var PAYPAL_INIT_ACKNOWLEDGEMENT_HOSTS = [
896
+ "https://www.paypal.com",
897
+ "https://www.sandbox.paypal.com"
898
+ ];
954
899
  var DIRECT_PAYPAL_RECOVERY_MESSAGE = "We couldn't open PayPal. Try again or choose another payment method.";
955
900
  var DIRECT_PAYPAL_RECOVERY_ACTION_STYLE = {
956
901
  width: "100%",
@@ -965,6 +910,9 @@ var DIRECT_PAYPAL_RECOVERY_ACTION_STYLE = {
965
910
  fontWeight: 700,
966
911
  cursor: "pointer"
967
912
  };
913
+ function isPayPalInitAcknowledgementTimeout(message) {
914
+ return message.includes("No ack for postMessage init()") && PAYPAL_INIT_ACKNOWLEDGEMENT_HOSTS.some((host) => message.includes(host)) && message.includes("10000ms");
915
+ }
968
916
  function buildDirectPayPalRecoveryMessage(popupBlocked) {
969
917
  return popupBlocked ? `${DIRECT_PAYPAL_RECOVERY_MESSAGE} Allow pop-ups for this site, then try again.` : DIRECT_PAYPAL_RECOVERY_MESSAGE;
970
918
  }
@@ -984,6 +932,8 @@ function DirectPayPalButtonImplementation({
984
932
  onTechnicalFailure,
985
933
  isProcessing = false,
986
934
  onLoadStateChange,
935
+ onInitializationStateChange,
936
+ allowAutomaticRetry = true,
987
937
  onButtonClick,
988
938
  runBeforeButtonClick,
989
939
  session,
@@ -1042,12 +992,29 @@ function DirectPayPalButtonImplementation({
1042
992
  const focusTargetRef = (0, import_react8.useRef)(null);
1043
993
  const pendingProviderFocusRef = (0, import_react8.useRef)(false);
1044
994
  const [ready, setReady] = (0, import_react8.useState)(false);
995
+ const [recovering, setRecovering] = (0, import_react8.useState)(false);
1045
996
  const [renderGeneration, setRenderGeneration] = (0, import_react8.useState)(0);
1046
997
  const activeRenderGenerationRef = (0, import_react8.useRef)(0);
1047
998
  const [showRetryFocusTarget, setShowRetryFocusTarget] = (0, import_react8.useState)(false);
999
+ const automaticRetryUsedRef = (0, import_react8.useRef)(false);
1000
+ const initializationFailureLoggedRef = (0, import_react8.useRef)(false);
1048
1001
  const [failed, setFailed] = (0, import_react8.useState)(false);
1049
1002
  const [submitting, setSubmitting] = (0, import_react8.useState)(false);
1050
1003
  const baseUrl = (0, import_react8.useMemo)(() => billingApiUrl.replace(/\/+$/, ""), [billingApiUrl]);
1004
+ const initializationConfigKey = (0, import_react8.useMemo)(
1005
+ () => JSON.stringify([
1006
+ baseUrl,
1007
+ clientId,
1008
+ currency,
1009
+ environment ?? "",
1010
+ isSubscription,
1011
+ sessionId,
1012
+ existingOrderId ?? ""
1013
+ ]),
1014
+ [baseUrl, clientId, currency, environment, existingOrderId, isSubscription, sessionId]
1015
+ );
1016
+ const initializationConfigKeyRef = (0, import_react8.useRef)(initializationConfigKey);
1017
+ const initializationConfigChanged = initializationConfigKeyRef.current !== initializationConfigKey;
1051
1018
  const [debugLines, setDebugLines] = (0, import_react8.useState)([]);
1052
1019
  const appendDebug = (line) => {
1053
1020
  if (!debug) return;
@@ -1060,6 +1027,7 @@ function DirectPayPalButtonImplementation({
1060
1027
  const onTechnicalFailureRef = (0, import_react8.useRef)(onTechnicalFailure);
1061
1028
  const onButtonClickRef = (0, import_react8.useRef)(onButtonClick);
1062
1029
  const onLoadStateChangeRef = (0, import_react8.useRef)(onLoadStateChange);
1030
+ const onInitializationStateChangeRef = (0, import_react8.useRef)(onInitializationStateChange);
1063
1031
  const runBeforeButtonClickRef = (0, import_react8.useRef)(runBeforeButtonClick);
1064
1032
  const sessionRef = (0, import_react8.useRef)(session);
1065
1033
  const emailRef = (0, import_react8.useRef)(email);
@@ -1070,6 +1038,13 @@ function DirectPayPalButtonImplementation({
1070
1038
  const invalidatedAttemptGenerationRef = (0, import_react8.useRef)(null);
1071
1039
  const attemptContextBySurfaceRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
1072
1040
  const approvalContextByTokenRef = (0, import_react8.useRef)(/* @__PURE__ */ new Map());
1041
+ const componentMountedRef = (0, import_react8.useRef)(false);
1042
+ (0, import_react8.useEffect)(() => {
1043
+ componentMountedRef.current = true;
1044
+ return () => {
1045
+ componentMountedRef.current = false;
1046
+ };
1047
+ }, []);
1073
1048
  (0, import_react8.useEffect)(() => {
1074
1049
  onTokenizedBodyRef.current = onTokenizedBody;
1075
1050
  }, [onTokenizedBody]);
@@ -1091,6 +1066,9 @@ function DirectPayPalButtonImplementation({
1091
1066
  (0, import_react8.useEffect)(() => {
1092
1067
  onLoadStateChangeRef.current = onLoadStateChange;
1093
1068
  }, [onLoadStateChange]);
1069
+ (0, import_react8.useEffect)(() => {
1070
+ onInitializationStateChangeRef.current = onInitializationStateChange;
1071
+ }, [onInitializationStateChange]);
1094
1072
  (0, import_react8.useEffect)(() => {
1095
1073
  runBeforeButtonClickRef.current = runBeforeButtonClick;
1096
1074
  }, [runBeforeButtonClick]);
@@ -1226,6 +1204,16 @@ function DirectPayPalButtonImplementation({
1226
1204
  clearAttemptTimer();
1227
1205
  };
1228
1206
  }, []);
1207
+ (0, import_react8.useEffect)(() => {
1208
+ if (initializationConfigKeyRef.current === initializationConfigKey) return;
1209
+ initializationConfigKeyRef.current = initializationConfigKey;
1210
+ automaticRetryUsedRef.current = false;
1211
+ initializationFailureLoggedRef.current = false;
1212
+ setReady(false);
1213
+ setRecovering(false);
1214
+ setFailed(false);
1215
+ onInitializationStateChangeRef.current?.("loading");
1216
+ }, [initializationConfigKey]);
1229
1217
  (0, import_react8.useEffect)(() => {
1230
1218
  invokeMerchantCallback(() => onLoadStateChangeRef.current?.(ready && !failed));
1231
1219
  }, [ready, failed]);
@@ -1260,7 +1248,29 @@ function DirectPayPalButtonImplementation({
1260
1248
  paymentMethodCategory: "paypal"
1261
1249
  });
1262
1250
  let cancelled = false;
1263
- let activeButtons = null;
1251
+ let superseded = false;
1252
+ const isInactive = () => cancelled || superseded;
1253
+ let paypalIntentIdempotencyKey;
1254
+ let paypalIntentAttemptActive = false;
1255
+ const beginPaypalIntentAttempt = () => {
1256
+ paypalIntentIdempotencyKey = (0, import_shared4.generateIdempotencyKey)();
1257
+ if (!paypalIntentIdempotencyKey) {
1258
+ console.warn(
1259
+ "[FloPay] DirectPayPal: secure idempotency key unavailable; retries may not be deduplicated."
1260
+ );
1261
+ }
1262
+ paypalIntentAttemptActive = true;
1263
+ };
1264
+ let retryTimer = null;
1265
+ let preRenderFailureHandled = false;
1266
+ let ownedButtons = null;
1267
+ let ownedButtonsClosed = false;
1268
+ const closeOwnedButtons = () => {
1269
+ if (!ownedButtons || ownedButtonsClosed) return;
1270
+ ownedButtonsClosed = true;
1271
+ ownedButtons.close().catch(() => {
1272
+ });
1273
+ };
1264
1274
  let overlayStartedAt = null;
1265
1275
  const finishOverlay = () => {
1266
1276
  if (overlayStartedAt === null) return;
@@ -1330,6 +1340,7 @@ function DirectPayPalButtonImplementation({
1330
1340
  };
1331
1341
  const errorMessages = [];
1332
1342
  const onWindowError = (ev) => {
1343
+ if (isInactive()) return;
1333
1344
  const msg = ev.message ?? String(ev.error ?? "(no message)");
1334
1345
  if (msg && (msg.toLowerCase().includes("paypal") || msg.toLowerCase().includes("zoid") || msg.toLowerCase().includes("postrobot") || msg.toLowerCase().includes("storage"))) {
1335
1346
  appendDebug(`window:error ${msg.slice(0, 140)}`);
@@ -1337,6 +1348,7 @@ function DirectPayPalButtonImplementation({
1337
1348
  }
1338
1349
  };
1339
1350
  const onUnhandledRejection = (ev) => {
1351
+ if (isInactive()) return;
1340
1352
  const reason = ev.reason instanceof Error ? ev.reason.message : String(ev.reason ?? "(no reason)");
1341
1353
  if (reason && (reason.toLowerCase().includes("paypal") || reason.toLowerCase().includes("zoid") || reason.toLowerCase().includes("postrobot") || reason.toLowerCase().includes("storage"))) {
1342
1354
  appendDebug(`window:rejection ${reason.slice(0, 140)}`);
@@ -1351,6 +1363,7 @@ function DirectPayPalButtonImplementation({
1351
1363
  if (debug && typeof PerformanceObserver !== "undefined") {
1352
1364
  try {
1353
1365
  perfObserver = new PerformanceObserver((list) => {
1366
+ if (isInactive()) return;
1354
1367
  for (const entry of list.getEntries()) {
1355
1368
  if (!entry.name.toLowerCase().includes("paypal")) continue;
1356
1369
  paypalRequestCount.value += 1;
@@ -1379,17 +1392,13 @@ function DirectPayPalButtonImplementation({
1379
1392
  return lower.includes("zoid destroyed") || lower.includes("destroyed all components") || lower.includes("window closed") || lower.includes("detected container element removed");
1380
1393
  };
1381
1394
  const forwardError = (message) => {
1382
- if (cancelled) return;
1395
+ if (isInactive()) return;
1383
1396
  if (isZoidLifecycleMessage(message)) return;
1384
1397
  const friendly = applyFriendlyMessageOverride(message) ?? message;
1385
1398
  invokeMerchantCallback(() => onErrorChangeRef.current?.(friendly));
1386
1399
  };
1387
1400
  const markRenderFailed = (message) => {
1388
1401
  if (cancelled) return;
1389
- if (isZoidLifecycleMessage(message)) {
1390
- appendDebug(`markRenderFailed:skip-zoid msg=${message.slice(0, 80)}`);
1391
- return;
1392
- }
1393
1402
  setFailed(true);
1394
1403
  if (!message.includes("paypal_ineligible")) {
1395
1404
  telemetrySource.error({
@@ -1402,8 +1411,48 @@ function DirectPayPalButtonImplementation({
1402
1411
  }
1403
1412
  console.error("[FloPay] DirectPayPal load/render failure:", message);
1404
1413
  };
1414
+ const handlePreRenderFailure = (message, retryable = false) => {
1415
+ if (cancelled) return;
1416
+ const isRecoveryAttempt = automaticRetryUsedRef.current || !allowAutomaticRetry;
1417
+ if (preRenderFailureHandled) return;
1418
+ preRenderFailureHandled = true;
1419
+ superseded = true;
1420
+ stopDiagnostics();
1421
+ closeOwnedButtons();
1422
+ const isRetryableInitializationFailure = retryable || isPayPalInitAcknowledgementTimeout(message);
1423
+ if (isRetryableInitializationFailure && allowAutomaticRetry && !automaticRetryUsedRef.current) {
1424
+ automaticRetryUsedRef.current = true;
1425
+ setReady(false);
1426
+ setSubmitting(false);
1427
+ setRecovering(true);
1428
+ onInitializationStateChangeRef.current?.("retrying");
1429
+ retryTimer = setTimeout(() => {
1430
+ if (cancelled) return;
1431
+ setRenderGeneration((generation) => generation + 1);
1432
+ }, PAYPAL_INIT_RETRY_DELAY_MS);
1433
+ return;
1434
+ }
1435
+ if (isRetryableInitializationFailure || isRecoveryAttempt) {
1436
+ setReady(false);
1437
+ setSubmitting(false);
1438
+ setRecovering(false);
1439
+ setFailed(true);
1440
+ onInitializationStateChangeRef.current?.("exhausted");
1441
+ if (!initializationFailureLoggedRef.current) {
1442
+ initializationFailureLoggedRef.current = true;
1443
+ const failureCode = isPayPalInitAcknowledgementTimeout(message) ? "paypal_init_timeout" : retryable ? "paypal_render_timeout" : "paypal_init_failure";
1444
+ console.error(
1445
+ "[FloPay] Direct PayPal initialization failed:",
1446
+ failureCode
1447
+ );
1448
+ }
1449
+ return;
1450
+ }
1451
+ markRenderFailed(message);
1452
+ };
1405
1453
  setFailed(false);
1406
1454
  const dispatchTokenizedBody = async (body, prepared = beforeClickRef.current) => {
1455
+ if (isInactive()) return;
1407
1456
  const effectiveSessionId = prepared?.sessionId ?? sessionId;
1408
1457
  if (onTokenizedBodyRef.current) {
1409
1458
  await onTokenizedBodyRef.current(body, {
@@ -1457,41 +1506,45 @@ function DirectPayPalButtonImplementation({
1457
1506
  }
1458
1507
  );
1459
1508
  if (response.ok) {
1509
+ if (superseded || cancelled && componentMountedRef.current) return;
1460
1510
  finishProcessing();
1461
1511
  telemetrySource.terminal({
1462
1512
  outcome: "payment_succeeded",
1463
1513
  provider: "paypal",
1464
1514
  paymentMethodCategory: "paypal"
1465
1515
  });
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
- })));
1516
+ invokeMerchantCallback(() => onCompleteRef.current?.({ status: "succeeded", checkoutMethod: "paypal" }));
1492
1517
  return;
1493
1518
  }
1519
+ if (isInactive()) return;
1520
+ const json = await response.json().catch(() => null);
1521
+ const rawMessage = json?.["message"] ?? "PayPal payment failed.";
1522
+ const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
1523
+ finishProcessing();
1524
+ if (typeof json?.["declineCode"] === "string") {
1525
+ telemetrySource.terminal({
1526
+ outcome: "payment_declined",
1527
+ provider: "paypal",
1528
+ paymentMethodCategory: "paypal"
1529
+ });
1530
+ } else {
1531
+ telemetrySource.error({
1532
+ errorCode: "PAYMENT_PROCESSING_FAILED",
1533
+ stage: "processing",
1534
+ provider: "paypal",
1535
+ paymentMethodCategory: "paypal",
1536
+ requestCategory: "process_payment",
1537
+ statusClass: response.status >= 500 ? "5xx" : response.status >= 400 ? "4xx" : response.status >= 300 ? "3xx" : "unknown"
1538
+ });
1539
+ }
1540
+ forwardError(message);
1541
+ invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message, {
1542
+ code: json?.["code"],
1543
+ declineCode: json?.["declineCode"]
1544
+ })));
1545
+ return;
1494
1546
  } catch (err) {
1547
+ if (isInactive()) return;
1495
1548
  finishProcessing();
1496
1549
  telemetrySource.error({
1497
1550
  errorCode: "NETWORK_REQUEST_FAILED",
@@ -1507,15 +1560,21 @@ function DirectPayPalButtonImplementation({
1507
1560
  invokeMerchantCallback(() => onDeclineRef.current?.(buildDeclineEvent("paypal", message)));
1508
1561
  return;
1509
1562
  }
1510
- invokeMerchantCallback(() => onCompleteRef.current?.({ status: "succeeded", checkoutMethod: "paypal" }));
1511
1563
  };
1564
+ const supersededRenderError = () => new import_shared4.FloPayError(
1565
+ "PayPal render was superseded.",
1566
+ "api_error",
1567
+ { code: "paypal_render_superseded" }
1568
+ );
1569
+ const resolveExistingOrder = (orderId) => isInactive() ? Promise.reject(supersededRenderError()) : Promise.resolve(orderId);
1512
1570
  const createPaypalIntent = async (fallbackMessage) => {
1571
+ if (isInactive()) {
1572
+ throw supersededRenderError();
1573
+ }
1574
+ if (!paypalIntentAttemptActive) beginPaypalIntentAttempt();
1513
1575
  const prepared = beforeClickRef.current;
1514
1576
  const effectiveSessionId = prepared?.sessionId ?? sessionId;
1515
- const effectiveEmail = prepared?.accountPatch?.email ?? emailRef.current;
1516
- const intentHeaders = { "Content-Type": "application/json" };
1517
1577
  const currentNonce = prepared?.nonce ?? nonceRef.current;
1518
- if (currentNonce) intentHeaders["x-checkout-session-token"] = currentNonce;
1519
1578
  telemetrySource.log({
1520
1579
  name: "payment.intent.started",
1521
1580
  stage: "processing",
@@ -1523,22 +1582,22 @@ function DirectPayPalButtonImplementation({
1523
1582
  paymentMethodCategory: "paypal",
1524
1583
  requestCategory: "intent_create"
1525
1584
  });
1526
- const response = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
1527
- method: "POST",
1528
- headers: intentHeaders,
1529
- body: JSON.stringify({
1530
- sessionId: effectiveSessionId,
1531
- email: effectiveEmail,
1585
+ const intent = await new import_js.PaymentAPI(baseUrl, { telemetry: false }).createSessionIntent(
1586
+ effectiveSessionId,
1587
+ currentNonce ?? "",
1588
+ {
1589
+ provider: "paypal",
1590
+ paymentMethodCategory: "wallet",
1532
1591
  paymentMethodType: "paypal",
1533
- isPaypal: "true"
1534
- })
1535
- });
1536
- const json = await response.json().catch(() => null);
1537
- if (!response.ok) {
1538
- throw new Error(json?.message ?? fallbackMessage);
1592
+ paymentMethodId: null,
1593
+ intentKind: isSubscription ? "subscription" : "order"
1594
+ },
1595
+ { idempotencyKey: paypalIntentIdempotencyKey }
1596
+ );
1597
+ if (isInactive()) {
1598
+ throw supersededRenderError();
1539
1599
  }
1540
- const id = json?.data?.id;
1541
- if (!id) {
1600
+ if (intent.provider !== "paypal") {
1542
1601
  throw new Error(fallbackMessage);
1543
1602
  }
1544
1603
  telemetrySource.log({
@@ -1549,7 +1608,24 @@ function DirectPayPalButtonImplementation({
1549
1608
  requestCategory: "intent_create",
1550
1609
  statusClass: "2xx"
1551
1610
  });
1552
- return id;
1611
+ return intent.providerObjectId;
1612
+ };
1613
+ const reportPayPalDecline = (error, context) => {
1614
+ const currentNonce = context?.nonce ?? nonceRef.current;
1615
+ if (!currentNonce) return;
1616
+ const effectiveSessionId = context?.sessionId ?? sessionId;
1617
+ const providerDeclineReason = getProviderDeclineCode(error) ?? getProviderErrorCode(error) ?? "provider_declined";
1618
+ void new import_js.PaymentAPI(baseUrl, { telemetry: false }).reportSessionIntentDecline(
1619
+ effectiveSessionId,
1620
+ currentNonce,
1621
+ {
1622
+ provider: "paypal",
1623
+ paymentMethodCategory: "wallet",
1624
+ paymentMethodType: "paypal",
1625
+ providerDeclineReason
1626
+ }
1627
+ ).catch(() => {
1628
+ });
1553
1629
  };
1554
1630
  const effectRenderGeneration = renderGeneration;
1555
1631
  appendDebug("loadScript:scheduled (deferred 1 tick)");
@@ -1578,9 +1654,11 @@ function DirectPayPalButtonImplementation({
1578
1654
  ...paypalSdkEnv === "sandbox" ? { dataNamespace: "paypal_sandbox" } : {}
1579
1655
  });
1580
1656
  loadPromise.then((paypal) => {
1581
- appendDebug(`loadScript:resolved cancelled=${cancelled} ns=${!!paypal} buttons=${!!paypal?.Buttons}`);
1582
- if (cancelled || !paypal?.Buttons) {
1583
- if (!cancelled && !paypal?.Buttons) appendDebug("FAIL: namespace missing Buttons factory");
1657
+ appendDebug(`loadScript:resolved inactive=${isInactive()} ns=${!!paypal} buttons=${!!paypal?.Buttons}`);
1658
+ if (isInactive()) return;
1659
+ if (!paypal?.Buttons) {
1660
+ appendDebug("FAIL: namespace missing Buttons factory");
1661
+ handlePreRenderFailure("PayPal SDK did not expose the Buttons factory.");
1584
1662
  return;
1585
1663
  }
1586
1664
  telemetrySource.log({
@@ -1590,7 +1668,7 @@ function DirectPayPalButtonImplementation({
1590
1668
  paymentMethodCategory: "paypal"
1591
1669
  });
1592
1670
  const handleApprove = async (data) => {
1593
- if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
1671
+ if (isInactive() || activeRenderGenerationRef.current !== effectRenderGeneration) return;
1594
1672
  const token = data.subscriptionID ?? data.orderID ?? "";
1595
1673
  const context = token ? approvalContextByTokenRef.current.get(token) : attemptContextBySurfaceRef.current.get(effectRenderGeneration);
1596
1674
  if (!context || !isAttemptActive(context.generation)) return;
@@ -1615,7 +1693,7 @@ function DirectPayPalButtonImplementation({
1615
1693
  } catch (err) {
1616
1694
  forwardError(err instanceof Error ? err.message : "PayPal capture failed.");
1617
1695
  } finally {
1618
- setSubmitting(false);
1696
+ if (!isInactive()) setSubmitting(false);
1619
1697
  }
1620
1698
  };
1621
1699
  const buttons = paypal.Buttons({
@@ -1626,11 +1704,20 @@ function DirectPayPalButtonImplementation({
1626
1704
  // inline-session/account patches land before the order is created,
1627
1705
  // matching the Stripe-rendered PayPal flow.
1628
1706
  onClick: async (_data, actions) => {
1707
+ if (isInactive()) {
1708
+ await actions.reject();
1709
+ return;
1710
+ }
1711
+ beginPaypalIntentAttempt();
1629
1712
  const runner = runBeforeButtonClickRef.current;
1630
1713
  let prepared = {};
1631
1714
  if (runner) {
1632
1715
  try {
1633
1716
  const beforeClick = await runner("paypal");
1717
+ if (isInactive()) {
1718
+ await actions.reject();
1719
+ return;
1720
+ }
1634
1721
  if (!beforeClick.proceed) {
1635
1722
  beforeClickRef.current = null;
1636
1723
  attemptContextBySurfaceRef.current.delete(effectRenderGeneration);
@@ -1684,10 +1771,11 @@ function DirectPayPalButtonImplementation({
1684
1771
  // prior backend round-trip (the `paypal_direct_required` retry
1685
1772
  // path), feed it straight to PayPal instead of creating a new one.
1686
1773
  // Otherwise fall back to the normal create-intent call.
1687
- createOrder: isSubscription ? void 0 : existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal order.").then(registerApprovalContext),
1688
- createSubscription: isSubscription ? existingOrderId ? () => Promise.resolve(registerApprovalContext(existingOrderId)) : () => createPaypalIntent("Failed to create PayPal subscription.").then(registerApprovalContext) : void 0,
1774
+ createOrder: isSubscription ? void 0 : existingOrderId ? () => resolveExistingOrder(existingOrderId).then(registerApprovalContext) : () => createPaypalIntent("Failed to create PayPal order.").then(registerApprovalContext),
1775
+ createSubscription: isSubscription ? existingOrderId ? () => resolveExistingOrder(existingOrderId).then(registerApprovalContext) : () => createPaypalIntent("Failed to create PayPal subscription.").then(registerApprovalContext) : void 0,
1689
1776
  onApprove: handleApprove,
1690
1777
  onCancel: () => {
1778
+ if (isInactive()) return;
1691
1779
  const context = attemptContextBySurfaceRef.current.get(effectRenderGeneration) ?? beforeClickRef.current;
1692
1780
  if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1693
1781
  beforeClickRef.current = null;
@@ -1701,7 +1789,7 @@ function DirectPayPalButtonImplementation({
1701
1789
  invalidateAttempt({ generation: context?.generation, remount: true, focus: false });
1702
1790
  },
1703
1791
  onError: (err) => {
1704
- if (cancelled || activeRenderGenerationRef.current !== effectRenderGeneration) return;
1792
+ if (isInactive() || activeRenderGenerationRef.current !== effectRenderGeneration) return;
1705
1793
  const message = err instanceof Error ? err.message : "PayPal failed to render.";
1706
1794
  appendDebug(`onError rendered=${rendered} msg=${message.slice(0, 120)}`);
1707
1795
  if (rendered) {
@@ -1721,13 +1809,16 @@ function DirectPayPalButtonImplementation({
1721
1809
  paymentMethodCategory: "paypal",
1722
1810
  requestCategory: "provider_sdk"
1723
1811
  });
1812
+ if (isProviderDecline(err)) {
1813
+ reportPayPalDecline(err, context);
1814
+ }
1724
1815
  if (context) attemptContextBySurfaceRef.current.delete(context.surfaceKey);
1725
1816
  beforeClickRef.current = null;
1726
1817
  invalidateAttempt({ generation: context?.generation, remount: true, showRetry: true, focus: true });
1727
1818
  notifyTechnicalFailure(err, { code: "paypal_runtime_failed" });
1728
1819
  return;
1729
1820
  }
1730
- markRenderFailed(message);
1821
+ handlePreRenderFailure(message);
1731
1822
  }
1732
1823
  });
1733
1824
  const eligible = buttons.isEligible();
@@ -1740,17 +1831,19 @@ function DirectPayPalButtonImplementation({
1740
1831
  });
1741
1832
  if (!eligible) {
1742
1833
  setReady(false);
1743
- markRenderFailed(
1834
+ handlePreRenderFailure(
1744
1835
  "PayPal buttons are not eligible to render in this context (paypal_ineligible)."
1745
1836
  );
1746
1837
  return;
1747
1838
  }
1748
1839
  const typedButtons = buttons;
1840
+ ownedButtons = typedButtons;
1749
1841
  if (debug) {
1750
1842
  appendDebug(`container:dims ${formatDims(container)} visibility=${typeof document !== "undefined" ? document.visibilityState : "(no document)"}`);
1751
1843
  }
1752
1844
  if (debug && typeof MutationObserver !== "undefined") {
1753
1845
  containerObserver = new MutationObserver((mutations) => {
1846
+ if (isInactive()) return;
1754
1847
  for (const mutation of mutations) {
1755
1848
  if (mutation.type === "childList") {
1756
1849
  mutation.addedNodes.forEach((node) => {
@@ -1778,7 +1871,7 @@ function DirectPayPalButtonImplementation({
1778
1871
  });
1779
1872
  }
1780
1873
  const snapshotContainer = (when) => {
1781
- if (cancelled || rendered) return;
1874
+ if (isInactive() || rendered) return;
1782
1875
  const iframes = container.querySelectorAll("iframe");
1783
1876
  const hasStorageAccess = typeof document !== "undefined" && "hasStorageAccess" in document;
1784
1877
  appendDebug(`watchdog:${when} container=${formatDims(container)} children=${container.childElementCount} iframes=${iframes.length} visibility=${typeof document !== "undefined" ? document.visibilityState : "(no document)"} cookies=${typeof navigator !== "undefined" ? navigator.cookieEnabled : "?"} hasStorageAccessApi=${hasStorageAccess} paypalNetRequests=${paypalRequestCount.value}`);
@@ -1798,17 +1891,25 @@ function DirectPayPalButtonImplementation({
1798
1891
  watchdogTimers.push(setTimeout(() => snapshotContainer("15s"), 15e3));
1799
1892
  }
1800
1893
  appendDebug("render:start");
1894
+ watchdogTimers.push(setTimeout(() => {
1895
+ if (isInactive() || rendered) return;
1896
+ appendDebug("watchdog:11s retryable render timeout");
1897
+ handlePreRenderFailure(
1898
+ "PayPal Buttons render did not settle within 11000ms.",
1899
+ true
1900
+ );
1901
+ }, PAYPAL_RENDER_WATCHDOG_MS));
1801
1902
  buttons.render(container).then(() => {
1802
- appendDebug(`render:resolved cancelled=${cancelled}`);
1903
+ appendDebug(`render:resolved inactive=${isInactive()}`);
1803
1904
  stopDiagnostics();
1804
- if (cancelled) {
1805
- typedButtons.close().catch(() => {
1806
- });
1905
+ if (isInactive()) {
1906
+ closeOwnedButtons();
1807
1907
  return;
1808
1908
  }
1809
- activeButtons = typedButtons;
1810
1909
  rendered = true;
1811
1910
  setReady(true);
1911
+ setRecovering(false);
1912
+ onInitializationStateChangeRef.current?.("ready");
1812
1913
  telemetrySource.log({
1813
1914
  name: "provider.ready",
1814
1915
  stage: "provider_ready",
@@ -1826,24 +1927,22 @@ function DirectPayPalButtonImplementation({
1826
1927
  const message = err instanceof Error ? err.message : "PayPal failed to render.";
1827
1928
  appendDebug(`render:rejected msg=${message.slice(0, 120)}`);
1828
1929
  stopDiagnostics();
1829
- markRenderFailed(message);
1930
+ handlePreRenderFailure(message);
1830
1931
  });
1831
1932
  }).catch((err) => {
1832
1933
  const message = err instanceof Error ? err.message : "PayPal SDK failed to load.";
1833
1934
  appendDebug(`loadScript:rejected msg=${message.slice(0, 120)}`);
1834
- markRenderFailed(message);
1935
+ handlePreRenderFailure(message);
1835
1936
  });
1836
1937
  }, 0);
1837
1938
  return () => {
1838
1939
  cancelled = true;
1839
1940
  clearTimeout(startTimer);
1941
+ if (retryTimer) clearTimeout(retryTimer);
1840
1942
  stopDiagnostics();
1841
- if (activeButtons) {
1842
- activeButtons.close().catch(() => {
1843
- });
1844
- }
1943
+ closeOwnedButtons();
1845
1944
  };
1846
- }, [baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration, telemetrySource]);
1945
+ }, [allowAutomaticRetry, baseUrl, clientId, currency, environment, isSubscription, sessionId, existingOrderId, renderGeneration, telemetrySource]);
1847
1946
  const debugPanel = debug ? /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1848
1947
  "pre",
1849
1948
  {
@@ -1868,7 +1967,7 @@ ${debugLines.join("\n")}`
1868
1967
  ]
1869
1968
  }
1870
1969
  ) : null;
1871
- if (failed) {
1970
+ if (failed && !initializationConfigChanged) {
1872
1971
  return debug ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { children: debugPanel }) : null;
1873
1972
  }
1874
1973
  return (
@@ -1878,38 +1977,51 @@ ${debugLines.join("\n")}`
1878
1977
  // intentionally has no margin/padding so the parent owns all spacing.
1879
1978
  /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
1880
1979
  debugPanel,
1881
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { position: "relative", minHeight: DEFAULT_BUTTON_HEIGHT }, children: [
1882
- !ready && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1883
- "div",
1884
- {
1885
- "data-testid": "flopay-direct-paypal-placeholder",
1886
- style: {
1887
- position: "absolute",
1888
- inset: 0,
1889
- borderRadius: 8,
1890
- background: "#e5e7eb",
1891
- animation: "flopay-pulse 1.5s ease-in-out infinite",
1892
- pointerEvents: "none"
1893
- }
1894
- }
1895
- ),
1896
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1897
- "div",
1898
- {
1899
- ref: containerRef,
1900
- "data-testid": "flopay-direct-paypal-container",
1901
- tabIndex: -1,
1902
- "aria-label": "PayPal payment method",
1903
- style: {
1904
- minHeight: DEFAULT_BUTTON_HEIGHT,
1905
- display: "flex",
1906
- opacity: ready ? 1 : 0
1907
- },
1908
- "aria-busy": submitting || isProcessing
1909
- },
1910
- renderGeneration
1911
- )
1912
- ] }),
1980
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1981
+ "div",
1982
+ {
1983
+ "aria-hidden": recovering ? true : void 0,
1984
+ style: recovering ? {
1985
+ position: "relative",
1986
+ height: 0,
1987
+ opacity: 0,
1988
+ pointerEvents: "none"
1989
+ } : { position: "relative", minHeight: DEFAULT_BUTTON_HEIGHT },
1990
+ children: [
1991
+ !ready && !recovering && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1992
+ "div",
1993
+ {
1994
+ "data-testid": "flopay-direct-paypal-placeholder",
1995
+ style: {
1996
+ position: "absolute",
1997
+ inset: 0,
1998
+ borderRadius: 8,
1999
+ background: "#e5e7eb",
2000
+ animation: "flopay-pulse 1.5s ease-in-out infinite",
2001
+ pointerEvents: "none"
2002
+ }
2003
+ }
2004
+ ),
2005
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2006
+ "div",
2007
+ {
2008
+ ref: containerRef,
2009
+ "data-testid": "flopay-direct-paypal-container",
2010
+ tabIndex: -1,
2011
+ "aria-label": "PayPal payment method",
2012
+ style: {
2013
+ minHeight: DEFAULT_BUTTON_HEIGHT,
2014
+ display: "flex",
2015
+ opacity: ready ? 1 : 0
2016
+ },
2017
+ "aria-busy": submitting || isProcessing
2018
+ },
2019
+ renderGeneration
2020
+ )
2021
+ ]
2022
+ },
2023
+ renderGeneration
2024
+ ),
1913
2025
  showRetryFocusTarget && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1914
2026
  "button",
1915
2027
  {
@@ -1937,6 +2049,22 @@ var import_jsx_runtime7 = require("react/jsx-runtime");
1937
2049
  var STRIPE_RESUME_KEY = "flopay_stripe_resume";
1938
2050
  var LEGACY_WALLET_RESUME_KEY = "flopay_wallet_resume";
1939
2051
  var PAYPAL_RESUME_KEY = "flopay_paypal_resume";
2052
+ async function reportNonCardDecline({
2053
+ billingApiUrl,
2054
+ sessionId,
2055
+ nonce,
2056
+ request
2057
+ }) {
2058
+ if (!sessionId || !nonce) return;
2059
+ try {
2060
+ await new import_js2.PaymentAPI(billingApiUrl, { telemetry: false }).reportSessionIntentDecline(
2061
+ sessionId,
2062
+ nonce,
2063
+ request
2064
+ );
2065
+ } catch {
2066
+ }
2067
+ }
1940
2068
  function isAccountValidationError(err) {
1941
2069
  return err instanceof import_shared6.FloPayError && typeof err.statusCode === "number" && err.statusCode >= 400 && err.statusCode < 500;
1942
2070
  }
@@ -2226,11 +2354,9 @@ function ExpressCheckoutReadySwap({
2226
2354
  function isExpressCheckoutRowVisible(state) {
2227
2355
  return state !== "unavailable" && state !== "load_error";
2228
2356
  }
2229
- var SplitCardForm = (0, import_react9.forwardRef)(
2230
- function SplitCardForm2(props, ref) {
2231
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SplitCardFormInner, { ...props, innerRef: ref });
2232
- }
2233
- );
2357
+ function SplitCardForm(props) {
2358
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(SplitCardFormInner, { ...props });
2359
+ }
2234
2360
  function PayPalButtonInner({
2235
2361
  sessionId,
2236
2362
  nonce,
@@ -2261,7 +2387,6 @@ function PayPalButtonInner({
2261
2387
  const [showRecoveryAction, setShowRecoveryAction] = (0, import_react9.useState)(false);
2262
2388
  const pendingProviderFocusRef = (0, import_react9.useRef)(false);
2263
2389
  const attemptContextBySurfaceRef = (0, import_react9.useRef)(/* @__PURE__ */ new Map());
2264
- const baseUrl = billingApiUrl.replace(/\/+$/, "");
2265
2390
  const {
2266
2391
  startAttempt,
2267
2392
  finishAttempt,
@@ -2432,33 +2557,21 @@ function PayPalButtonInner({
2432
2557
  event.paymentFailed({ reason: "fail" });
2433
2558
  return;
2434
2559
  }
2435
- const intentHeaders = { "Content-Type": "application/json" };
2436
- if (effectiveNonce) intentHeaders["x-checkout-session-token"] = effectiveNonce;
2437
- const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
2438
- method: "POST",
2439
- headers: intentHeaders,
2440
- body: JSON.stringify({
2441
- sessionId: effectiveSessionId,
2442
- email: effectiveEmail,
2443
- paymentMethodType: paymentMethod?.id ?? "paypal",
2444
- isPaypal: "true",
2445
- setupFutureUsage: "off_session",
2446
- setup_future_usage: "off_session"
2447
- })
2448
- });
2449
- if (!intentResponse.ok) {
2450
- const intentError = await buildFloPayApiErrorFromResponse(
2451
- intentResponse,
2452
- "Failed to create payment intent"
2453
- );
2454
- onErrorChange?.(intentError.message);
2455
- onDecline?.(buildDeclineEvent("paypal", intentError));
2456
- event.paymentFailed({ reason: "fail", message: intentError.message });
2457
- return;
2560
+ const intent = await new import_js2.PaymentAPI(billingApiUrl).createSessionIntent(
2561
+ effectiveSessionId,
2562
+ effectiveNonce ?? "",
2563
+ {
2564
+ provider: "stripe",
2565
+ paymentMethodCategory: "wallet",
2566
+ paymentMethodType: "paypal",
2567
+ paymentMethodId: paymentMethod?.id ?? null,
2568
+ intentKind: "payment"
2569
+ }
2570
+ );
2571
+ if (intent.provider !== "stripe") {
2572
+ throw new Error("Invalid provider returned for PayPal intent");
2458
2573
  }
2459
- const intentJson = await intentResponse.json();
2460
- const intentClientSecret = intentJson.data?.id;
2461
- if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
2574
+ const intentClientSecret = intent.clientSecret;
2462
2575
  const confirmParams = {
2463
2576
  return_url: window.location.href
2464
2577
  };
@@ -2495,6 +2608,17 @@ function PayPalButtonInner({
2495
2608
  code: getProviderErrorCode(confirmError),
2496
2609
  declineCode: getProviderDeclineCode(confirmError)
2497
2610
  }));
2611
+ await reportNonCardDecline({
2612
+ billingApiUrl,
2613
+ sessionId: effectiveSessionId,
2614
+ nonce: effectiveNonce,
2615
+ request: {
2616
+ provider: "stripe",
2617
+ paymentMethodCategory: "wallet",
2618
+ paymentMethodType: "paypal",
2619
+ providerDeclineReason: getProviderErrorCode(confirmError) ?? "provider_declined"
2620
+ }
2621
+ });
2498
2622
  } else {
2499
2623
  recoverTechnicalFailure(confirmError, "stripe_paypal_confirm_failed", attemptContext?.generation);
2500
2624
  }
@@ -2523,7 +2647,7 @@ function PayPalButtonInner({
2523
2647
  } finally {
2524
2648
  setSubmitting(false);
2525
2649
  }
2526
- }, [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]);
2650
+ }, [stripe, elements, sessionId, nonce, email, billingApiUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]);
2527
2651
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
2528
2652
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2529
2653
  ExpressCheckoutReadySwap,
@@ -2599,6 +2723,7 @@ function WalletButtonInner({
2599
2723
  nonce,
2600
2724
  email,
2601
2725
  billingApiUrl,
2726
+ intentKind,
2602
2727
  expressMethods,
2603
2728
  onTokenizedBody,
2604
2729
  onErrorChange,
@@ -2617,7 +2742,6 @@ function WalletButtonInner({
2617
2742
  onLoadStateChange?.(loadState);
2618
2743
  }, [loadState, onLoadStateChange]);
2619
2744
  const [submitting, setSubmitting] = (0, import_react9.useState)(false);
2620
- const baseUrl = billingApiUrl.replace(/\/+$/, "");
2621
2745
  const focusTargetRef = (0, import_react9.useRef)(null);
2622
2746
  const recoveryActionRef = (0, import_react9.useRef)(null);
2623
2747
  const [surfaceKey, setSurfaceKey] = (0, import_react9.useState)(0);
@@ -2718,31 +2842,21 @@ function WalletButtonInner({
2718
2842
  if (!effectiveSessionId || !effectiveEmail) {
2719
2843
  throw new Error("Missing sessionId or email for wallet payment");
2720
2844
  }
2721
- const intentHeaders = { "Content-Type": "application/json" };
2722
- if (effectiveNonce) intentHeaders["x-checkout-session-token"] = effectiveNonce;
2723
- const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
2724
- method: "POST",
2725
- headers: intentHeaders,
2726
- body: JSON.stringify({
2727
- sessionId: effectiveSessionId,
2728
- email: effectiveEmail,
2729
- paymentMethodType: paymentMethod.id,
2730
- isPaypal: false
2731
- })
2732
- });
2733
- if (!intentResponse.ok) {
2734
- const intentError = await buildFloPayApiErrorFromResponse(
2735
- intentResponse,
2736
- "Failed to create payment intent"
2737
- );
2738
- onErrorChange?.(intentError.message);
2739
- onDecline?.(buildDeclineEvent(method, intentError));
2740
- event.paymentFailed({ reason: "fail", message: intentError.message });
2741
- return;
2845
+ const intent = await new import_js2.PaymentAPI(billingApiUrl).createSessionIntent(
2846
+ effectiveSessionId,
2847
+ effectiveNonce ?? "",
2848
+ {
2849
+ provider: "stripe",
2850
+ paymentMethodCategory: "wallet",
2851
+ paymentMethodType: walletType ?? buttonMethod,
2852
+ paymentMethodId: paymentMethod.id,
2853
+ intentKind
2854
+ }
2855
+ );
2856
+ if (intent.provider !== "stripe") {
2857
+ throw new Error("Invalid provider returned for wallet intent");
2742
2858
  }
2743
- const intentJson = await intentResponse.json();
2744
- const intentClientSecret = intentJson.data?.id;
2745
- if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
2859
+ const intentClientSecret = intent.clientSecret;
2746
2860
  const { error: confirmError, intentId } = (0, import_shared6.isSetupIntentClientSecret)(intentClientSecret) ? await stripe.confirmCardSetup(intentClientSecret, { payment_method: paymentMethod.id }).then((r) => ({ error: r.error, intentId: r.setupIntent?.id })) : await stripe.confirmCardPayment(intentClientSecret, { payment_method: paymentMethod.id }).then((r) => ({ error: r.error, intentId: r.paymentIntent?.id }));
2747
2861
  if (confirmError) {
2748
2862
  if (isProviderDecline(confirmError)) {
@@ -2752,6 +2866,17 @@ function WalletButtonInner({
2752
2866
  code: getProviderErrorCode(confirmError),
2753
2867
  declineCode: getProviderDeclineCode(confirmError)
2754
2868
  }));
2869
+ await reportNonCardDecline({
2870
+ billingApiUrl,
2871
+ sessionId: effectiveSessionId,
2872
+ nonce: effectiveNonce,
2873
+ request: {
2874
+ provider: "stripe",
2875
+ paymentMethodCategory: "wallet",
2876
+ paymentMethodType: walletType ?? buttonMethod,
2877
+ providerDeclineReason: getProviderErrorCode(confirmError) ?? "provider_declined"
2878
+ }
2879
+ });
2755
2880
  } else {
2756
2881
  recoverTechnicalFailure(walletType, confirmError, "stripe_wallet_confirm_failed", attemptContext?.generation);
2757
2882
  }
@@ -2772,7 +2897,7 @@ function WalletButtonInner({
2772
2897
  setSubmitting(false);
2773
2898
  }
2774
2899
  },
2775
- [stripe, elements, sessionId, nonce, email, baseUrl, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]
2900
+ [stripe, elements, sessionId, nonce, email, billingApiUrl, intentKind, onTokenizedBody, onErrorChange, onDecline, runBeforeButtonClick, recoverTechnicalFailure, finishAttempt, isAttemptCurrent, isAttemptInvalidated, surfaceKey]
2776
2901
  );
2777
2902
  const expressMethodMap = (0, import_react9.useMemo)(() => {
2778
2903
  const allKeys = ["applePay", "googlePay", "paypal", "link", "amazonPay", "klarna"];
@@ -2905,6 +3030,7 @@ function StripeMethodButton({
2905
3030
  fontFamily
2906
3031
  }) {
2907
3032
  const brand = (0, import_shared5.resolveStripeMethodBrandVariant)(method, themeId);
3033
+ const vendoredLogoUrl = PAYMENT_METHOD_LOGO_URLS[method];
2908
3034
  const resolvedBackground = brand?.backgroundColor ?? backgroundColor ?? "#ffffff";
2909
3035
  const resolvedBorder = brand?.borderColor ?? borderColor ?? "#d1d5db";
2910
3036
  const resolvedTextColor = brand?.textColor ?? textColor ?? "#262833";
@@ -2970,15 +3096,36 @@ function StripeMethodButton({
2970
3096
  gap: (0, import_shared5.hasVendoredStripeMethodLogo)(method) ? 8 : 0
2971
3097
  },
2972
3098
  children: [
2973
- brand?.logoSvg && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3099
+ vendoredLogoUrl ? (
3100
+ // Vendored brand logo — an <img> asset (not inline SVG) so its
3101
+ // bytes ship as a sibling file, out of the JS bundle. 3:2 box
3102
+ // matching the datatrans logos' 120×80 viewBox; each ships its own
3103
+ // white rounded-rect background so it stays legible on any tile.
3104
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
3105
+ "img",
3106
+ {
3107
+ src: vendoredLogoUrl,
3108
+ alt: "",
3109
+ "aria-hidden": "true",
3110
+ loading: "lazy",
3111
+ decoding: "async",
3112
+ width: 30,
3113
+ height: 20,
3114
+ style: {
3115
+ width: 30,
3116
+ height: 20,
3117
+ flexShrink: 0,
3118
+ borderRadius: 3,
3119
+ objectFit: "contain",
3120
+ display: "inline-flex"
3121
+ }
3122
+ }
3123
+ )
3124
+ ) : brand?.logoSvg ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2974
3125
  "span",
2975
3126
  {
2976
3127
  "aria-hidden": "true",
2977
3128
  style: {
2978
- // 3:2 box matching the vendored datatrans logos' 120×80
2979
- // viewBox. Each logo ships with its own white rounded-rect
2980
- // background baked in, so the mark stays legible on any
2981
- // brand-coloured tile without an extra chip wrapper here.
2982
3129
  width: 30,
2983
3130
  height: 20,
2984
3131
  flexShrink: 0,
@@ -2988,7 +3135,7 @@ function StripeMethodButton({
2988
3135
  },
2989
3136
  dangerouslySetInnerHTML: { __html: brand.logoSvg }
2990
3137
  }
2991
- ),
3138
+ ) : null,
2992
3139
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: (0, import_shared5.getStripeMethodDisplayName)(method) })
2993
3140
  ]
2994
3141
  }
@@ -3046,7 +3193,6 @@ function StripeMethodInlineForm({
3046
3193
  const submittingRef = (0, import_react9.useRef)(false);
3047
3194
  const [isMethodComplete, setIsMethodComplete] = (0, import_react9.useState)(false);
3048
3195
  const [loadState, setLoadState] = (0, import_react9.useState)("loading");
3049
- const baseUrl = billingApiUrl.replace(/\/+$/, "");
3050
3196
  const handlePay = (0, import_react9.useCallback)(async () => {
3051
3197
  if (!stripe || !elements || isProcessing || submittingRef.current || !isMethodComplete) return;
3052
3198
  submittingRef.current = true;
@@ -3077,27 +3223,22 @@ function StripeMethodInlineForm({
3077
3223
  if (!effectiveSessionId || !effectiveEmail) {
3078
3224
  throw new Error("Missing sessionId or email for payment.");
3079
3225
  }
3080
- const intentHeaders = { "Content-Type": "application/json" };
3081
- if (effectiveNonce) intentHeaders["x-checkout-session-token"] = effectiveNonce;
3082
- const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
3083
- method: "POST",
3084
- headers: intentHeaders,
3085
- body: JSON.stringify({
3086
- sessionId: effectiveSessionId,
3087
- email: effectiveEmail,
3088
- paymentMethodType: paymentMethod.type || method,
3089
- isPaypal: false
3090
- })
3091
- });
3092
- if (!intentResponse.ok) {
3093
- const intentError = await buildFloPayApiErrorFromResponse(intentResponse, "Failed to create payment intent");
3094
- onErrorChange?.(intentError.message);
3095
- onDecline?.(buildDeclineEvent("card", intentError));
3096
- return;
3226
+ const paymentMethodType = paymentMethod.type || method;
3227
+ const intent = await new import_js2.PaymentAPI(billingApiUrl).createSessionIntent(
3228
+ effectiveSessionId,
3229
+ effectiveNonce ?? "",
3230
+ {
3231
+ provider: "stripe",
3232
+ paymentMethodCategory: "apm",
3233
+ paymentMethodType,
3234
+ paymentMethodId: null,
3235
+ intentKind: "payment"
3236
+ }
3237
+ );
3238
+ if (intent.provider !== "stripe") {
3239
+ throw new Error("Invalid provider returned for APM intent");
3097
3240
  }
3098
- const intentJson = await intentResponse.json();
3099
- const intentClientSecret = intentJson.data?.id;
3100
- if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
3241
+ const intentClientSecret = intent.clientSecret;
3101
3242
  if (typeof window !== "undefined") {
3102
3243
  try {
3103
3244
  localStorage.setItem(STRIPE_RESUME_KEY, JSON.stringify({
@@ -3130,11 +3271,21 @@ function StripeMethodInlineForm({
3130
3271
  const message = confirmError.message ?? "Payment failed.";
3131
3272
  onErrorChange?.(message);
3132
3273
  onDecline?.(buildDeclineEvent("card", message, { code: confirmError.code }));
3274
+ await reportNonCardDecline({
3275
+ billingApiUrl,
3276
+ sessionId: effectiveSessionId,
3277
+ nonce: effectiveNonce,
3278
+ request: {
3279
+ provider: "stripe",
3280
+ paymentMethodCategory: "apm",
3281
+ paymentMethodType: method,
3282
+ providerDeclineReason: confirmError.code ?? "provider_declined"
3283
+ }
3284
+ });
3133
3285
  return;
3134
3286
  }
3135
- const SUCCESSFUL_PI_STATUSES = /* @__PURE__ */ new Set(["succeeded", "requires_capture", "processing"]);
3136
3287
  const piStatus = paymentIntent?.status;
3137
- if (!piStatus || !SUCCESSFUL_PI_STATUSES.has(piStatus)) {
3288
+ if (!isSuccessfulPaymentIntentStatus(piStatus)) {
3138
3289
  if (typeof window !== "undefined") {
3139
3290
  try {
3140
3291
  localStorage.removeItem(STRIPE_RESUME_KEY);
@@ -3180,7 +3331,7 @@ function StripeMethodInlineForm({
3180
3331
  sessionId,
3181
3332
  nonce,
3182
3333
  email,
3183
- baseUrl,
3334
+ billingApiUrl,
3184
3335
  method,
3185
3336
  onTokenizedBody,
3186
3337
  onErrorChange,
@@ -3244,22 +3395,7 @@ function StripeMethodInlineForm({
3244
3395
  children: submitting ? "Processing\u2026" : `Pay with ${(0, import_shared5.getStripeMethodDisplayName)(method)}`
3245
3396
  }
3246
3397
  ),
3247
- errorText && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { role: "alert", "data-testid": "flopay-error", style: {
3248
- margin: "0.75rem 0 0",
3249
- padding: "0.625rem 0.875rem",
3250
- background: "#FEF2F2",
3251
- border: "1px solid #FECACA",
3252
- borderRadius: "8px",
3253
- color: "#991B1B",
3254
- fontSize: "0.85rem",
3255
- fontWeight: 600,
3256
- display: "flex",
3257
- alignItems: "center",
3258
- gap: "0.5rem"
3259
- }, children: [
3260
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 9v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z", stroke: "#DC2626", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }),
3261
- errorText
3262
- ] })
3398
+ errorText && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorBanner, { margin: "0.75rem 0 0", children: errorText })
3263
3399
  ] });
3264
3400
  }
3265
3401
  function StripePaymentElementInner({
@@ -3286,7 +3422,6 @@ function StripePaymentElementInner({
3286
3422
  onExpandApm,
3287
3423
  expandedApmMethod
3288
3424
  }) {
3289
- const baseUrl = billingApiUrl.replace(/\/+$/, "");
3290
3425
  const [expandedMethod, setExpandedMethod] = (0, import_react9.useState)(null);
3291
3426
  const [submittingMethod, setSubmittingMethod] = (0, import_react9.useState)(null);
3292
3427
  const submittingRef = (0, import_react9.useRef)(false);
@@ -3317,31 +3452,21 @@ function StripePaymentElementInner({
3317
3452
  if (!effectiveSessionId || !effectiveEmail) {
3318
3453
  throw new Error("Missing sessionId or email for payment.");
3319
3454
  }
3320
- const intentHeaders = { "Content-Type": "application/json" };
3321
- if (effectiveNonce) intentHeaders["x-checkout-session-token"] = effectiveNonce;
3322
- const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
3323
- method: "POST",
3324
- headers: intentHeaders,
3325
- body: JSON.stringify({
3326
- sessionId: effectiveSessionId,
3327
- email: effectiveEmail,
3328
- // Wire type, not pm_xxx — backend scopes `payment_method_types`
3329
- // to this single method so unrelated account-wide methods
3330
- // (apple_pay without domain verification, …) don't poison PI
3331
- // creation.
3455
+ const intent = await new import_js2.PaymentAPI(billingApiUrl).createSessionIntent(
3456
+ effectiveSessionId,
3457
+ effectiveNonce ?? "",
3458
+ {
3459
+ provider: "stripe",
3460
+ paymentMethodCategory: "apm",
3332
3461
  paymentMethodType: method,
3333
- isPaypal: false
3334
- })
3335
- });
3336
- if (!intentResponse.ok) {
3337
- const intentError = await buildFloPayApiErrorFromResponse(intentResponse, "Failed to create payment intent");
3338
- onErrorChange?.(intentError.message);
3339
- onDecline?.(buildDeclineEvent("card", intentError));
3340
- return;
3462
+ paymentMethodId: null,
3463
+ intentKind: "payment"
3464
+ }
3465
+ );
3466
+ if (intent.provider !== "stripe") {
3467
+ throw new Error("Invalid provider returned for APM intent");
3341
3468
  }
3342
- const intentJson = await intentResponse.json();
3343
- const intentClientSecret = intentJson.data?.id;
3344
- if (!intentClientSecret) throw new Error("No client_secret in payment intent response");
3469
+ const intentClientSecret = intent.clientSecret;
3345
3470
  if (typeof window !== "undefined") {
3346
3471
  try {
3347
3472
  localStorage.setItem(STRIPE_RESUME_KEY, JSON.stringify({
@@ -3382,11 +3507,21 @@ function StripePaymentElementInner({
3382
3507
  const message = confirmError.message ?? "Payment failed.";
3383
3508
  onErrorChange?.(message);
3384
3509
  onDecline?.(buildDeclineEvent("card", message, { code: confirmError.code }));
3510
+ await reportNonCardDecline({
3511
+ billingApiUrl,
3512
+ sessionId: effectiveSessionId,
3513
+ nonce: effectiveNonce,
3514
+ request: {
3515
+ provider: "stripe",
3516
+ paymentMethodCategory: "apm",
3517
+ paymentMethodType: method,
3518
+ providerDeclineReason: confirmError.code ?? "provider_declined"
3519
+ }
3520
+ });
3385
3521
  return;
3386
3522
  }
3387
- const SUCCESSFUL_PI_STATUSES = /* @__PURE__ */ new Set(["succeeded", "requires_capture", "processing"]);
3388
3523
  const piStatus = paymentIntent?.status;
3389
- if (!piStatus || !SUCCESSFUL_PI_STATUSES.has(piStatus)) {
3524
+ if (!isSuccessfulPaymentIntentStatus(piStatus)) {
3390
3525
  if (typeof window !== "undefined") {
3391
3526
  try {
3392
3527
  localStorage.removeItem(STRIPE_RESUME_KEY);
@@ -3430,7 +3565,7 @@ function StripePaymentElementInner({
3430
3565
  sessionId,
3431
3566
  nonce,
3432
3567
  email,
3433
- baseUrl,
3568
+ billingApiUrl,
3434
3569
  billingName,
3435
3570
  onTokenizedBody,
3436
3571
  onErrorChange,
@@ -3531,15 +3666,10 @@ function SplitCardFormInner({
3531
3666
  firstName,
3532
3667
  lastName,
3533
3668
  chv,
3534
- submitLabel = "CONFIRM PAYMENT",
3535
3669
  className,
3536
- children,
3537
3670
  isProcessing: externalProcessing,
3538
3671
  error: externalError,
3539
3672
  onErrorChange,
3540
- onFullNameChange,
3541
- onFirstNameChange,
3542
- onLastNameChange,
3543
3673
  showPayPal = true,
3544
3674
  showStripe = true,
3545
3675
  enabledPaymentMethods,
@@ -3578,17 +3708,14 @@ function SplitCardFormInner({
3578
3708
  directPaypal,
3579
3709
  isSubscription = false,
3580
3710
  session,
3581
- debug = false,
3582
- innerRef
3711
+ debug = false
3583
3712
  }) {
3584
3713
  const flopay = useFloPay();
3585
3714
  const paypalFlopay = usePayPalFloPay();
3586
- const elements = useElements();
3587
3715
  const checkout = (0, import_react9.useContext)(CheckoutContext);
3588
3716
  const contextBillingUrl = useBillingApiUrl();
3589
3717
  const [processing, setProcessing] = (0, import_react9.useState)(false);
3590
3718
  const [error, setError] = (0, import_react9.useState)(null);
3591
- const [is3DSActive, setIs3DSActive] = (0, import_react9.useState)(false);
3592
3719
  const [selectedCountry, setSelectedCountry] = (0, import_react9.useState)(countryProp ?? "US");
3593
3720
  const [zipCode, setZipCode] = (0, import_react9.useState)(zipProp ?? "");
3594
3721
  const [addressLine1, setAddressLine1] = (0, import_react9.useState)(addressLine1Prop ?? "");
@@ -3606,8 +3733,9 @@ function SplitCardFormInner({
3606
3733
  const enableAVS = avsConfig !== null;
3607
3734
  const vaultBlockReady = Boolean(session?.vault?.html);
3608
3735
  const vaultGatewayAdvertised = Boolean(session?.gateways?.pcivault);
3736
+ const vaultCapabilityAvailable = vaultBlockReady || vaultGatewayAdvertised;
3609
3737
  const vaultActive = Boolean(
3610
- showStripe && (vaultBlockReady || vaultGatewayAdvertised) && flopay && sessionId
3738
+ showStripe && vaultCapabilityAvailable && flopay && sessionId
3611
3739
  );
3612
3740
  const cardCapture = (0, import_react9.useMemo)(() => {
3613
3741
  if (!flopay || !vaultActive || !sessionId) return null;
@@ -3639,7 +3767,9 @@ function SplitCardFormInner({
3639
3767
  const [vaultMount, setVaultMount] = (0, import_react9.useState)(
3640
3768
  () => toVaultMount(session?.vault)
3641
3769
  );
3642
- const [viewState, setViewState] = (0, import_react9.useState)(initialCardOpen ? "card" : "buttons");
3770
+ const [viewState, setViewState] = (0, import_react9.useState)(
3771
+ initialCardOpen && vaultCapabilityAvailable ? "card" : "buttons"
3772
+ );
3643
3773
  const [expandedApmMethod, setExpandedApmMethod] = (0, import_react9.useState)(null);
3644
3774
  const showCardForm = viewState === "expanding" || viewState === "card";
3645
3775
  const TRANSITION_MS = 280;
@@ -3664,12 +3794,10 @@ function SplitCardFormInner({
3664
3794
  }, TRANSITION_MS);
3665
3795
  }, []);
3666
3796
  (0, import_react9.useEffect)(() => {
3667
- if (layout === "buttons" && initialCardOpen) {
3797
+ if (layout === "buttons" && initialCardOpen && vaultCapabilityAvailable) {
3668
3798
  setViewState("card");
3669
3799
  }
3670
- }, [layout, initialCardOpen]);
3671
- const [fullName, setFullName] = (0, import_react9.useState)("");
3672
- const [formReady, setFormReady] = (0, import_react9.useState)(false);
3800
+ }, [layout, initialCardOpen, vaultCapabilityAvailable]);
3673
3801
  const [overlayStatus, setOverlayStatus] = (0, import_react9.useState)(null);
3674
3802
  const processingRef = (0, import_react9.useRef)(false);
3675
3803
  const [paypalDirectRetry, setPaypalDirectRetry] = (0, import_react9.useState)(null);
@@ -3722,7 +3850,6 @@ function SplitCardFormInner({
3722
3850
  }, [appearance, bStyles, layout]);
3723
3851
  const isInlineSessionPatchProcessing = checkout.inlineSessionPatchProcessing ?? false;
3724
3852
  const isSubmitting = (externalProcessing ?? processing) || isInlineSessionPatchProcessing;
3725
- const isSelfContained = !onTokenizedBody;
3726
3853
  const baseUrl = resolvedBillingApiUrl.replace(/\/+$/, "");
3727
3854
  const resolvedAccount = (0, import_react9.useMemo)(() => mergeAccountPatch({
3728
3855
  userId,
@@ -3732,6 +3859,7 @@ function SplitCardFormInner({
3732
3859
  country: countryProp,
3733
3860
  zip: zipProp
3734
3861
  }, accountPatch), [userId, email, firstName, lastName, countryProp, zipProp, accountPatch]);
3862
+ const resolvedBillingName = [resolvedAccount.firstName, resolvedAccount.lastName].filter((part) => Boolean(part?.trim())).join(" ");
3735
3863
  const stripeInstance = (0, import_react9.useMemo)(() => {
3736
3864
  if (!flopay) return null;
3737
3865
  return flopay.getRawProvider();
@@ -3871,7 +3999,6 @@ function SplitCardFormInner({
3871
3999
  updateError,
3872
4000
  emitDecline,
3873
4001
  resolvedAccount,
3874
- fullName,
3875
4002
  avsConfig,
3876
4003
  avsCheckProp,
3877
4004
  sessionId,
@@ -3884,7 +4011,6 @@ function SplitCardFormInner({
3884
4011
  updateError,
3885
4012
  emitDecline,
3886
4013
  resolvedAccount,
3887
- fullName,
3888
4014
  avsConfig,
3889
4015
  avsCheckProp,
3890
4016
  sessionId,
@@ -3893,7 +4019,7 @@ function SplitCardFormInner({
3893
4019
  };
3894
4020
  const vaultCompletedRef = (0, import_react9.useRef)(false);
3895
4021
  const buildVaultAccountSnapshot = (0, import_react9.useCallback)(() => {
3896
- const { resolvedAccount: resolvedAccount2, avsConfig: avsConfig2, fullName: fullName2, avsCheckProp: avsCheckProp2 } = vaultOutcomeRef.current;
4022
+ const { resolvedAccount: resolvedAccount2, avsConfig: avsConfig2, avsCheckProp: avsCheckProp2 } = vaultOutcomeRef.current;
3897
4023
  const cc = selectedCountryRef.current || resolvedAccount2.country || "US";
3898
4024
  const stateVisible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.state, cc) : false;
3899
4025
  const line1Visible = avsConfig2 ? (0, import_shared5.isAVSFieldVisible)(avsConfig2.address_line_1, cc) : false;
@@ -3906,8 +4032,8 @@ function SplitCardFormInner({
3906
4032
  accountData: {
3907
4033
  userId: resolvedAccount2.userId ?? "",
3908
4034
  email: resolvedAccount2.email ?? "",
3909
- firstName: resolvedAccount2.firstName ?? fullName2.trim().split(/\s+/)[0] ?? "",
3910
- lastName: resolvedAccount2.lastName ?? fullName2.trim().split(/\s+/).slice(1).join(" ") ?? "",
4035
+ firstName: resolvedAccount2.firstName ?? "",
4036
+ lastName: resolvedAccount2.lastName ?? "",
3911
4037
  ...zipVisible && zipCodeRef.current ? { zip: zipCodeRef.current } : {},
3912
4038
  ...cityVisible && cityRef.current ? { city: cityRef.current } : {},
3913
4039
  ...stateValue2 ? { state: stateValue2 } : {},
@@ -4035,6 +4161,7 @@ function SplitCardFormInner({
4035
4161
  const [walletLoadState, setWalletLoadState] = (0, import_react9.useState)("loading");
4036
4162
  const [paymentElementLoadState, setPaymentElementLoadState] = (0, import_react9.useState)("loading");
4037
4163
  const [directPaypalReady, setDirectPaypalReady] = (0, import_react9.useState)(false);
4164
+ const [directPaypalInitializationState, setDirectPaypalInitializationState] = (0, import_react9.useState)("loading");
4038
4165
  const [inAppBrowserDetected, setInAppBrowserDetected] = (0, import_react9.useState)();
4039
4166
  (0, import_react9.useEffect)(() => {
4040
4167
  setInAppBrowserDetected(isInAppBrowser());
@@ -4042,12 +4169,15 @@ function SplitCardFormInner({
4042
4169
  const shouldShowPayPal = showPayPal && (directPaypalConfigured || (hasEnabledMethodsPayload ? paypalEnabled : inAppBrowserDetected === false));
4043
4170
  const shouldShowWallets = showStripe && showWallets;
4044
4171
  const shouldRenderDirectPayPal = shouldShowPayPal && directPaypalConfigured;
4045
- const shouldRenderStripePayPal = shouldShowPayPal && !directPaypalConfigured && !!paypalStripeInstance;
4172
+ const directPaypalUnavailable = directPaypalInitializationState === "retrying" || directPaypalInitializationState === "exhausted";
4173
+ const shouldRenderStripePayPal = shouldShowPayPal && !!paypalStripeInstance && (!directPaypalConfigured || directPaypalUnavailable);
4046
4174
  const shouldRenderWallets = shouldShowWallets && !!stripeInstance;
4047
4175
  const shouldRenderPaymentElement = showStripe && hasEnabledMethods && paymentElementMethodsForCurrency.length > 0 && !!stripeInstance;
4048
- const shouldDisplayPayPalRow = shouldRenderDirectPayPal ? directPaypalReady : shouldRenderStripePayPal && isExpressCheckoutRowVisible(paypalLoadState);
4176
+ const shouldDisplayPayPalRow = shouldRenderDirectPayPal && !directPaypalUnavailable ? directPaypalReady : shouldRenderStripePayPal && isExpressCheckoutRowVisible(paypalLoadState);
4049
4177
  const shouldDisplayWalletRow = shouldRenderWallets && isExpressCheckoutRowVisible(walletLoadState);
4050
4178
  const shouldDisplayPaymentElementRow = shouldRenderPaymentElement && paymentElementLoadState !== "load_error";
4179
+ const expressSurfaceUsable = (state) => state === "loading" || state === "ready";
4180
+ const hasUsableNonCardCapability = shouldRenderDirectPayPal || shouldRenderStripePayPal && expressSurfaceUsable(paypalLoadState) || shouldRenderWallets && expressSurfaceUsable(walletLoadState) || shouldRenderPaymentElement && paymentElementLoadState !== "load_error";
4051
4181
  const validationFiredRef = (0, import_react9.useRef)(false);
4052
4182
  (0, import_react9.useEffect)(() => {
4053
4183
  if (validationFiredRef.current) return;
@@ -4067,8 +4197,27 @@ function SplitCardFormInner({
4067
4197
  );
4068
4198
  onError?.(err);
4069
4199
  updateError(err.message);
4200
+ } else if (inAppBrowserDetected !== void 0 && !vaultCapabilityAvailable && !hasUsableNonCardCapability) {
4201
+ validationFiredRef.current = true;
4202
+ const err = new import_shared6.FloPayError(
4203
+ "FloPay: this checkout session has no vault card capability and no usable supported non-card payment methods. Upgrade the billing backend integration.",
4204
+ "api_error",
4205
+ { code: "UnsupportedBackendVaultCapability" }
4206
+ );
4207
+ onError?.(err);
4208
+ updateError(err.message);
4070
4209
  }
4071
- }, [showStripe, showPayPal, directPaypalConfigured, paypalStripeInstance, onError, updateError]);
4210
+ }, [
4211
+ showStripe,
4212
+ showPayPal,
4213
+ directPaypalConfigured,
4214
+ paypalStripeInstance,
4215
+ inAppBrowserDetected,
4216
+ vaultCapabilityAvailable,
4217
+ hasUsableNonCardCapability,
4218
+ onError,
4219
+ updateError
4220
+ ]);
4072
4221
  const deprecationLoggedRef = (0, import_react9.useRef)(false);
4073
4222
  (0, import_react9.useEffect)(() => {
4074
4223
  if (deprecationLoggedRef.current) return;
@@ -4082,13 +4231,6 @@ function SplitCardFormInner({
4082
4231
  `[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.`
4083
4232
  );
4084
4233
  }, [hasEnabledMethods, showApplePay, showGooglePay]);
4085
- const handleNameChange = (0, import_react9.useCallback)((value) => {
4086
- setFullName(value);
4087
- onFullNameChange?.(value);
4088
- const parts = value.trim().split(/\s+/);
4089
- onFirstNameChange?.(parts[0] ?? "");
4090
- onLastNameChange?.(parts.length > 1 ? parts.slice(1).join(" ") : "");
4091
- }, [onFullNameChange, onFirstNameChange, onLastNameChange]);
4092
4234
  const applyInlineSessionPatch = (0, import_react9.useCallback)(
4093
4235
  (patch, method) => {
4094
4236
  if (!checkout.applyInlineSessionPatch) {
@@ -4169,8 +4311,8 @@ function SplitCardFormInner({
4169
4311
  accountData: {
4170
4312
  userId: effectiveAccount.userId ?? "",
4171
4313
  email: effectiveAccount.email ?? "",
4172
- firstName: effectiveAccount.firstName ?? fullName.trim().split(/\s+/)[0] ?? "",
4173
- lastName: effectiveAccount.lastName ?? fullName.trim().split(/\s+/).slice(1).join(" ") ?? "",
4314
+ firstName: effectiveAccount.firstName ?? "",
4315
+ lastName: effectiveAccount.lastName ?? "",
4174
4316
  ...avsConfig ? (() => {
4175
4317
  const c = selectedCountryRef.current;
4176
4318
  const stateVisible = (0, import_shared5.isAVSFieldVisible)(avsConfig.state, c);
@@ -4218,70 +4360,6 @@ function SplitCardFormInner({
4218
4360
  return;
4219
4361
  }
4220
4362
  const json = await response.json().catch(() => null);
4221
- if (json?.type === "3ds_required") {
4222
- const secret = json["threeDSecureToken"];
4223
- if (!flopay || !secret) {
4224
- setOverlayStatus("error");
4225
- updateError("3DS authentication required but no token provided.");
4226
- return;
4227
- }
4228
- setIs3DSActive(true);
4229
- try {
4230
- const retryBillingAddress = {};
4231
- const retryCC = selectedCountryRef.current;
4232
- const retryCountry = enableAVS ? retryCC : effectiveAccount.country;
4233
- if (retryCountry) retryBillingAddress["country"] = retryCountry;
4234
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.postal_code, retryCC) && zipCodeRef.current.trim()) {
4235
- retryBillingAddress["postal_code"] = zipCodeRef.current.trim();
4236
- } else if (!avsConfig && effectiveAccount.zip) {
4237
- retryBillingAddress["postal_code"] = effectiveAccount.zip;
4238
- }
4239
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.address_line_1, retryCC) && addressLine1Ref.current.trim()) retryBillingAddress["line1"] = addressLine1Ref.current.trim();
4240
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.address_line_2, retryCC) && addressLine2Ref.current.trim()) retryBillingAddress["line2"] = addressLine2Ref.current.trim();
4241
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.city, retryCC) && cityRef.current.trim()) retryBillingAddress["city"] = cityRef.current.trim();
4242
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.state, retryCC) && stateRef.current.trim()) {
4243
- retryBillingAddress["state"] = stateRef.current.trim();
4244
- } else if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.address_line_1, retryCC) && !(0, import_shared5.isAVSFieldVisible)(avsConfig.state, retryCC) && (0, import_shared5.isAVSFieldVisible)(avsConfig.postal_code, retryCC)) {
4245
- const derivedRetryState = (0, import_shared5.getStateFromPostalCode)(retryCC, zipCodeRef.current.trim());
4246
- if (derivedRetryState) retryBillingAddress["state"] = derivedRetryState;
4247
- }
4248
- const result = await flopay.confirmPayment({
4249
- clientSecret: secret,
4250
- returnUrl: window.location.href,
4251
- billingDetails: {
4252
- ...effectiveAccount.email ? { email: effectiveAccount.email } : {},
4253
- ...fullName.trim() ? { name: fullName.trim() } : {},
4254
- ...Object.keys(retryBillingAddress).length > 0 ? { address: retryBillingAddress } : {}
4255
- }
4256
- });
4257
- if (result.error) {
4258
- setOverlayStatus("error");
4259
- updateError(result.error.message);
4260
- onError?.(result.error);
4261
- emitDecline("card", result.error);
4262
- return;
4263
- }
4264
- if (result.status === "succeeded" || result.status === "processing") {
4265
- processingRef.current = false;
4266
- await processPaymentInternal({
4267
- id: result.paymentIntentId,
4268
- type: "card",
4269
- threeDSecureActionResultTokenId: result.paymentIntentId
4270
- }, {
4271
- // Preserve the session/nonce/account overrides resolved
4272
- // above so the retry talks to the same (possibly
4273
- // freshly bootstrapped) session as the original attempt.
4274
- accountPatch: overrides?.accountPatch,
4275
- sessionId: effectiveSessionId,
4276
- nonce: effectiveNonce,
4277
- completionPaymentMethodId: resolvedCompletionPaymentMethodId
4278
- });
4279
- }
4280
- } finally {
4281
- setIs3DSActive(false);
4282
- }
4283
- return;
4284
- }
4285
4363
  if (json?.type === "paypal_redirect_required") {
4286
4364
  const secret = json["threeDSecureToken"];
4287
4365
  const savedPmId = json["paymentMethodId"] || tokenizedBody.id || "";
@@ -4335,6 +4413,8 @@ function SplitCardFormInner({
4335
4413
  emitDecline("paypal", "paypal_direct_required retry limit exceeded");
4336
4414
  return;
4337
4415
  }
4416
+ setDirectPaypalReady(false);
4417
+ setDirectPaypalInitializationState("loading");
4338
4418
  setPaypalDirectRetry({ orderId, attempts: prevAttempts + 1 });
4339
4419
  setOverlayStatus(null);
4340
4420
  return;
@@ -4357,7 +4437,7 @@ function SplitCardFormInner({
4357
4437
  processingRef.current = false;
4358
4438
  }
4359
4439
  },
4360
- [baseUrl, sessionId, nonce, resolvedAccount, fullName, chv, flopay, paypalFlopay, onComplete, onError, updateError, emitDecline]
4440
+ [baseUrl, sessionId, nonce, resolvedAccount, resolvedBillingName, chv, flopay, paypalFlopay, onComplete, onError, updateError, emitDecline]
4361
4441
  );
4362
4442
  const dispatchTokenizedBody = (0, import_react9.useCallback)(
4363
4443
  (tokenizedBody, overrides) => {
@@ -4369,33 +4449,6 @@ function SplitCardFormInner({
4369
4449
  },
4370
4450
  [onTokenizedBody, processPaymentInternal]
4371
4451
  );
4372
- (0, import_react9.useImperativeHandle)(innerRef, () => ({
4373
- async handleNextAction(secret) {
4374
- if (!flopay) return;
4375
- setIs3DSActive(true);
4376
- try {
4377
- const result = await flopay.confirmPayment({
4378
- clientSecret: secret,
4379
- returnUrl: window.location.href
4380
- });
4381
- if (result.error) {
4382
- updateError(result.error.message);
4383
- onError?.(result.error);
4384
- emitDecline("card", result.error);
4385
- } else if (result.status === "succeeded" || result.status === "processing") {
4386
- dispatchTokenizedBody({
4387
- id: result.paymentIntentId,
4388
- type: "card",
4389
- threeDSecureActionResultTokenId: result.paymentIntentId
4390
- });
4391
- }
4392
- } catch (err) {
4393
- updateError(err instanceof Error ? err.message : "Payment authentication failed.");
4394
- } finally {
4395
- setIs3DSActive(false);
4396
- }
4397
- }
4398
- }), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
4399
4452
  const stripeResumeAttemptedRef = (0, import_react9.useRef)(false);
4400
4453
  (0, import_react9.useEffect)(() => {
4401
4454
  if (typeof window === "undefined" || stripeResumeAttemptedRef.current) return;
@@ -4458,7 +4511,7 @@ function SplitCardFormInner({
4458
4511
  return;
4459
4512
  }
4460
4513
  const piStatus = paymentIntent?.status;
4461
- const successful = piStatus === "succeeded" || piStatus === "requires_capture" || piStatus === "processing";
4514
+ const successful = isSuccessfulPaymentIntentStatus(piStatus);
4462
4515
  if (!paymentIntent || !successful) {
4463
4516
  const message = piStatus === "canceled" ? "Payment was canceled." : "Payment was not completed. Please try again.";
4464
4517
  updateError(message);
@@ -4475,165 +4528,10 @@ function SplitCardFormInner({
4475
4528
  }, persistedOverrides);
4476
4529
  })();
4477
4530
  }, [sessionId, dispatchTokenizedBody, flopay, updateError, emitDecline]);
4478
- const handleSubmit = (0, import_react9.useCallback)(
4479
- async (e) => {
4480
- e.preventDefault();
4481
- if (!flopay || !elements || isSubmitting || processingRef.current) return;
4482
- if (vaultActive) return;
4483
- if (layout !== "buttons") {
4484
- onButtonClick?.("card");
4485
- }
4486
- setProcessing(true);
4487
- setOverlayStatus("processing");
4488
- updateError(null);
4489
- let handedOff = false;
4490
- try {
4491
- if (avsConfig) {
4492
- const country = selectedCountryRef.current;
4493
- const postal = computePostalCodeState(
4494
- country,
4495
- zipCodeRef.current,
4496
- (0, import_shared5.isAVSFieldVisible)(avsConfig.postal_code, country)
4497
- );
4498
- if (postal.empty) {
4499
- setZipTouched(true);
4500
- updateError((0, import_shared5.getPostalCodeLabel)(country) + " is required");
4501
- return;
4502
- }
4503
- if (postal.malformed) {
4504
- setZipTouched(true);
4505
- updateError(malformedPostcodeMessage(country));
4506
- return;
4507
- }
4508
- if ((0, import_shared5.isAVSFieldVisible)(avsConfig.address_line_1, country) && !addressLine1Ref.current.trim()) {
4509
- updateError("Street address is required");
4510
- return;
4511
- }
4512
- if ((0, import_shared5.isAVSFieldVisible)(avsConfig.city, country) && !cityRef.current.trim()) {
4513
- updateError("City is required");
4514
- return;
4515
- }
4516
- if ((0, import_shared5.isAVSFieldVisible)(avsConfig.state, country) && !stateRef.current.trim()) {
4517
- updateError((0, import_shared5.getStateLabel)(country) + " is required");
4518
- return;
4519
- }
4520
- }
4521
- if (!vaultActive) {
4522
- const submitResult = await flopay.submitElements();
4523
- if (submitResult.error) {
4524
- updateError(submitResult.error.message);
4525
- onError?.(submitResult.error);
4526
- return;
4527
- }
4528
- }
4529
- const billingAddress = {};
4530
- const cc = selectedCountryRef.current;
4531
- const avsCountry = enableAVS ? cc : resolvedAccount.country;
4532
- if (avsCountry) billingAddress["country"] = avsCountry;
4533
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.postal_code, cc) && zipCodeRef.current.trim()) {
4534
- billingAddress["postal_code"] = zipCodeRef.current.trim();
4535
- } else if (!avsConfig && resolvedAccount.zip) {
4536
- billingAddress["postal_code"] = resolvedAccount.zip;
4537
- }
4538
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.address_line_1, cc) && addressLine1Ref.current.trim()) billingAddress["line1"] = addressLine1Ref.current.trim();
4539
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.address_line_2, cc) && addressLine2Ref.current.trim()) billingAddress["line2"] = addressLine2Ref.current.trim();
4540
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.city, cc) && cityRef.current.trim()) billingAddress["city"] = cityRef.current.trim();
4541
- if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.state, cc) && stateRef.current.trim()) {
4542
- billingAddress["state"] = stateRef.current.trim();
4543
- } else if (avsConfig && (0, import_shared5.isAVSFieldVisible)(avsConfig.address_line_1, cc) && !(0, import_shared5.isAVSFieldVisible)(avsConfig.state, cc) && (0, import_shared5.isAVSFieldVisible)(avsConfig.postal_code, cc)) {
4544
- const derivedSubmitState = (0, import_shared5.getStateFromPostalCode)(cc, zipCodeRef.current.trim());
4545
- if (derivedSubmitState) billingAddress["state"] = derivedSubmitState;
4546
- }
4547
- const billingDetails = {
4548
- ...resolvedAccount.email ? { email: resolvedAccount.email } : {},
4549
- ...fullName.trim() ? { name: fullName.trim() } : {},
4550
- ...Object.keys(billingAddress).length > 0 ? { address: billingAddress } : {}
4551
- };
4552
- if (!sessionId || !resolvedAccount.email) {
4553
- throw new import_shared6.FloPayError("Missing sessionId or email", "validation_error");
4554
- }
4555
- {
4556
- const pmResult = await flopay.createPaymentMethod(billingDetails);
4557
- if (pmResult.error || !pmResult.paymentMethodId) {
4558
- updateError(pmResult.error?.message ?? "Failed to create payment method.");
4559
- return;
4560
- }
4561
- const intentHeaders = { "Content-Type": "application/json" };
4562
- if (nonce) intentHeaders["x-checkout-session-token"] = nonce;
4563
- const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
4564
- method: "POST",
4565
- headers: intentHeaders,
4566
- body: JSON.stringify({
4567
- sessionId,
4568
- email: resolvedAccount.email,
4569
- paymentMethodType: pmResult.paymentMethodId,
4570
- isPaypal: false
4571
- })
4572
- });
4573
- if (!intentResponse.ok) {
4574
- const intentError = await buildFloPayApiErrorFromResponse(
4575
- intentResponse,
4576
- "Failed to create payment intent"
4577
- );
4578
- setOverlayStatus("error");
4579
- updateError(intentError.message);
4580
- onError?.(intentError);
4581
- emitDecline("card", intentError);
4582
- await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
4583
- return;
4584
- }
4585
- const intentJson = await intentResponse.json();
4586
- const intentClientSecret = intentJson.data?.id;
4587
- if (!intentClientSecret) throw new import_shared6.FloPayError("No client_secret in payment intent response", "api_error");
4588
- const confirmResult = await flopay.confirmCardPayment({
4589
- clientSecret: intentClientSecret,
4590
- paymentMethodId: pmResult.paymentMethodId
4591
- });
4592
- if (confirmResult.error) {
4593
- setOverlayStatus("error");
4594
- updateError(confirmResult.error.message);
4595
- onError?.(confirmResult.error);
4596
- emitDecline("card", confirmResult.error);
4597
- await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
4598
- return;
4599
- }
4600
- const rawProvider = typeof flopay.getRawProvider === "function" ? flopay.getRawProvider() : null;
4601
- const confirmedPaymentIntent = await retrievePaymentIntentFromProvider(
4602
- rawProvider,
4603
- intentClientSecret
4604
- );
4605
- const paymentIntentId = confirmResult.paymentIntentId ?? confirmedPaymentIntent?.id;
4606
- const paymentMethodId = confirmResult.paymentMethodId ?? resolvePaymentIntentPaymentMethodId(confirmedPaymentIntent) ?? pmResult.paymentMethodId;
4607
- if (!paymentIntentId) {
4608
- const error2 = new import_shared6.FloPayError("No payment intent returned after confirmation.", "api_error");
4609
- setOverlayStatus("error");
4610
- updateError(error2.message);
4611
- onError?.(error2);
4612
- await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
4613
- return;
4614
- }
4615
- handedOff = isSelfContained;
4616
- dispatchTokenizedBody({
4617
- id: paymentMethodId,
4618
- type: "card",
4619
- threeDSecureActionResultTokenId: paymentIntentId,
4620
- originalPaymentMethodId: pmResult.paymentMethodId
4621
- });
4622
- }
4623
- } catch (err) {
4624
- setOverlayStatus("error");
4625
- updateError(err instanceof Error ? err.message : "An unexpected error occurred");
4626
- await new Promise((r) => setTimeout(r, PROCESSING_OVERLAY_ERROR_DELAY_MS));
4627
- } finally {
4628
- if (!handedOff) {
4629
- setProcessing(false);
4630
- setOverlayStatus(null);
4631
- }
4632
- }
4633
- },
4634
- [flopay, elements, isSubmitting, sessionId, nonce, resolvedAccount.email, baseUrl, isSelfContained, vaultActive, dispatchTokenizedBody, onButtonClick, onError, updateError, emitDecline, layout]
4635
- );
4636
- const isReady = flopay !== null && (vaultActive || elements !== null);
4531
+ const handleSubmit = (0, import_react9.useCallback)((event) => {
4532
+ event.preventDefault();
4533
+ }, []);
4534
+ const isReady = flopay !== null;
4637
4535
  if (!isReady) {
4638
4536
  return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { "data-testid": "flopay-loading", "aria-busy": "true", children: "Loading payment form..." });
4639
4537
  }
@@ -4676,21 +4574,6 @@ function SplitCardFormInner({
4676
4574
  "--flopay-input-font-family": resolvedInputFontFamily,
4677
4575
  "--flopay-input-font-weight": String(resolvedInputFontWeight)
4678
4576
  };
4679
- const stripeElementStyle = {
4680
- style: {
4681
- base: {
4682
- ...sharedInputTypography,
4683
- fontSmoothing: "antialiased",
4684
- "::placeholder": {
4685
- color: resolvedPlaceholderColor,
4686
- fontSize: resolvedInputFontSize,
4687
- fontFamily: resolvedInputFontFamily,
4688
- fontWeight: resolvedInputFontWeight
4689
- }
4690
- },
4691
- invalid: { color: "#ef4444" }
4692
- }
4693
- };
4694
4577
  const containerOverrides = bStyles.cardFormContainer ?? {};
4695
4578
  const containerPadding = containerOverrides.padding ?? (isButtons ? "0" : "1rem");
4696
4579
  const containerRadius = containerOverrides.borderRadius ?? resolvedBorderRadius;
@@ -4703,7 +4586,6 @@ function SplitCardFormInner({
4703
4586
  expectedOrigin: vaultMount.expectedOrigin,
4704
4587
  theme: vaultThemeColors,
4705
4588
  onReady: () => {
4706
- setFormReady(true);
4707
4589
  if (!avsConfig || typeof document === "undefined") return;
4708
4590
  const focusFirstAvs = () => {
4709
4591
  const block = document.querySelector('[data-testid="flopay-avs-fields"]');
@@ -4834,68 +4716,6 @@ function SplitCardFormInner({
4834
4716
  order: vaultActive ? -3 : 0,
4835
4717
  ...bStyles.title
4836
4718
  }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(TitleContentSlot, { content: cardTitleContent }) }),
4837
- !vaultActive && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
4838
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
4839
- backgroundColor: cardInputBg,
4840
- border: `1px solid ${resolvedBorder}`,
4841
- borderTopLeftRadius: resolvedBorderRadius,
4842
- borderTopRightRadius: resolvedBorderRadius,
4843
- padding: "10px"
4844
- }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CardNumberElement, { onReady: () => setFormReady(true), options: stripeElementStyle }) }),
4845
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: { display: "flex" }, children: [
4846
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
4847
- flex: 1,
4848
- backgroundColor: cardInputBg,
4849
- // Longhand only — mixing `border` shorthand with per-side
4850
- // overrides triggers React's "shorthand vs longhand" warning
4851
- // because render order isn't deterministic.
4852
- borderTop: "none",
4853
- borderRight: "none",
4854
- borderBottom: `1px solid ${resolvedBorder}`,
4855
- borderLeft: `1px solid ${resolvedBorder}`,
4856
- borderBottomLeftRadius: resolvedBorderRadius,
4857
- padding: "10px"
4858
- }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CardExpiryElement, { options: stripeElementStyle }) }),
4859
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
4860
- flex: 1,
4861
- backgroundColor: cardInputBg,
4862
- borderTop: "none",
4863
- borderRight: `1px solid ${resolvedBorder}`,
4864
- borderBottom: `1px solid ${resolvedBorder}`,
4865
- borderLeft: `1px solid ${resolvedBorder}`,
4866
- borderBottomRightRadius: resolvedBorderRadius,
4867
- padding: "10px"
4868
- }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CardCvcElement, { options: stripeElementStyle }) })
4869
- ] })
4870
- ] }),
4871
- !vaultActive && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
4872
- backgroundColor: cardInputBg,
4873
- border: `1px solid ${resolvedBorder}`,
4874
- borderRadius: resolvedBorderRadius,
4875
- marginTop: "0.5rem",
4876
- padding: "10px"
4877
- }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4878
- "input",
4879
- {
4880
- className: "flopay-shared-input",
4881
- id: "flopay-cc-name",
4882
- name: "cc-name",
4883
- placeholder: "Full Name on Card",
4884
- autoComplete: "cc-name",
4885
- value: fullName,
4886
- onChange: (e) => handleNameChange(e.target.value),
4887
- disabled: isSubmitting,
4888
- required: true,
4889
- style: {
4890
- width: "100%",
4891
- border: "none",
4892
- outline: "none",
4893
- background: "transparent",
4894
- ...sharedInputTypography,
4895
- ...isButtons && bStyles.nameInput ? bStyles.nameInput : {}
4896
- }
4897
- }
4898
- ) }),
4899
4719
  avsConfig && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { order: vaultActive ? -1 : 0 }, "data-testid": "flopay-avs-fields", children: (() => {
4900
4720
  const cc = selectedCountry;
4901
4721
  const inputWrapStyle = (invalid = false) => ({
@@ -5120,55 +4940,9 @@ function SplitCardFormInner({
5120
4940
  )
5121
4941
  ] });
5122
4942
  })() }),
5123
- vaultActive && cardPreFormSlot && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { order: -2, width: "100%" }, children: cardPreFormSlot }),
5124
- vaultActive && vaultCardFieldsNode,
5125
- displayError && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { role: "alert", "data-testid": "flopay-error", style: {
5126
- margin: "0.75rem 0",
5127
- padding: "0.625rem 0.875rem",
5128
- background: "#FEF2F2",
5129
- border: "1px solid #FECACA",
5130
- borderRadius: "8px",
5131
- color: "#991B1B",
5132
- fontSize: "0.85rem",
5133
- fontWeight: 600,
5134
- display: "flex",
5135
- alignItems: "center",
5136
- gap: "0.5rem",
5137
- ...bStyles.errorBanner ? bStyles.errorBanner : {}
5138
- }, children: [
5139
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 9v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z", stroke: "#DC2626", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }),
5140
- displayError
5141
- ] }),
5142
- !vaultActive && (children ?? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5143
- "button",
5144
- {
5145
- type: "submit",
5146
- disabled: !formReady || isSubmitting,
5147
- "data-testid": "flopay-submit",
5148
- onMouseEnter: (e) => {
5149
- if (formReady && !isSubmitting) e.currentTarget.style.backgroundColor = resolvedPrimaryHoverColor;
5150
- },
5151
- onMouseLeave: (e) => {
5152
- e.currentTarget.style.backgroundColor = bStyles.submitButton?.backgroundColor ?? resolvedPrimaryColor;
5153
- },
5154
- style: {
5155
- width: "100%",
5156
- padding: "0.875rem",
5157
- marginTop: "1rem",
5158
- backgroundColor: resolvedPrimaryColor,
5159
- color: "white",
5160
- border: "none",
5161
- borderRadius: resolvedBorderRadius,
5162
- fontSize: bStyles.submitButtonFontSize ?? "1rem",
5163
- fontWeight: 600,
5164
- cursor: !formReady || isSubmitting ? "not-allowed" : "pointer",
5165
- opacity: !formReady || isSubmitting ? 0.5 : 1,
5166
- transition: "background-color 0.15s",
5167
- ...bStyles.submitButton
5168
- },
5169
- children: isSubmitting ? "PROCESSING..." : submitLabel
5170
- }
5171
- ))
4943
+ vaultActive && cardPreFormSlot && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { order: -2, width: "100%" }, children: cardPreFormSlot }),
4944
+ vaultActive && vaultCardFieldsNode,
4945
+ displayError && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorBanner, { margin: "0.75rem 0", styleOverride: bStyles.errorBanner, children: displayError })
5172
4946
  ] });
5173
4947
  const gateDebugStyle = {
5174
4948
  margin: 0,
@@ -5288,6 +5062,7 @@ function SplitCardFormInner({
5288
5062
  runBeforeButtonClick,
5289
5063
  isProcessing: isSubmitting,
5290
5064
  onLoadStateChange: setDirectPaypalReady,
5065
+ onInitializationStateChange: setDirectPaypalInitializationState,
5291
5066
  session: session ?? null,
5292
5067
  existingOrderId: paypalDirectRetry?.orderId,
5293
5068
  debug
@@ -5320,6 +5095,7 @@ function SplitCardFormInner({
5320
5095
  nonce,
5321
5096
  email: resolvedAccount.email,
5322
5097
  billingApiUrl: resolvedBillingApiUrl,
5098
+ intentKind: totalAmount > 0 ? "payment" : "setup",
5323
5099
  expressMethods: walletExpressMethods,
5324
5100
  onTokenizedBody: dispatchTokenizedBody,
5325
5101
  onErrorChange: updateError,
@@ -5363,7 +5139,7 @@ function SplitCardFormInner({
5363
5139
  }
5364
5140
  }
5365
5141
  ),
5366
- showStripe && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5142
+ vaultActive && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
5367
5143
  "button",
5368
5144
  {
5369
5145
  type: "button",
@@ -5422,27 +5198,11 @@ function SplitCardFormInner({
5422
5198
  children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CardButtonContentSlot, { content: cardButtonContent })
5423
5199
  }
5424
5200
  ),
5425
- displayError && viewState === "buttons" && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { role: "alert", "data-testid": "flopay-error", style: {
5426
- margin: "0.25rem 0",
5427
- padding: "0.625rem 0.875rem",
5428
- background: "#FEF2F2",
5429
- border: "1px solid #FECACA",
5430
- borderRadius: "8px",
5431
- color: "#991B1B",
5432
- fontSize: "0.85rem",
5433
- fontWeight: 600,
5434
- display: "flex",
5435
- alignItems: "center",
5436
- gap: "0.5rem",
5437
- ...bStyles.errorBanner ? bStyles.errorBanner : {}
5438
- }, children: [
5439
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M12 9v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z", stroke: "#DC2626", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }),
5440
- displayError
5441
- ] })
5201
+ displayError && viewState === "buttons" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorBanner, { margin: "0.25rem 0", styleOverride: bStyles.errorBanner, children: displayError })
5442
5202
  ]
5443
5203
  }
5444
5204
  ),
5445
- showStripe && isCardView && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
5205
+ vaultActive && isCardView && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: {
5446
5206
  gridArea: "1 / 1",
5447
5207
  ...cardAnim ? { animation: cardAnim } : {},
5448
5208
  ...viewState === "collapsing" ? { pointerEvents: "none" } : {}
@@ -5686,6 +5446,7 @@ function SplitCardFormInner({
5686
5446
  runBeforeButtonClick,
5687
5447
  isProcessing: isSubmitting,
5688
5448
  onLoadStateChange: setDirectPaypalReady,
5449
+ onInitializationStateChange: setDirectPaypalInitializationState,
5689
5450
  session: session ?? null,
5690
5451
  existingOrderId: paypalDirectRetry?.orderId,
5691
5452
  debug
@@ -5718,6 +5479,7 @@ function SplitCardFormInner({
5718
5479
  nonce,
5719
5480
  email: resolvedAccount.email,
5720
5481
  billingApiUrl: resolvedBillingApiUrl,
5482
+ intentKind: totalAmount > 0 ? "payment" : "setup",
5721
5483
  expressMethods: walletExpressMethods,
5722
5484
  onTokenizedBody: dispatchTokenizedBody,
5723
5485
  onErrorChange: updateError,
@@ -5762,7 +5524,7 @@ function SplitCardFormInner({
5762
5524
  }
5763
5525
  )
5764
5526
  ] }),
5765
- showStripe && (shouldDisplayWalletRow || shouldDisplayPayPalRow || shouldDisplayPaymentElementRow) && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: {
5527
+ vaultActive && (shouldDisplayWalletRow || shouldDisplayPayPalRow || shouldDisplayPaymentElementRow) && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { style: {
5766
5528
  display: "flex",
5767
5529
  alignItems: "center",
5768
5530
  gap: "0.75rem",
@@ -5774,7 +5536,7 @@ function SplitCardFormInner({
5774
5536
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: "or pay with card" }),
5775
5537
  /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { style: { flex: 1, height: 1, backgroundColor: "#ddd" } })
5776
5538
  ] }),
5777
- showStripe && cardFormBlock
5539
+ vaultActive && cardFormBlock
5778
5540
  ] });
5779
5541
  }
5780
5542
 
@@ -5802,6 +5564,9 @@ async function confirmResumeIntent(stripe, clientSecret, data, isSetupIntent) {
5802
5564
  const { paymentIntent, error } = await stripe.confirmCardPayment(clientSecret, data);
5803
5565
  return { error, intent: paymentIntent ?? null };
5804
5566
  }
5567
+ function withCheckoutMethod(error, checkoutMethod) {
5568
+ return Object.assign(error, { checkoutMethod });
5569
+ }
5805
5570
  function getRedirectResultFromCheckoutProcessError(error) {
5806
5571
  if (!error?.type || !error.threeDSecureToken) {
5807
5572
  return null;
@@ -6023,7 +5788,7 @@ function checkoutProcessErrorToFloPayError(error, fallbackMessage = "Payment fai
6023
5788
  const checkoutMethod = options?.checkoutMethod ?? error?.checkoutMethod ?? (error?.type === "paypal_redirect_required" ? "paypal" : "card");
6024
5789
  const rawMessage = error?.message ?? fallbackMessage;
6025
5790
  const message = applyFriendlyMessageOverride(rawMessage) ?? rawMessage;
6026
- return Object.assign(
5791
+ return withCheckoutMethod(
6027
5792
  new import_shared7.FloPayError(
6028
5793
  message,
6029
5794
  "api_error",
@@ -6031,7 +5796,7 @@ function checkoutProcessErrorToFloPayError(error, fallbackMessage = "Payment fai
6031
5796
  code: error?.gatewayErrorCode
6032
5797
  }
6033
5798
  ),
6034
- { checkoutMethod }
5799
+ checkoutMethod
6035
5800
  );
6036
5801
  }
6037
5802
  function resolveSavedPaymentReturnUrl(session) {
@@ -6203,13 +5968,13 @@ async function processSavedPaymentForMode({
6203
5968
  if (recoveredRedirect) {
6204
5969
  return recoveredRedirect;
6205
5970
  }
6206
- throw Object.assign(
5971
+ throw withCheckoutMethod(
6207
5972
  new import_shared7.FloPayError(
6208
5973
  "Your card requires authentication. Please enter your payment details below.",
6209
5974
  "api_error",
6210
5975
  { code: "authentication_required" }
6211
5976
  ),
6212
- { checkoutMethod: "card" }
5977
+ "card"
6213
5978
  );
6214
5979
  }
6215
5980
  throw new import_shared7.FloPayError(
@@ -6237,13 +6002,13 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
6237
6002
  }
6238
6003
  if (redirectResult.type === "3ds_required") {
6239
6004
  if (!attempt3DS || !redirectResult.threeDSecureToken) {
6240
- throw Object.assign(
6005
+ throw withCheckoutMethod(
6241
6006
  new import_shared7.FloPayError(
6242
6007
  "Your card requires authentication. Please enter your payment details below.",
6243
6008
  "api_error",
6244
6009
  { code: "authentication_required" }
6245
6010
  ),
6246
- { checkoutMethod: "card" }
6011
+ "card"
6247
6012
  );
6248
6013
  }
6249
6014
  const isSetupIntent = (0, import_shared7.isSetupIntentClientSecret)(redirectResult.threeDSecureToken);
@@ -6252,13 +6017,13 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
6252
6017
  const retrieved = await retrieveResumeIntent(stripe, redirectResult.threeDSecureToken, isSetupIntent);
6253
6018
  if (retrieved) {
6254
6019
  if (retrieved.error) {
6255
- throw Object.assign(
6020
+ throw withCheckoutMethod(
6256
6021
  new import_shared7.FloPayError(
6257
6022
  retrieved.error.message ?? `Failed to retrieve 3DS ${isSetupIntent ? "setup" : "payment"} status.`,
6258
6023
  "api_error",
6259
6024
  { code: retrieved.error.code }
6260
6025
  ),
6261
- { checkoutMethod: "card" }
6026
+ "card"
6262
6027
  );
6263
6028
  }
6264
6029
  const existingIntent = retrieved.intent;
@@ -6281,13 +6046,13 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
6281
6046
  ) : null;
6282
6047
  if (confirmed) {
6283
6048
  if (confirmed.error) {
6284
- throw Object.assign(
6049
+ throw withCheckoutMethod(
6285
6050
  new import_shared7.FloPayError(
6286
6051
  confirmed.error.message ?? "3DS authentication failed.",
6287
6052
  "api_error",
6288
6053
  { code: confirmed.error.code }
6289
6054
  ),
6290
- { checkoutMethod: "card" }
6055
+ "card"
6291
6056
  );
6292
6057
  }
6293
6058
  paymentIntent = confirmed.intent;
@@ -6296,13 +6061,13 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
6296
6061
  clientSecret: redirectResult.threeDSecureToken
6297
6062
  });
6298
6063
  if (nextAction.error) {
6299
- throw Object.assign(
6064
+ throw withCheckoutMethod(
6300
6065
  new import_shared7.FloPayError(
6301
6066
  nextAction.error.message ?? "3DS authentication failed.",
6302
6067
  "api_error",
6303
6068
  { code: nextAction.error.code }
6304
6069
  ),
6305
- { checkoutMethod: "card" }
6070
+ "card"
6306
6071
  );
6307
6072
  }
6308
6073
  paymentIntent = (isSetupIntent ? nextAction.setupIntent : nextAction.paymentIntent) ?? null;
@@ -6338,17 +6103,17 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
6338
6103
  telemetry
6339
6104
  });
6340
6105
  }
6341
- throw Object.assign(
6106
+ throw withCheckoutMethod(
6342
6107
  new import_shared7.FloPayError("3DS authentication did not complete successfully.", "api_error"),
6343
- { checkoutMethod: "card" }
6108
+ "card"
6344
6109
  );
6345
6110
  }
6346
6111
  if (redirectResult.type === "paypal_redirect_required") {
6347
6112
  const paypalStripe = (paypalFlopay ?? flopay)?.getRawProvider();
6348
6113
  if (!paypalStripe) {
6349
- throw Object.assign(
6114
+ throw withCheckoutMethod(
6350
6115
  new import_shared7.FloPayError("PayPal is not available.", "api_error"),
6351
- { checkoutMethod: "paypal" }
6116
+ "paypal"
6352
6117
  );
6353
6118
  }
6354
6119
  if (redirectResult.paymentMethodId) {
@@ -6356,13 +6121,13 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
6356
6121
  clientSecret: redirectResult.threeDSecureToken
6357
6122
  });
6358
6123
  if (error2) {
6359
- throw Object.assign(
6124
+ throw withCheckoutMethod(
6360
6125
  new import_shared7.FloPayError(
6361
6126
  error2.message ?? "PayPal authorization failed.",
6362
6127
  "api_error",
6363
6128
  { code: error2.code }
6364
6129
  ),
6365
- { checkoutMethod: "paypal" }
6130
+ "paypal"
6366
6131
  );
6367
6132
  }
6368
6133
  return {
@@ -6379,13 +6144,13 @@ async function handleSavedPaymentRedirectResult(redirectResult, {
6379
6144
  redirect: "if_required"
6380
6145
  });
6381
6146
  if (error) {
6382
- throw Object.assign(
6147
+ throw withCheckoutMethod(
6383
6148
  new import_shared7.FloPayError(
6384
6149
  error.message ?? "PayPal authorization failed.",
6385
6150
  "api_error",
6386
6151
  { code: error.code }
6387
6152
  ),
6388
- { checkoutMethod: "paypal" }
6153
+ "paypal"
6389
6154
  );
6390
6155
  }
6391
6156
  return {
@@ -6616,7 +6381,6 @@ function FloPayCheckout({
6616
6381
  onComplete,
6617
6382
  onError,
6618
6383
  onDecline,
6619
- onFullNameChange,
6620
6384
  onCountryChange,
6621
6385
  onZipChange,
6622
6386
  showPayPal = true,
@@ -6638,7 +6402,6 @@ function FloPayCheckout({
6638
6402
  cardFieldOrder,
6639
6403
  cardPreFormSlot,
6640
6404
  avsLayout,
6641
- submitLabel,
6642
6405
  className,
6643
6406
  initialErrorMessage = null,
6644
6407
  children,
@@ -6671,8 +6434,11 @@ function FloPayCheckout({
6671
6434
  const [createSessionPatch, setCreateSessionPatch] = (0, import_react10.useState)(void 0);
6672
6435
  const [createSessionPatchBaseHash, setCreateSessionPatchBaseHash] = (0, import_react10.useState)("");
6673
6436
  const [cardBootstrapPending, setCardBootstrapPending] = (0, import_react10.useState)(false);
6437
+ const [paypalInitializationState, setPaypalInitializationState] = (0, import_react10.useState)("loading");
6438
+ const [paypalManualRetryGeneration, setPaypalManualRetryGeneration] = (0, import_react10.useState)(0);
6674
6439
  const autoCheckoutAttempted = (0, import_react10.useRef)(false);
6675
6440
  const paypalResumeAttempted = (0, import_react10.useRef)(false);
6441
+ const paypalInitializationErrorEmittedRef = (0, import_react10.useRef)(false);
6676
6442
  const savedPaymentKeysRef = (0, import_react10.useRef)(null);
6677
6443
  const telemetryCheckoutContext = (0, import_react10.useMemo)(() => ({
6678
6444
  checkoutMode: checkoutModeProp ?? currentMode,
@@ -6692,6 +6458,28 @@ function FloPayCheckout({
6692
6458
  }, [flopay, paypalFlopay, telemetryCheckoutContext]);
6693
6459
  const onSessionCompletedRef = (0, import_react10.useRef)(onSessionCompleted);
6694
6460
  onSessionCompletedRef.current = onSessionCompleted;
6461
+ const handlePayPalInitializationStateChange = (0, import_react10.useCallback)(
6462
+ (state) => {
6463
+ setPaypalInitializationState(state);
6464
+ if (state !== "exhausted" || paypalInitializationErrorEmittedRef.current) return;
6465
+ paypalInitializationErrorEmittedRef.current = true;
6466
+ onErrorRef.current?.(new import_shared8.FloPayError(
6467
+ "PayPal is temporarily unavailable. Please try again.",
6468
+ "api_error",
6469
+ { code: "paypal_init_timeout" }
6470
+ ));
6471
+ },
6472
+ []
6473
+ );
6474
+ const retryPayPalInitialization = (0, import_react10.useCallback)(() => {
6475
+ setPaypalInitializationState("retrying");
6476
+ setPaypalManualRetryGeneration((generation) => generation + 1);
6477
+ }, []);
6478
+ (0, import_react10.useEffect)(() => {
6479
+ setPaypalInitializationState("loading");
6480
+ setPaypalManualRetryGeneration(0);
6481
+ paypalInitializationErrorEmittedRef.current = false;
6482
+ }, [activeSessionId]);
6695
6483
  (0, import_react10.useEffect)(() => {
6696
6484
  console.info("[FloPay] Checkout initialized", {
6697
6485
  sdk_version: import_shared8.SDK_VERSION,
@@ -7775,42 +7563,41 @@ function FloPayCheckout({
7775
7563
  if (isPaypalOnlySession && session && directPaypalConfig) {
7776
7564
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(import_jsx_runtime8.Fragment, { children: [
7777
7565
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className, style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
7778
- modeError && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
7566
+ modeError && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorBanner, { icon: false, children: modeError }),
7567
+ paypalInitializationState === "retrying" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { role: "status", "aria-live": "polite", children: "PayPal is temporarily unavailable. Retrying\u2026" }),
7568
+ paypalInitializationState === "exhausted" && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { role: "alert", children: [
7569
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { children: "PayPal is temporarily unavailable. Please try again." }),
7570
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", onClick: retryPayPalInitialization, children: "Retry PayPal" })
7571
+ ] }),
7572
+ paypalInitializationState !== "exhausted" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
7779
7573
  "div",
7780
7574
  {
7781
- role: "alert",
7782
- "data-testid": "flopay-error",
7783
- style: {
7784
- padding: "0.625rem 0.875rem",
7785
- background: "#FEF2F2",
7786
- border: "1px solid #FECACA",
7787
- borderRadius: "8px",
7788
- color: "#991B1B",
7789
- fontSize: "0.85rem",
7790
- fontWeight: 600
7791
- },
7792
- children: modeError
7793
- }
7794
- ),
7795
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
7796
- InstrumentedDirectPayPalButton,
7797
- {
7798
- sessionId: activeSessionId,
7799
- nonce: session.clientSecret || void 0,
7800
- billingApiUrl: resolvedBillingUrl,
7801
- email: session.customer?.email,
7802
- clientId: directPaypalConfig.clientId,
7803
- environment: directPaypalConfig.environment,
7804
- currency: (session.currency ?? "usd").toUpperCase(),
7805
- isSubscription: session.mode === "subscription",
7806
- onComplete,
7807
- onErrorChange: setModeError,
7808
- onDecline,
7809
- onButtonClick,
7810
- session,
7811
- telemetry,
7812
- telemetryContext: telemetryCheckoutContext,
7813
- debug
7575
+ "aria-hidden": paypalInitializationState === "retrying" ? true : void 0,
7576
+ style: paypalInitializationState === "retrying" ? { position: "relative", height: 0, opacity: 0, pointerEvents: "none" } : void 0,
7577
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
7578
+ InstrumentedDirectPayPalButton,
7579
+ {
7580
+ sessionId: activeSessionId,
7581
+ nonce: session.clientSecret || void 0,
7582
+ billingApiUrl: resolvedBillingUrl,
7583
+ email: session.customer?.email,
7584
+ clientId: directPaypalConfig.clientId,
7585
+ environment: directPaypalConfig.environment,
7586
+ currency: (session.currency ?? "usd").toUpperCase(),
7587
+ isSubscription: session.mode === "subscription",
7588
+ onComplete,
7589
+ onErrorChange: setModeError,
7590
+ onDecline,
7591
+ onButtonClick,
7592
+ onInitializationStateChange: handlePayPalInitializationStateChange,
7593
+ allowAutomaticRetry: paypalManualRetryGeneration === 0,
7594
+ session,
7595
+ telemetry,
7596
+ telemetryContext: telemetryCheckoutContext,
7597
+ debug
7598
+ },
7599
+ `paypal-init-${paypalManualRetryGeneration}`
7600
+ )
7814
7601
  }
7815
7602
  )
7816
7603
  ] }) }),
@@ -7906,7 +7693,6 @@ function FloPayCheckout({
7906
7693
  onDecline,
7907
7694
  error: modeError,
7908
7695
  onErrorChange: setModeError,
7909
- onFullNameChange,
7910
7696
  showPayPal,
7911
7697
  showStripe,
7912
7698
  enabledPaymentMethods: unified?.data.stripe?.enabledPaymentMethods,
@@ -7935,7 +7721,6 @@ function FloPayCheckout({
7935
7721
  avsCheck: !!enableAVS,
7936
7722
  checkoutType: createSessionParams ? "embedded_checkout" : "standard_checkout",
7937
7723
  checkoutLayout: children ? "custom_layout" : layout === "buttons" ? "buttons_layout" : "default_layout",
7938
- submitLabel,
7939
7724
  className,
7940
7725
  directPaypal: resolveDirectPaypalConfig(unified),
7941
7726
  isSubscription: session?.mode === "subscription",
@@ -7974,10 +7759,6 @@ function SessionInjector({
7974
7759
  }
7975
7760
  function InterimButtonsView({
7976
7761
  onButtonClick,
7977
- onCardButtonClick,
7978
- cardLoading = false,
7979
- cardOpen,
7980
- errorMessage,
7981
7762
  showPayPal,
7982
7763
  showStripe = true,
7983
7764
  showApplePay,
@@ -7990,12 +7771,6 @@ function InterimButtonsView({
7990
7771
  cardTitleContent
7991
7772
  }) {
7992
7773
  const [showCardForm, setShowCardForm] = (0, import_react10.useState)(false);
7993
- const isCardOpenControlled = typeof cardOpen === "boolean";
7994
- (0, import_react10.useEffect)(() => {
7995
- if (isCardOpenControlled) {
7996
- setShowCardForm(cardOpen);
7997
- }
7998
- }, [cardOpen, isCardOpenControlled]);
7999
7774
  const themeBundle = (0, import_react10.useMemo)(() => (0, import_shared8.resolveTheme)(theme), [theme]);
8000
7775
  const bStyles = (0, import_react10.useMemo)(() => {
8001
7776
  const base = themeBundle?.buttonsLayout ?? (0, import_shared8.resolveButtonsLayoutTheme)(buttonsTheme);
@@ -8036,13 +7811,8 @@ function InterimButtonsView({
8036
7811
  "button",
8037
7812
  {
8038
7813
  type: "button",
8039
- onClick: () => {
8040
- if (!isCardOpenControlled) {
8041
- setShowCardForm(false);
8042
- }
8043
- },
7814
+ onClick: () => setShowCardForm(false),
8044
7815
  "aria-label": "Back to payment methods",
8045
- disabled: isCardOpenControlled || cardLoading,
8046
7816
  style: {
8047
7817
  display: "inline-flex",
8048
7818
  alignItems: "center",
@@ -8054,8 +7824,8 @@ function InterimButtonsView({
8054
7824
  fontWeight: 500,
8055
7825
  padding: 0,
8056
7826
  flexShrink: 0,
8057
- opacity: isCardOpenControlled || cardLoading ? 0.6 : 1,
8058
- cursor: isCardOpenControlled || cardLoading ? "not-allowed" : "pointer",
7827
+ opacity: 1,
7828
+ cursor: "pointer",
8059
7829
  ...bStyles.backButton
8060
7830
  },
8061
7831
  children: [
@@ -8135,16 +7905,10 @@ function InterimButtonsView({
8135
7905
  "button",
8136
7906
  {
8137
7907
  type: "button",
8138
- onClick: async () => {
8139
- if (cardLoading) return;
8140
- if (onCardButtonClick) {
8141
- await onCardButtonClick();
8142
- return;
8143
- }
7908
+ onClick: () => {
8144
7909
  onButtonClick?.("card");
8145
7910
  setShowCardForm(true);
8146
7911
  },
8147
- disabled: cardLoading,
8148
7912
  style: {
8149
7913
  width: "100%",
8150
7914
  ...cardButtonSizing,
@@ -8154,7 +7918,7 @@ function InterimButtonsView({
8154
7918
  borderRadius: "8px",
8155
7919
  fontSize: bStyles.cardButtonFontSize ?? "0.95rem",
8156
7920
  fontWeight: 600,
8157
- cursor: cardLoading ? "not-allowed" : "pointer",
7921
+ cursor: "pointer",
8158
7922
  display: "flex",
8159
7923
  alignItems: "center",
8160
7924
  justifyContent: "center",
@@ -8162,7 +7926,7 @@ function InterimButtonsView({
8162
7926
  boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
8163
7927
  transition: "transform 0.1s",
8164
7928
  position: "relative",
8165
- opacity: cardLoading ? 0.6 : 1,
7929
+ opacity: 1,
8166
7930
  ...bStyles.cardButton
8167
7931
  },
8168
7932
  onMouseDown: (e) => {
@@ -8174,375 +7938,75 @@ function InterimButtonsView({
8174
7938
  children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CardButtonContentSlot, { content: cardButtonContent })
8175
7939
  }
8176
7940
  ),
8177
- errorMessage && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { style: {
8178
- margin: "0.25rem 0",
8179
- padding: "0.625rem 0.875rem",
8180
- background: "#FEF2F2",
8181
- border: "1px solid #FECACA",
8182
- borderRadius: "8px",
8183
- color: "#991B1B",
8184
- fontSize: "0.85rem",
8185
- fontWeight: 600,
8186
- display: "flex",
8187
- alignItems: "center",
8188
- gap: "0.5rem",
8189
- ...bStyles.errorBanner
8190
- }, children: [
8191
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M12 9v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z", stroke: "#DC2626", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }),
8192
- errorMessage
8193
- ] }),
8194
7941
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("style", { children: `@keyframes flopay-interim-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }` })
8195
7942
  ] });
8196
7943
  }
8197
7944
 
8198
- // src/checkout-form.tsx
8199
- var import_js5 = require("@flopay/js");
8200
- var import_shared9 = require("@flopay/shared");
7945
+ // src/elements.tsx
8201
7946
  var import_react11 = require("react");
8202
7947
  var import_jsx_runtime9 = require("react/jsx-runtime");
8203
- var WALLET_RESUME_KEY = "flopay_wallet_resume";
8204
- var CheckoutForm = (0, import_react11.forwardRef)(
8205
- function CheckoutForm2(props, ref) {
8206
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CheckoutFormInner, { ...props, innerRef: ref });
8207
- }
8208
- );
8209
- function CheckoutFormInner({
8210
- sessionId,
8211
- nonce,
8212
- billingApiUrl,
8213
- email,
8214
- userId,
8215
- onComplete,
8216
- onError,
8217
- onDecline,
8218
- onTokenizedBody,
8219
- layout = "auto",
8220
- submitLabel = "Pay",
8221
- showAddress = false,
8222
- className,
8223
- children,
8224
- firstName,
8225
- lastName,
8226
- chv,
8227
- isProcessing: externalProcessing,
8228
- error: externalError,
8229
- onErrorChange,
8230
- innerRef
8231
- }) {
8232
- const flopay = useFloPay();
8233
- const paypalFlopay = usePayPalFloPay();
8234
- const elements = useElements();
8235
- const contextBillingUrl = useBillingApiUrl();
8236
- const [processing, setProcessing] = (0, import_react11.useState)(false);
8237
- const [error, setError] = (0, import_react11.useState)(null);
8238
- const [is3DSActive, setIs3DSActive] = (0, import_react11.useState)(false);
8239
- const displayError = externalError ?? error;
8240
- const isSubmitting = externalProcessing ?? processing;
8241
- const isSelfContained = !onTokenizedBody;
8242
- const baseUrl = (billingApiUrl || contextBillingUrl).replace(/\/+$/, "");
8243
- const updateError = (0, import_react11.useCallback)(
8244
- (err) => {
8245
- setError(err);
8246
- onErrorChange?.(err);
8247
- },
8248
- [onErrorChange]
8249
- );
8250
- const emitDecline = (0, import_react11.useCallback)(
8251
- (input, overrides) => {
8252
- onDecline?.(buildDeclineEvent("card", input, overrides));
8253
- },
8254
- [onDecline]
8255
- );
8256
- const processPaymentInternal = (0, import_react11.useCallback)(
8257
- async (tokenizedBody, completionPaymentMethodId) => {
8258
- setProcessing(true);
8259
- updateError(null);
8260
- const resolvedCompletionPaymentMethodId = completionPaymentMethodId ?? resolveTokenizedPaymentMethodId(tokenizedBody);
8261
- const requestTokenizedBody = tokenizedBody.originalPaymentMethodId ? { ...tokenizedBody, originalPaymentMethodId: void 0 } : tokenizedBody;
8262
- try {
8263
- const api = new import_js5.PaymentAPI(baseUrl);
8264
- const response = await api.processPayment(userId ?? "", {
8265
- sessionId,
8266
- nonce,
8267
- tokenizedData: requestTokenizedBody,
8268
- accountData: {
8269
- userId: userId ?? "",
8270
- email: email ?? "",
8271
- firstName: firstName ?? "",
8272
- lastName: lastName ?? ""
8273
- },
8274
- chv
8275
- });
8276
- if (response.ok) {
8277
- onComplete?.({
8278
- status: "succeeded",
8279
- paymentIntentId: tokenizedBody.threeDSecureActionResultTokenId,
8280
- paymentMethodId: resolvedCompletionPaymentMethodId,
8281
- checkoutMethod: tokenizedBody.isPaypal ? "paypal" : "card"
8282
- });
8283
- return;
8284
- }
8285
- const json = await response.json().catch(() => null);
8286
- if (json?.type === "3ds_required") {
8287
- const secret = json["threeDSecureToken"];
8288
- if (!flopay || !secret) {
8289
- updateError("3DS authentication required but no token provided.");
8290
- return;
8291
- }
8292
- setIs3DSActive(true);
8293
- try {
8294
- const retryFullName = `${firstName ?? ""} ${lastName ?? ""}`.trim();
8295
- const result = await flopay.confirmPayment({
8296
- clientSecret: secret,
8297
- returnUrl: window.location.href,
8298
- billingDetails: {
8299
- ...email ? { email } : {},
8300
- ...retryFullName ? { name: retryFullName } : {}
8301
- }
8302
- });
8303
- if (result.error) {
8304
- updateError(result.error.message);
8305
- onError?.(result.error);
8306
- emitDecline(result.error);
8307
- return;
8308
- }
8309
- if (result.status === "succeeded" || result.status === "processing") {
8310
- await processPaymentInternal({
8311
- id: result.paymentIntentId,
8312
- type: "card",
8313
- threeDSecureActionResultTokenId: result.paymentIntentId
8314
- }, resolvedCompletionPaymentMethodId);
8315
- }
8316
- } finally {
8317
- setIs3DSActive(false);
8318
- }
8319
- return;
8320
- }
8321
- if (json?.type === "paypal_redirect_required") {
8322
- const secret = json["threeDSecureToken"] ?? json["clientSecret"];
8323
- const pmId = json["paymentMethodId"];
8324
- const paypalStripe = (paypalFlopay ?? flopay)?.getRawProvider();
8325
- if (!paypalStripe || !secret) {
8326
- const message = "PayPal is not available.";
8327
- updateError(message);
8328
- onDecline?.(buildDeclineEvent("paypal", message));
8329
- return;
8330
- }
8331
- localStorage.setItem(WALLET_RESUME_KEY, JSON.stringify({
8332
- sessionId,
8333
- paymentIntentId: "",
8334
- tokenType: "card",
8335
- tokenId: pmId ?? "",
8336
- status: "pending_redirect"
8337
- }));
8338
- const confirmParams = {
8339
- return_url: window.location.href
8340
- };
8341
- if (pmId) confirmParams["payment_method"] = pmId;
8342
- const { error: confirmError } = await paypalStripe.confirmPayment({
8343
- clientSecret: secret,
8344
- confirmParams,
8345
- redirect: "if_required"
8346
- });
8347
- if (confirmError) {
8348
- const message = confirmError.message ?? "PayPal payment failed.";
8349
- updateError(message);
8350
- onDecline?.(buildDeclineEvent("paypal", message, { code: confirmError.code }));
8351
- }
8352
- return;
8353
- }
8354
- const errorMessage = json?.message ?? "Payment failed. Please try again.";
8355
- updateError(errorMessage);
8356
- emitDecline(errorMessage, {
8357
- code: json?.code,
8358
- declineCode: json?.declineCode ?? json?.gatewayDeclineReason
8359
- });
8360
- } catch (err) {
8361
- updateError(err instanceof Error ? err.message : "An unexpected error occurred");
8362
- } finally {
8363
- setProcessing(false);
8364
- }
8365
- },
8366
- [baseUrl, sessionId, nonce, userId, email, firstName, lastName, chv, flopay, paypalFlopay, onComplete, onError, onDecline, updateError, emitDecline]
8367
- );
8368
- const dispatchTokenizedBody = (0, import_react11.useCallback)(
8369
- (tokenizedBody) => {
8370
- if (onTokenizedBody) {
8371
- onTokenizedBody(tokenizedBody);
8372
- } else {
8373
- processPaymentInternal(tokenizedBody);
8374
- }
8375
- },
8376
- [onTokenizedBody, processPaymentInternal]
8377
- );
8378
- (0, import_react11.useImperativeHandle)(innerRef, () => ({
8379
- async handleNextAction(secret) {
8380
- if (!flopay) return;
8381
- setIs3DSActive(true);
8382
- try {
8383
- const result = await flopay.confirmPayment({
8384
- clientSecret: secret,
8385
- returnUrl: window.location.href
8386
- });
8387
- if (result.error) {
8388
- updateError(result.error.message);
8389
- onError?.(result.error);
8390
- emitDecline(result.error);
8391
- } else if (result.status === "succeeded" || result.status === "processing") {
8392
- dispatchTokenizedBody({
8393
- id: result.paymentIntentId,
8394
- type: "card",
8395
- threeDSecureActionResultTokenId: result.paymentIntentId
8396
- });
8397
- }
8398
- } catch (err) {
8399
- updateError(err instanceof Error ? err.message : "Payment authentication failed.");
8400
- } finally {
8401
- setIs3DSActive(false);
8402
- }
8403
- }
8404
- }), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
8405
- (0, import_react11.useEffect)(() => {
8406
- if (typeof window === "undefined") return;
8407
- const stored = localStorage.getItem(WALLET_RESUME_KEY);
8408
- if (!stored) return;
8409
- try {
8410
- const payload = JSON.parse(stored);
8411
- if (payload.sessionId === sessionId) {
8412
- localStorage.removeItem(WALLET_RESUME_KEY);
8413
- dispatchTokenizedBody({
8414
- id: payload.tokenId,
8415
- type: payload.tokenType,
8416
- threeDSecureActionResultTokenId: payload.paymentIntentId
8417
- });
8418
- }
8419
- } catch {
8420
- localStorage.removeItem(WALLET_RESUME_KEY);
8421
- }
8422
- }, [sessionId, dispatchTokenizedBody]);
8423
- const handleSubmit = (0, import_react11.useCallback)(
8424
- async (e) => {
8425
- e.preventDefault();
8426
- if (!flopay || !elements || isSubmitting) return;
8427
- setProcessing(true);
8428
- updateError(null);
8429
- let handedOff = false;
8430
- try {
8431
- const submitResult = await flopay.submitElements();
8432
- if (submitResult.error) {
8433
- updateError(submitResult.error.message);
8434
- onError?.(submitResult.error);
8435
- return;
8436
- }
8437
- const fullName = `${firstName ?? ""} ${lastName ?? ""}`.trim();
8438
- const pmResult = await flopay.createPaymentMethod({
8439
- ...email ? { email } : {},
8440
- ...fullName ? { name: fullName } : {}
8441
- });
8442
- if (pmResult.error || !pmResult.paymentMethodId) {
8443
- updateError(pmResult.error?.message ?? "Failed to create payment method.");
8444
- return;
8445
- }
8446
- if (!sessionId || !email) {
8447
- throw new import_shared9.FloPayError("Missing sessionId or email", "validation_error");
8448
- }
8449
- const intentHeaders = { "Content-Type": "application/json" };
8450
- if (nonce) intentHeaders["x-checkout-session-token"] = nonce;
8451
- const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
8452
- method: "POST",
8453
- headers: intentHeaders,
8454
- body: JSON.stringify({
8455
- sessionId,
8456
- email,
8457
- paymentMethodType: pmResult.paymentMethodId,
8458
- isPaypal: false
8459
- })
8460
- });
8461
- if (!intentResponse.ok) {
8462
- const intentError = await buildFloPayApiErrorFromResponse(
8463
- intentResponse,
8464
- "Failed to create payment intent"
8465
- );
8466
- updateError(intentError.message);
8467
- onError?.(intentError);
8468
- emitDecline(intentError);
8469
- return;
8470
- }
8471
- const intentJson = await intentResponse.json();
8472
- const intentClientSecret = intentJson.data?.id;
8473
- if (!intentClientSecret) throw new import_shared9.FloPayError("No client_secret in payment intent response", "api_error");
8474
- const confirmResult = await flopay.confirmCardPayment({
8475
- clientSecret: intentClientSecret,
8476
- paymentMethodId: pmResult.paymentMethodId
8477
- });
8478
- if (confirmResult.error) {
8479
- updateError(confirmResult.error.message);
8480
- onError?.(confirmResult.error);
8481
- emitDecline(confirmResult.error);
8482
- return;
7948
+ function createElementComponent(elementType, displayName) {
7949
+ function ElementComponent({
7950
+ className,
7951
+ id,
7952
+ style,
7953
+ options,
7954
+ onChange,
7955
+ onReady,
7956
+ onFocus,
7957
+ onBlur,
7958
+ onEscape
7959
+ }) {
7960
+ const containerRef = (0, import_react11.useRef)(null);
7961
+ const elementRef = (0, import_react11.useRef)(null);
7962
+ const { elements, reportInteractive } = (0, import_react11.useContext)(FloPayContext);
7963
+ (0, import_react11.useEffect)(() => {
7964
+ if (!elements || !containerRef.current) return;
7965
+ let mounted = true;
7966
+ (async () => {
7967
+ let element = elements.getElement(elementType);
7968
+ if (!element) {
7969
+ element = await elements.create(elementType, options);
8483
7970
  }
8484
- const rawProvider = typeof flopay.getRawProvider === "function" ? flopay.getRawProvider() : null;
8485
- const confirmedPaymentIntent = await retrievePaymentIntentFromProvider(
8486
- rawProvider,
8487
- intentClientSecret
8488
- );
8489
- const paymentIntentId = confirmResult.paymentIntentId ?? confirmedPaymentIntent?.id;
8490
- const paymentMethodId = confirmResult.paymentMethodId ?? resolvePaymentIntentPaymentMethodId(confirmedPaymentIntent) ?? pmResult.paymentMethodId;
8491
- if (!paymentIntentId) {
8492
- const error2 = new import_shared9.FloPayError("No payment intent returned after confirmation.", "api_error");
8493
- updateError(error2.message);
8494
- onError?.(error2);
7971
+ if (!mounted || !containerRef.current) {
8495
7972
  return;
8496
7973
  }
8497
- handedOff = isSelfContained;
8498
- dispatchTokenizedBody({
8499
- id: paymentMethodId,
8500
- type: "card",
8501
- threeDSecureActionResultTokenId: paymentIntentId,
8502
- originalPaymentMethodId: pmResult.paymentMethodId
7974
+ element.mount(containerRef.current);
7975
+ elementRef.current = element;
7976
+ if (onChange) element.on("change", onChange);
7977
+ element.on("ready", () => {
7978
+ reportInteractive?.();
7979
+ onReady?.();
8503
7980
  });
8504
- } catch (err) {
8505
- updateError(err instanceof Error ? err.message : "An unexpected error occurred");
8506
- } finally {
8507
- if (!handedOff) {
8508
- setProcessing(false);
8509
- }
8510
- }
8511
- },
8512
- [flopay, elements, isSubmitting, sessionId, nonce, email, firstName, lastName, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError, emitDecline]
8513
- );
8514
- const isReady = flopay !== null && elements !== null;
8515
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("form", { onSubmit: handleSubmit, className, style: { position: "relative" }, children: [
8516
- (is3DSActive || isSubmitting) && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { "data-testid": "flopay-overlay", style: {
8517
- position: "absolute",
8518
- inset: 0,
8519
- background: "rgba(255,255,255,0.7)",
8520
- display: "flex",
8521
- alignItems: "center",
8522
- justifyContent: "center",
8523
- zIndex: 10
8524
- }, children: is3DSActive ? "Verifying payment..." : "Processing..." }),
8525
- !isReady && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { "data-testid": "flopay-loading", "aria-busy": "true", children: "Loading payment form..." }),
8526
- isReady && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(import_jsx_runtime9.Fragment, { children: [
8527
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PaymentElement, { options: { layout } }),
8528
- showAddress && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(AddressElement, { options: { mode: showAddress === true ? "billing" : showAddress } }),
8529
- displayError && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { role: "alert", "data-testid": "flopay-error", style: { color: "red", margin: "0.75rem 0" }, children: displayError }),
8530
- children ?? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
8531
- "button",
8532
- {
8533
- type: "submit",
8534
- disabled: isSubmitting || !isReady,
8535
- "data-testid": "flopay-submit",
8536
- children: isSubmitting ? "Processing..." : submitLabel
7981
+ if (onFocus) element.on("focus", onFocus);
7982
+ if (onBlur) element.on("blur", onBlur);
7983
+ if (onEscape) element.on("escape", onEscape);
7984
+ })();
7985
+ return () => {
7986
+ mounted = false;
7987
+ if (elementRef.current) {
7988
+ try {
7989
+ elementRef.current.unmount();
7990
+ } catch {
7991
+ }
7992
+ elementRef.current = null;
8537
7993
  }
8538
- )
8539
- ] })
8540
- ] });
7994
+ };
7995
+ }, [elements, reportInteractive]);
7996
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { ref: containerRef, className, id, style });
7997
+ }
7998
+ ElementComponent.displayName = displayName;
7999
+ return ElementComponent;
8541
8000
  }
8001
+ var PaymentElement2 = createElementComponent(
8002
+ "payment",
8003
+ "PaymentElement"
8004
+ );
8005
+ var AddressElement = createElementComponent("address", "AddressElement");
8542
8006
 
8543
8007
  // src/paypal-button.tsx
8544
- var import_js6 = require("@flopay/js");
8545
- var import_shared10 = require("@flopay/shared");
8008
+ var import_js5 = require("@flopay/js");
8009
+ var import_shared9 = require("@flopay/shared");
8546
8010
  var import_react12 = require("react");
8547
8011
  var import_jsx_runtime10 = require("react/jsx-runtime");
8548
8012
  function PayPalButton({
@@ -8569,7 +8033,7 @@ function PayPalButton({
8569
8033
  const processPaymentInternal = (0, import_react12.useCallback)(
8570
8034
  async (tokenizedBody) => {
8571
8035
  try {
8572
- const api = new import_js6.PaymentAPI(baseUrl);
8036
+ const api = new import_js5.PaymentAPI(baseUrl);
8573
8037
  const response = await api.processPayment(userId ?? "", {
8574
8038
  sessionId,
8575
8039
  nonce,
@@ -8658,7 +8122,7 @@ function PayPalButton({
8658
8122
  setSubmitting(true);
8659
8123
  onErrorChange?.(null);
8660
8124
  if (!sessionId || !email) {
8661
- throw new import_shared10.FloPayError("Missing sessionId or email for PayPal payment", "validation_error");
8125
+ throw new import_shared9.FloPayError("Missing sessionId or email for PayPal payment", "validation_error");
8662
8126
  }
8663
8127
  const result = await flopay.confirmPayPalPayment({
8664
8128
  billingApiUrl: baseUrl,
@@ -8733,18 +8197,18 @@ function PayPalButton({
8733
8197
 
8734
8198
  // src/automatic-payment-button.tsx
8735
8199
  var import_react13 = require("react");
8736
- var import_js7 = require("@flopay/js");
8737
- var import_shared11 = require("@flopay/shared");
8200
+ var import_js6 = require("@flopay/js");
8201
+ var import_shared10 = require("@flopay/shared");
8738
8202
  var import_jsx_runtime11 = require("react/jsx-runtime");
8739
8203
  var DEFAULT_BUTTONS_LAYOUT_BUTTON_HEIGHT3 = 44;
8740
8204
  function sleep2(ms) {
8741
8205
  return new Promise((resolve) => setTimeout(resolve, ms));
8742
8206
  }
8743
8207
  function coerceError(err, fallbackMessage) {
8744
- if (err instanceof import_shared11.FloPayError) {
8208
+ if (err instanceof import_shared10.FloPayError) {
8745
8209
  return err;
8746
8210
  }
8747
- return new import_shared11.FloPayError(
8211
+ return new import_shared10.FloPayError(
8748
8212
  err instanceof Error ? err.message : fallbackMessage,
8749
8213
  "api_error"
8750
8214
  );
@@ -8816,7 +8280,7 @@ function FloPayAutomaticPaymentButton({
8816
8280
  ...buttonProps
8817
8281
  }) {
8818
8282
  const resolvedBillingUrl = (0, import_react13.useMemo)(
8819
- () => (0, import_shared11.resolveBillingApiUrl)(billingApiUrl),
8283
+ () => (0, import_shared10.resolveBillingApiUrl)(billingApiUrl),
8820
8284
  [billingApiUrl]
8821
8285
  );
8822
8286
  const createSessionDraft = (0, import_react13.useMemo)(
@@ -8917,7 +8381,7 @@ function FloPayAutomaticPaymentButton({
8917
8381
  const processResolvedSession = (0, import_react13.useCallback)(async (apiResult, resolvedSessionId, options) => {
8918
8382
  const session = apiResult.data.session ?? null;
8919
8383
  if (!session) {
8920
- throw new import_shared11.FloPayError("No session data returned", "api_error");
8384
+ throw new import_shared10.FloPayError("No session data returned", "api_error");
8921
8385
  }
8922
8386
  const effectiveNonce = session.clientSecret || nonce;
8923
8387
  if (session.status === "complete") {
@@ -8930,19 +8394,19 @@ function FloPayAutomaticPaymentButton({
8930
8394
  return;
8931
8395
  }
8932
8396
  if (session.status === "expired") {
8933
- throw new import_shared11.FloPayError("Checkout session has expired.", "api_error", {
8397
+ throw new import_shared10.FloPayError("Checkout session has expired.", "api_error", {
8934
8398
  code: "checkout_session_expired"
8935
8399
  });
8936
8400
  }
8937
8401
  try {
8938
8402
  if (apiResult.autoProcessingPending) {
8939
- const api = new import_js7.PaymentAPI(resolvedBillingUrl);
8403
+ const api = new import_js6.PaymentAPI(resolvedBillingUrl);
8940
8404
  const completed = await api.waitForCheckoutSessionCompletion(apiResult.autoProcessingPending.sessionId, {
8941
8405
  initialDelayMs: apiResult.autoProcessingPending.retryAfterMs
8942
8406
  });
8943
8407
  const completedSession = completed.data.session;
8944
8408
  if (!completedSession || completedSession.status !== "complete") {
8945
- throw new import_shared11.FloPayError("Automatic payment failed. Please try again.", "api_error", {
8409
+ throw new import_shared10.FloPayError("Automatic payment failed. Please try again.", "api_error", {
8946
8410
  code: completedSession?.status === "expired" ? "checkout_session_expired" : "checkout_processing_timeout"
8947
8411
  });
8948
8412
  }
@@ -8985,7 +8449,7 @@ function FloPayAutomaticPaymentButton({
8985
8449
  }
8986
8450
  );
8987
8451
  }
8988
- const api = new import_js7.PaymentAPI(resolvedBillingUrl);
8452
+ const api = new import_js6.PaymentAPI(resolvedBillingUrl);
8989
8453
  const completed = await api.waitForCheckoutSessionCompletion(resolvedSessionId, {
8990
8454
  initialDelayMs: 0,
8991
8455
  nonce: effectiveNonce
@@ -9077,7 +8541,7 @@ function FloPayAutomaticPaymentButton({
9077
8541
  }
9078
8542
  setFallbackSession(null);
9079
8543
  if (sessionId && createSessionDraft) {
9080
- const error = new import_shared11.FloPayError(
8544
+ const error = new import_shared10.FloPayError(
9081
8545
  "Provide either sessionId or create-session props, not both.",
9082
8546
  "validation_error"
9083
8547
  );
@@ -9089,7 +8553,7 @@ function FloPayAutomaticPaymentButton({
9089
8553
  return;
9090
8554
  }
9091
8555
  if (!sessionId && !createSessionDraft) {
9092
- const error = new import_shared11.FloPayError(
8556
+ const error = new import_shared10.FloPayError(
9093
8557
  "Provide a sessionId or the props required to create an automatic payment session.",
9094
8558
  "validation_error"
9095
8559
  );
@@ -9104,7 +8568,7 @@ function FloPayAutomaticPaymentButton({
9104
8568
  setOverlayError(null);
9105
8569
  setOverlayStatus("processing");
9106
8570
  try {
9107
- const api = new import_js7.PaymentAPI(resolvedBillingUrl);
8571
+ const api = new import_js6.PaymentAPI(resolvedBillingUrl);
9108
8572
  if (sessionId) {
9109
8573
  const result = await api.getUnifiedCheckoutSession(sessionId, nonce);
9110
8574
  await processResolvedSession(result, sessionId);
@@ -9118,7 +8582,7 @@ function FloPayAutomaticPaymentButton({
9118
8582
  fromCreateSession: true
9119
8583
  });
9120
8584
  } catch (err) {
9121
- if (err instanceof import_shared11.FloPayError && err.code === "session_auto_completed") {
8585
+ if (err instanceof import_shared10.FloPayError && err.code === "session_auto_completed") {
9122
8586
  await showSuccess({
9123
8587
  result: { status: "succeeded" },
9124
8588
  session: null,
@@ -9168,9 +8632,9 @@ function FloPayAutomaticPaymentButton({
9168
8632
  const handleFallbackDecline = (0, import_react13.useCallback)((decline) => {
9169
8633
  onDeclineRef.current?.(decline);
9170
8634
  }, []);
9171
- const themeBundle = (0, import_react13.useMemo)(() => (0, import_shared11.resolveTheme)(theme), [theme]);
8635
+ const themeBundle = (0, import_react13.useMemo)(() => (0, import_shared10.resolveTheme)(theme), [theme]);
9172
8636
  const bStyles = (0, import_react13.useMemo)(() => {
9173
- const base = themeBundle?.buttonsLayout ?? (0, import_shared11.resolveButtonsLayoutTheme)(buttonsTheme);
8637
+ const base = themeBundle?.buttonsLayout ?? (0, import_shared10.resolveButtonsLayoutTheme)(buttonsTheme);
9174
8638
  if (!stylesOverride) return base;
9175
8639
  return {
9176
8640
  ...base,
@@ -9336,11 +8800,6 @@ function FloPayAutomaticPaymentButton({
9336
8800
  // Annotate the CommonJS export names for ESM import in node:
9337
8801
  0 && (module.exports = {
9338
8802
  AddressElement,
9339
- CardCvcElement,
9340
- CardElement,
9341
- CardExpiryElement,
9342
- CardNumberElement,
9343
- CheckoutForm,
9344
8803
  DirectPayPalButton,
9345
8804
  FloPayAutomaticPaymentButton,
9346
8805
  FloPayCheckout,