@flopay/react 0.3.11 → 0.3.12

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
@@ -118,7 +118,7 @@ function FloPayProvider({
118
118
  // src/flopay-checkout.tsx
119
119
  var import_react7 = __toESM(require("react"), 1);
120
120
  var import_js = require("@flopay/js");
121
- var import_shared5 = require("@flopay/shared");
121
+ var import_shared6 = require("@flopay/shared");
122
122
 
123
123
  // src/card-button-content.tsx
124
124
  var import_react3 = require("react");
@@ -249,7 +249,7 @@ var AddressElement = createElementComponent("address", "AddressElement");
249
249
 
250
250
  // src/split-card-form.tsx
251
251
  var import_react_stripe_js = require("@stripe/react-stripe-js");
252
- var import_shared3 = require("@flopay/shared");
252
+ var import_shared4 = require("@flopay/shared");
253
253
  var import_react6 = require("react");
254
254
 
255
255
  // src/hooks.ts
@@ -271,8 +271,116 @@ function useBillingApiUrl() {
271
271
  return ctx.billingApiUrl || (0, import_shared2.resolveBillingApiUrl)();
272
272
  }
273
273
 
274
+ // src/checkout-utils.ts
275
+ var import_shared3 = require("@flopay/shared");
276
+ function mergeInlineSessionPatches(base, patch) {
277
+ if (!patch) return base;
278
+ if (!base) return patch;
279
+ return {
280
+ account: { ...base.account ?? {}, ...patch.account ?? {} },
281
+ couponCodes: patch.couponCodes ?? base.couponCodes,
282
+ tagsData: {
283
+ ...base.tagsData ?? {},
284
+ ...patch.tagsData ?? {}
285
+ },
286
+ utmMetadata: patch.utmMetadata ?? base.utmMetadata
287
+ };
288
+ }
289
+ function mergeInlineSessionPatch(params, patch) {
290
+ if (!patch) return params;
291
+ return {
292
+ ...params,
293
+ account: {
294
+ ...params.account,
295
+ ...patch.account ?? {}
296
+ },
297
+ couponCodes: patch.couponCodes ?? params.couponCodes,
298
+ tagsData: {
299
+ ...params.tagsData ?? {},
300
+ ...patch.tagsData ?? {}
301
+ },
302
+ utmMetadata: patch.utmMetadata ?? params.utmMetadata
303
+ };
304
+ }
305
+ function buildSyntheticSession(params, checkoutModeOverride) {
306
+ const totalAmount = [
307
+ ...(params.items ?? []).map((item) => item.overrideAmount ?? item.totalAmount ?? 0),
308
+ ...(params.subscriptions ?? []).map((subscription) => subscription.overrideAmount ?? subscription.totalAmount ?? 0)
309
+ ].reduce((sum, value) => sum + value, 0);
310
+ const currency = params.items?.[0]?.currency ?? params.subscriptions?.[0]?.currency ?? "USD";
311
+ return {
312
+ id: "",
313
+ clientSecret: "",
314
+ mode: "payment",
315
+ amount: Math.round(totalAmount * 100),
316
+ currency,
317
+ status: "open",
318
+ customer: {
319
+ id: params.account.userId,
320
+ email: params.account.email ?? "",
321
+ firstName: params.account.firstName,
322
+ lastName: params.account.lastName,
323
+ gender: params.account.gender ?? void 0,
324
+ city: params.account.city ?? void 0,
325
+ state: params.account.state ?? void 0,
326
+ country: params.account.country ?? void 0,
327
+ zip: params.account.zip ?? void 0
328
+ },
329
+ successUrl: params.successUrl,
330
+ cancelUrl: params.cancelUrl,
331
+ checkoutMode: params.checkoutMode ?? checkoutModeOverride ?? "full",
332
+ items: (params.items ?? []).map((item, idx) => ({
333
+ uuid: `synthetic-item-${idx}`,
334
+ checkoutSessionId: "",
335
+ providerItemId: item.providerItemId,
336
+ providerItemName: item.providerItemName ?? item.providerItemId,
337
+ quantity: item.quantity ?? 1,
338
+ totalAmount: item.totalAmount,
339
+ overrideAmount: item.overrideAmount ?? null,
340
+ currency: item.currency ?? currency
341
+ })),
342
+ subscriptions: (params.subscriptions ?? []).map((subscription, idx) => ({
343
+ uuid: `synthetic-sub-${idx}`,
344
+ checkoutSessionId: "",
345
+ providerPlanId: subscription.providerPlanId,
346
+ providerPlanName: subscription.providerPlanName ?? subscription.providerPlanId,
347
+ quantity: subscription.quantity ?? 1,
348
+ totalAmount: subscription.totalAmount,
349
+ overrideAmount: subscription.overrideAmount ?? null,
350
+ currency: subscription.currency ?? currency
351
+ }))
352
+ };
353
+ }
354
+ function ensureInlineSessionReady(params) {
355
+ if (!params.account.email?.trim()) {
356
+ throw new import_shared3.FloPayError(
357
+ "Email is required before continuing with card checkout.",
358
+ "validation_error",
359
+ { param: "createSession.account.email" }
360
+ );
361
+ }
362
+ }
363
+ function mergeAccountPatch(base, patch) {
364
+ if (!patch) return base;
365
+ return {
366
+ ...base,
367
+ ...patch
368
+ };
369
+ }
370
+ function buildDeclineEvent(method, input, overrides) {
371
+ const message = typeof input === "string" ? input : input.message;
372
+ const code = typeof input === "string" ? overrides?.code : overrides?.code ?? input.code;
373
+ const declineCode = typeof input === "string" ? overrides?.declineCode : overrides?.declineCode ?? input.declineCode;
374
+ return {
375
+ method,
376
+ message,
377
+ ...code ? { code } : {},
378
+ ...declineCode ? { declineCode } : {}
379
+ };
380
+ }
381
+
274
382
  // src/split-card-form.tsx
275
- var import_shared4 = require("@flopay/shared");
383
+ var import_shared5 = require("@flopay/shared");
276
384
  var import_jsx_runtime4 = require("react/jsx-runtime");
277
385
  var WALLET_RESUME_KEY = "flopay_wallet_resume";
278
386
  var FLOPAY_KEYFRAMES = `
@@ -410,7 +518,8 @@ function PayPalButtonInner({
410
518
  onTokenizedBody,
411
519
  onErrorChange,
412
520
  isProcessing = false,
413
- onButtonClick
521
+ onButtonClick,
522
+ onDecline
414
523
  }) {
415
524
  const stripe = (0, import_react_stripe_js.useStripe)();
416
525
  const elements = (0, import_react_stripe_js.useElements)();
@@ -430,7 +539,9 @@ function PayPalButtonInner({
430
539
  try {
431
540
  setSubmitting(true);
432
541
  if (redirectStatus === "failed") {
433
- onErrorChange?.("PayPal payment was declined. Please try again.");
542
+ const message = "PayPal payment was declined. Please try again.";
543
+ onErrorChange?.(message);
544
+ onDecline?.(buildDeclineEvent("paypal", message));
434
545
  return;
435
546
  }
436
547
  const { paymentIntent, error } = await stripe.retrievePaymentIntent(clientSecret);
@@ -452,7 +563,9 @@ function PayPalButtonInner({
452
563
  url.searchParams.delete("redirect_status");
453
564
  window.history.replaceState({}, "", url.toString());
454
565
  } else {
455
- onErrorChange?.("PayPal payment was not completed. Please try again.");
566
+ const message = "PayPal payment was not completed. Please try again.";
567
+ onErrorChange?.(message);
568
+ onDecline?.(buildDeclineEvent("paypal", message));
456
569
  }
457
570
  } catch (err) {
458
571
  onErrorChange?.(err instanceof Error ? err.message : "Failed to complete PayPal payment.");
@@ -460,7 +573,7 @@ function PayPalButtonInner({
460
573
  setSubmitting(false);
461
574
  }
462
575
  })();
463
- }, [stripe, onTokenizedBody, onErrorChange]);
576
+ }, [stripe, onTokenizedBody, onErrorChange, onDecline]);
464
577
  const handlePayPalConfirm = (0, import_react6.useCallback)(async (_event) => {
465
578
  if (!stripe || !elements) return;
466
579
  onButtonClick?.("paypal");
@@ -501,7 +614,11 @@ function PayPalButtonInner({
501
614
  redirect: "if_required"
502
615
  });
503
616
  if (confirmError) {
504
- onErrorChange?.(confirmError.message ?? "PayPal payment failed.");
617
+ const message = confirmError.message ?? "PayPal payment failed.";
618
+ onErrorChange?.(message);
619
+ onDecline?.(buildDeclineEvent("paypal", message, {
620
+ code: confirmError.code
621
+ }));
505
622
  return;
506
623
  }
507
624
  const confirmedPmId = typeof paymentIntent?.payment_method === "string" ? paymentIntent.payment_method : paymentIntent?.payment_method?.id;
@@ -516,7 +633,7 @@ function PayPalButtonInner({
516
633
  } finally {
517
634
  setSubmitting(false);
518
635
  }
519
- }, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]);
636
+ }, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline]);
520
637
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
521
638
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
522
639
  import_react_stripe_js.ExpressCheckoutElement,
@@ -525,6 +642,9 @@ function PayPalButtonInner({
525
642
  onLoadError: () => {
526
643
  },
527
644
  onConfirm: handlePayPalConfirm,
645
+ onCancel: () => {
646
+ onDecline?.(buildDeclineEvent("paypal", "PayPal checkout was cancelled."));
647
+ },
528
648
  options: {
529
649
  buttonType: { paypal: "paypal" },
530
650
  paymentMethods: {
@@ -547,13 +667,15 @@ function WalletButtonInner({
547
667
  showGooglePay = true,
548
668
  onTokenizedBody,
549
669
  onErrorChange,
550
- onButtonClick
670
+ onButtonClick,
671
+ onDecline
551
672
  }) {
552
673
  const stripe = (0, import_react_stripe_js.useStripe)();
553
674
  const elements = (0, import_react_stripe_js.useElements)();
554
675
  const [ready, setReady] = (0, import_react6.useState)(false);
555
676
  const [submitting, setSubmitting] = (0, import_react6.useState)(false);
556
677
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
678
+ const lastWalletMethodRef = (0, import_react6.useRef)("card");
557
679
  const handleWalletConfirm = (0, import_react6.useCallback)(
558
680
  async (_event) => {
559
681
  if (!stripe || !elements) return;
@@ -594,7 +716,12 @@ function WalletButtonInner({
594
716
  { payment_method: paymentMethod.id }
595
717
  );
596
718
  if (confirmError) {
597
- onErrorChange?.(confirmError.message ?? "Wallet payment failed.");
719
+ const method = walletType === "apple_pay" ? "apple_pay" : "google_pay";
720
+ const message = confirmError.message ?? "Wallet payment failed.";
721
+ onErrorChange?.(message);
722
+ onDecline?.(buildDeclineEvent(method, message, {
723
+ code: confirmError.code
724
+ }));
598
725
  return;
599
726
  }
600
727
  onTokenizedBody({
@@ -608,7 +735,7 @@ function WalletButtonInner({
608
735
  setSubmitting(false);
609
736
  }
610
737
  },
611
- [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]
738
+ [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline]
612
739
  );
613
740
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
614
741
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -617,7 +744,14 @@ function WalletButtonInner({
617
744
  onReady: () => setReady(true),
618
745
  onLoadError: () => {
619
746
  },
747
+ onClick: (event) => {
748
+ lastWalletMethodRef.current = event.expressPaymentType === "apple_pay" ? "apple_pay" : "google_pay";
749
+ event.resolve();
750
+ },
620
751
  onConfirm: handleWalletConfirm,
752
+ onCancel: () => {
753
+ onDecline?.(buildDeclineEvent(lastWalletMethodRef.current, "Wallet checkout was cancelled."));
754
+ },
621
755
  options: {
622
756
  buttonType: { applePay: "plain", googlePay: "plain" },
623
757
  paymentMethods: {
@@ -642,6 +776,7 @@ function SplitCardFormInner({
642
776
  userId,
643
777
  onComplete,
644
778
  onError,
779
+ onDecline,
645
780
  onTokenizedBody,
646
781
  firstName,
647
782
  lastName,
@@ -664,6 +799,7 @@ function SplitCardFormInner({
664
799
  cardBackButtonContent,
665
800
  cardTitleContent,
666
801
  onButtonClick,
802
+ onBeforeButtonClick,
667
803
  enableAVS = false,
668
804
  avsLayout: avsLayoutProp = "row",
669
805
  country: countryProp,
@@ -672,6 +808,7 @@ function SplitCardFormInner({
672
808
  onZipChange,
673
809
  totalAmount = 0,
674
810
  currency = "usd",
811
+ initialCardOpen = false,
675
812
  innerRef
676
813
  }) {
677
814
  const flopay = useFloPay();
@@ -682,9 +819,10 @@ function SplitCardFormInner({
682
819
  const [is3DSActive, setIs3DSActive] = (0, import_react6.useState)(false);
683
820
  const [selectedCountry, setSelectedCountry] = (0, import_react6.useState)(countryProp ?? "US");
684
821
  const [zipCode, setZipCode] = (0, import_react6.useState)(zipProp ?? "");
822
+ const [accountPatch, setAccountPatch] = (0, import_react6.useState)({});
685
823
  const zipCodeRef = (0, import_react6.useRef)(zipProp ?? "");
686
824
  const selectedCountryRef = (0, import_react6.useRef)(countryProp ?? "US");
687
- const [viewState, setViewState] = (0, import_react6.useState)("buttons");
825
+ const [viewState, setViewState] = (0, import_react6.useState)(initialCardOpen ? "card" : "buttons");
688
826
  const showCardForm = viewState === "expanding" || viewState === "card";
689
827
  const TRANSITION_MS = 280;
690
828
  const expandToCard = (0, import_react6.useCallback)(() => {
@@ -695,6 +833,11 @@ function SplitCardFormInner({
695
833
  setViewState("collapsing");
696
834
  setTimeout(() => setViewState("buttons"), TRANSITION_MS);
697
835
  }, []);
836
+ (0, import_react6.useEffect)(() => {
837
+ if (layout === "buttons" && initialCardOpen) {
838
+ setViewState("card");
839
+ }
840
+ }, [layout, initialCardOpen]);
698
841
  const [fullName, setFullName] = (0, import_react6.useState)("");
699
842
  const [formReady, setFormReady] = (0, import_react6.useState)(false);
700
843
  const [overlayStatus, setOverlayStatus] = (0, import_react6.useState)(null);
@@ -702,7 +845,7 @@ function SplitCardFormInner({
702
845
  const resolvedBillingApiUrl = billingApiUrl || contextBillingUrl;
703
846
  const displayError = externalError ?? error;
704
847
  const bStyles = (0, import_react6.useMemo)(() => {
705
- const base = (0, import_shared3.resolveButtonsLayoutTheme)(buttonsTheme);
848
+ const base = (0, import_shared4.resolveButtonsLayoutTheme)(buttonsTheme);
706
849
  if (!buttonsStylesOverride) return base;
707
850
  return {
708
851
  ...base,
@@ -718,6 +861,14 @@ function SplitCardFormInner({
718
861
  const isSubmitting = externalProcessing ?? processing;
719
862
  const isSelfContained = !onTokenizedBody;
720
863
  const baseUrl = resolvedBillingApiUrl.replace(/\/+$/, "");
864
+ const resolvedAccount = (0, import_react6.useMemo)(() => mergeAccountPatch({
865
+ userId,
866
+ email,
867
+ firstName,
868
+ lastName,
869
+ country: countryProp,
870
+ zip: zipProp
871
+ }, accountPatch), [userId, email, firstName, lastName, countryProp, zipProp, accountPatch]);
721
872
  const stripeInstance = (0, import_react6.useMemo)(() => {
722
873
  if (!flopay) return null;
723
874
  return flopay.getRawProvider();
@@ -743,6 +894,12 @@ function SplitCardFormInner({
743
894
  },
744
895
  [onErrorChange]
745
896
  );
897
+ const emitDecline = (0, import_react6.useCallback)(
898
+ (method, input, overrides) => {
899
+ onDecline?.(buildDeclineEvent(method, input, overrides));
900
+ },
901
+ [onDecline]
902
+ );
746
903
  const showWallets = showApplePay || showGooglePay;
747
904
  const handleNameChange = (0, import_react6.useCallback)((value) => {
748
905
  setFullName(value);
@@ -750,6 +907,30 @@ function SplitCardFormInner({
750
907
  onFirstNameChange?.(parts[0] ?? "");
751
908
  onLastNameChange?.(parts.length > 1 ? parts.slice(1).join(" ") : "");
752
909
  }, [onFirstNameChange, onLastNameChange]);
910
+ const runBeforeCardButtonClick = (0, import_react6.useCallback)(async () => {
911
+ if (!onBeforeButtonClick) return true;
912
+ try {
913
+ const result = await onBeforeButtonClick({
914
+ method: "card",
915
+ sessionId: sessionId || void 0
916
+ });
917
+ if (result === false) {
918
+ return false;
919
+ }
920
+ if (result?.account) {
921
+ setAccountPatch((prev) => ({ ...prev ?? {}, ...result.account }));
922
+ }
923
+ return true;
924
+ } catch (err) {
925
+ const floPayErr = err instanceof import_shared5.FloPayError ? err : new import_shared5.FloPayError(
926
+ err instanceof Error ? err.message : "Card before-click hook failed.",
927
+ "validation_error"
928
+ );
929
+ updateError(floPayErr.message);
930
+ onError?.(floPayErr);
931
+ return false;
932
+ }
933
+ }, [onBeforeButtonClick, sessionId, updateError, onError]);
753
934
  const processPaymentInternal = (0, import_react6.useCallback)(
754
935
  async (tokenizedBody) => {
755
936
  if (processingRef.current) return;
@@ -762,16 +943,16 @@ function SplitCardFormInner({
762
943
  method: "POST",
763
944
  headers: {
764
945
  "Content-Type": "application/json",
765
- "x-user-id": userId ?? ""
946
+ "x-user-id": resolvedAccount.userId ?? ""
766
947
  },
767
948
  body: JSON.stringify({
768
949
  sessionId,
769
950
  tokenizedData: tokenizedBody,
770
951
  accountData: {
771
- userId: userId ?? "",
772
- email: email ?? "",
773
- firstName: firstName ?? fullName.trim().split(/\s+/)[0] ?? "",
774
- lastName: lastName ?? fullName.trim().split(/\s+/).slice(1).join(" ") ?? "",
952
+ userId: resolvedAccount.userId ?? "",
953
+ email: resolvedAccount.email ?? "",
954
+ firstName: resolvedAccount.firstName ?? fullName.trim().split(/\s+/)[0] ?? "",
955
+ lastName: resolvedAccount.lastName ?? fullName.trim().split(/\s+/).slice(1).join(" ") ?? "",
775
956
  ...enableAVS ? { zip: zipCodeRef.current, country: selectedCountryRef.current } : {}
776
957
  },
777
958
  chv
@@ -804,6 +985,7 @@ function SplitCardFormInner({
804
985
  setOverlayStatus("error");
805
986
  updateError(result.error.message);
806
987
  onError?.(result.error);
988
+ emitDecline("card", result.error);
807
989
  return;
808
990
  }
809
991
  if (result.status === "succeeded" || result.status === "processing") {
@@ -846,7 +1028,11 @@ function SplitCardFormInner({
846
1028
  });
847
1029
  if (confirmError) {
848
1030
  setOverlayStatus("error");
849
- updateError(confirmError.message ?? "PayPal payment failed.");
1031
+ const message2 = confirmError.message ?? "PayPal payment failed.";
1032
+ updateError(message2);
1033
+ emitDecline("paypal", message2, {
1034
+ code: confirmError.code
1035
+ });
850
1036
  }
851
1037
  } catch (err) {
852
1038
  setOverlayStatus("error");
@@ -855,7 +1041,12 @@ function SplitCardFormInner({
855
1041
  return;
856
1042
  }
857
1043
  setOverlayStatus("error");
858
- updateError(json?.message ?? "Payment failed. Please try again.");
1044
+ const message = json?.message ?? "Payment failed. Please try again.";
1045
+ updateError(message);
1046
+ emitDecline(tokenizedBody.isPaypal ? "paypal" : "card", message, {
1047
+ code: json?.code ?? json?.gatewayErrorCode,
1048
+ declineCode: json?.declineCode ?? json?.gatewayDeclineReason
1049
+ });
859
1050
  await new Promise((r) => setTimeout(r, 1500));
860
1051
  } catch (err) {
861
1052
  setOverlayStatus("error");
@@ -867,7 +1058,7 @@ function SplitCardFormInner({
867
1058
  processingRef.current = false;
868
1059
  }
869
1060
  },
870
- [baseUrl, sessionId, userId, email, firstName, lastName, fullName, chv, flopay, onComplete, onError, updateError]
1061
+ [baseUrl, sessionId, resolvedAccount, fullName, chv, flopay, onComplete, onError, updateError, emitDecline]
871
1062
  );
872
1063
  const dispatchTokenizedBody = (0, import_react6.useCallback)(
873
1064
  (tokenizedBody) => {
@@ -891,6 +1082,7 @@ function SplitCardFormInner({
891
1082
  if (result.error) {
892
1083
  updateError(result.error.message);
893
1084
  onError?.(result.error);
1085
+ emitDecline("card", result.error);
894
1086
  } else if (result.status === "succeeded" || result.status === "processing") {
895
1087
  dispatchTokenizedBody({
896
1088
  id: result.paymentIntentId,
@@ -904,7 +1096,7 @@ function SplitCardFormInner({
904
1096
  setIs3DSActive(false);
905
1097
  }
906
1098
  }
907
- }), [flopay, dispatchTokenizedBody, onError, updateError]);
1099
+ }), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
908
1100
  (0, import_react6.useEffect)(() => {
909
1101
  if (typeof window === "undefined") return;
910
1102
  const stored = localStorage.getItem(WALLET_RESUME_KEY);
@@ -927,14 +1119,16 @@ function SplitCardFormInner({
927
1119
  async (e) => {
928
1120
  e.preventDefault();
929
1121
  if (!flopay || !elements || isSubmitting || processingRef.current) return;
930
- onButtonClick?.("card");
1122
+ if (layout !== "buttons") {
1123
+ onButtonClick?.("card");
1124
+ }
931
1125
  setProcessing(true);
932
1126
  setOverlayStatus("processing");
933
1127
  updateError(null);
934
1128
  let handedOff = false;
935
1129
  try {
936
1130
  if (enableAVS && !zipCodeRef.current.trim()) {
937
- updateError((0, import_shared3.getPostalCodeLabel)(selectedCountryRef.current) + " is required");
1131
+ updateError((0, import_shared4.getPostalCodeLabel)(selectedCountryRef.current) + " is required");
938
1132
  return;
939
1133
  }
940
1134
  const submitResult = await flopay.submitElements();
@@ -955,23 +1149,23 @@ function SplitCardFormInner({
955
1149
  updateError(pmResult.error?.message ?? "Failed to create payment method.");
956
1150
  return;
957
1151
  }
958
- if (!sessionId || !email) {
959
- throw new import_shared4.FloPayError("Missing sessionId or email", "validation_error");
1152
+ if (!sessionId || !resolvedAccount.email) {
1153
+ throw new import_shared5.FloPayError("Missing sessionId or email", "validation_error");
960
1154
  }
961
1155
  const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
962
1156
  method: "POST",
963
1157
  headers: { "Content-Type": "application/json" },
964
1158
  body: JSON.stringify({
965
1159
  sessionId,
966
- email,
1160
+ email: resolvedAccount.email,
967
1161
  paymentMethodType: pmResult.paymentMethodId,
968
1162
  isPaypal: false
969
1163
  })
970
1164
  });
971
- if (!intentResponse.ok) throw new import_shared4.FloPayError("Failed to create payment intent", "api_error");
1165
+ if (!intentResponse.ok) throw new import_shared5.FloPayError("Failed to create payment intent", "api_error");
972
1166
  const intentJson = await intentResponse.json();
973
1167
  const intentClientSecret = intentJson.data?.id;
974
- if (!intentClientSecret) throw new import_shared4.FloPayError("No client_secret in payment intent response", "api_error");
1168
+ if (!intentClientSecret) throw new import_shared5.FloPayError("No client_secret in payment intent response", "api_error");
975
1169
  const confirmResult = await flopay.confirmCardPayment({
976
1170
  clientSecret: intentClientSecret,
977
1171
  paymentMethodId: pmResult.paymentMethodId
@@ -979,6 +1173,8 @@ function SplitCardFormInner({
979
1173
  if (confirmResult.error) {
980
1174
  setOverlayStatus("error");
981
1175
  updateError(confirmResult.error.message);
1176
+ onError?.(confirmResult.error);
1177
+ emitDecline("card", confirmResult.error);
982
1178
  await new Promise((r) => setTimeout(r, 1500));
983
1179
  return;
984
1180
  }
@@ -999,7 +1195,7 @@ function SplitCardFormInner({
999
1195
  }
1000
1196
  }
1001
1197
  },
1002
- [flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError]
1198
+ [flopay, elements, isSubmitting, sessionId, resolvedAccount.email, baseUrl, isSelfContained, dispatchTokenizedBody, onButtonClick, onError, updateError, emitDecline, layout]
1003
1199
  );
1004
1200
  const isReady = flopay !== null && elements !== null;
1005
1201
  if (!isReady) {
@@ -1170,7 +1366,7 @@ function SplitCardFormInner({
1170
1366
  cursor: "pointer",
1171
1367
  ...isButtons && bStyles.nameInput ? bStyles.nameInput : {}
1172
1368
  },
1173
- children: import_shared3.COUNTRY_OPTIONS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("option", { value: c.code, children: [
1369
+ children: import_shared4.COUNTRY_OPTIONS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("option", { value: c.code, children: [
1174
1370
  c.flag,
1175
1371
  " ",
1176
1372
  c.name
@@ -1187,7 +1383,7 @@ function SplitCardFormInner({
1187
1383
  }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1188
1384
  "input",
1189
1385
  {
1190
- placeholder: (0, import_shared3.getPostalCodeLabel)(selectedCountry),
1386
+ placeholder: (0, import_shared4.getPostalCodeLabel)(selectedCountry),
1191
1387
  autoComplete: "postal-code",
1192
1388
  value: zipCode,
1193
1389
  onChange: (e) => {
@@ -1284,32 +1480,36 @@ function SplitCardFormInner({
1284
1480
  PayPalButtonInner,
1285
1481
  {
1286
1482
  sessionId,
1287
- email,
1483
+ email: resolvedAccount.email,
1288
1484
  billingApiUrl: resolvedBillingApiUrl,
1289
1485
  onTokenizedBody: dispatchTokenizedBody,
1290
1486
  onErrorChange: updateError,
1291
1487
  isProcessing: isSubmitting,
1292
- onButtonClick
1488
+ onButtonClick,
1489
+ onDecline
1293
1490
  }
1294
1491
  ) }) : showPayPal ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { height: 44, borderRadius: 8, background: "#e5e7eb", animation: "flopay-pulse 1.5s ease-in-out infinite" } }) : null,
1295
1492
  showWallets && stripeInstance ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_stripe_js.Elements, { stripe: stripeInstance, options: walletOptions, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1296
1493
  WalletButtonInner,
1297
1494
  {
1298
1495
  sessionId,
1299
- email,
1496
+ email: resolvedAccount.email,
1300
1497
  billingApiUrl: resolvedBillingApiUrl,
1301
1498
  showApplePay,
1302
1499
  showGooglePay,
1303
1500
  onTokenizedBody: dispatchTokenizedBody,
1304
1501
  onErrorChange: updateError,
1305
- onButtonClick
1502
+ onButtonClick,
1503
+ onDecline
1306
1504
  }
1307
1505
  ) }) : showWallets ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { height: 44, borderRadius: 8, background: "#e5e7eb", animation: "flopay-pulse 1.5s ease-in-out infinite" } }) : null,
1308
1506
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1309
1507
  "button",
1310
1508
  {
1311
1509
  type: "button",
1312
- onClick: () => {
1510
+ onClick: async () => {
1511
+ const shouldContinue = await runBeforeCardButtonClick();
1512
+ if (!shouldContinue) return;
1313
1513
  onButtonClick?.("card");
1314
1514
  expandToCard();
1315
1515
  },
@@ -1374,23 +1574,25 @@ function SplitCardFormInner({
1374
1574
  WalletButtonInner,
1375
1575
  {
1376
1576
  sessionId,
1377
- email,
1577
+ email: resolvedAccount.email,
1378
1578
  billingApiUrl: resolvedBillingApiUrl,
1379
1579
  showApplePay,
1380
1580
  showGooglePay,
1381
1581
  onTokenizedBody: dispatchTokenizedBody,
1382
- onErrorChange: updateError
1582
+ onErrorChange: updateError,
1583
+ onDecline
1383
1584
  }
1384
1585
  ) }),
1385
1586
  showPayPal && stripeInstance && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_react_stripe_js.Elements, { stripe: stripeInstance, options: paypalOptions, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1386
1587
  PayPalButtonInner,
1387
1588
  {
1388
1589
  sessionId,
1389
- email,
1590
+ email: resolvedAccount.email,
1390
1591
  billingApiUrl: resolvedBillingApiUrl,
1391
1592
  onTokenizedBody: dispatchTokenizedBody,
1392
1593
  onErrorChange: updateError,
1393
- isProcessing: isSubmitting
1594
+ isProcessing: isSubmitting,
1595
+ onDecline
1394
1596
  }
1395
1597
  ) }),
1396
1598
  (showWallets && stripeInstance || showPayPal && stripeInstance) && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
@@ -1422,6 +1624,7 @@ function FloPayCheckout({
1422
1624
  error: errorNode,
1423
1625
  onComplete,
1424
1626
  onError,
1627
+ onDecline,
1425
1628
  showPayPal = true,
1426
1629
  showApplePay = true,
1427
1630
  showGooglePay = true,
@@ -1432,6 +1635,7 @@ function FloPayCheckout({
1432
1635
  cardBackButtonContent,
1433
1636
  cardTitleContent,
1434
1637
  onButtonClick,
1638
+ onBeforeButtonClick,
1435
1639
  enableAVS,
1436
1640
  avsLayout,
1437
1641
  submitLabel,
@@ -1442,7 +1646,7 @@ function FloPayCheckout({
1442
1646
  renderConfirmButton,
1443
1647
  onSessionCompleted
1444
1648
  }) {
1445
- const resolvedBillingUrl = (0, import_shared5.resolveBillingApiUrl)(billingApiUrl);
1649
+ const resolvedBillingUrl = (0, import_shared6.resolveBillingApiUrl)(billingApiUrl);
1446
1650
  const [unified, setUnified] = (0, import_react7.useState)(null);
1447
1651
  const [flopay, setFloPay] = (0, import_react7.useState)(null);
1448
1652
  const flopayRef = (0, import_react7.useRef)(null);
@@ -1453,13 +1657,28 @@ function FloPayCheckout({
1453
1657
  const [currentMode, setCurrentMode] = (0, import_react7.useState)("full");
1454
1658
  const [confirmProcessing, setConfirmProcessing] = (0, import_react7.useState)(false);
1455
1659
  const [modeError, setModeError] = (0, import_react7.useState)(null);
1660
+ const [createSessionPatch, setCreateSessionPatch] = (0, import_react7.useState)(void 0);
1661
+ const [cardBootstrapPending, setCardBootstrapPending] = (0, import_react7.useState)(false);
1662
+ const [deferredCardOpen, setDeferredCardOpen] = (0, import_react7.useState)(false);
1456
1663
  const autoCheckoutAttempted = (0, import_react7.useRef)(false);
1457
1664
  const onCompleteRef = (0, import_react7.useRef)(onComplete);
1458
1665
  onCompleteRef.current = onComplete;
1459
1666
  const onErrorRef = (0, import_react7.useRef)(onError);
1460
1667
  onErrorRef.current = onError;
1668
+ const onDeclineRef = (0, import_react7.useRef)(onDecline);
1669
+ onDeclineRef.current = onDecline;
1461
1670
  const onSessionCompletedRef = (0, import_react7.useRef)(onSessionCompleted);
1462
1671
  onSessionCompletedRef.current = onSessionCompleted;
1672
+ const effectiveCreateSession = (0, import_react7.useMemo)(
1673
+ () => createSessionParams ? mergeInlineSessionPatch(createSessionParams, createSessionPatch) : void 0,
1674
+ [createSessionParams, createSessionPatch]
1675
+ );
1676
+ const emitDecline = (0, import_react7.useCallback)(
1677
+ (method, input, overrides) => {
1678
+ onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
1679
+ },
1680
+ []
1681
+ );
1463
1682
  const processPaymentForMode = (0, import_react7.useCallback)(
1464
1683
  async (sess) => {
1465
1684
  const baseUrl = resolvedBillingUrl.replace(/\/+$/, "");
@@ -1499,9 +1718,13 @@ function FloPayCheckout({
1499
1718
  // No client secret available
1500
1719
  };
1501
1720
  }
1502
- throw new import_shared5.FloPayError(
1721
+ throw new import_shared6.FloPayError(
1503
1722
  json?.message ?? "Payment failed. Please try again.",
1504
- "api_error"
1723
+ "api_error",
1724
+ {
1725
+ code: json?.code ?? json?.gatewayErrorCode,
1726
+ declineCode: json?.declineCode ?? json?.gatewayDeclineReason
1727
+ }
1505
1728
  );
1506
1729
  },
1507
1730
  [resolvedBillingUrl, resolvedSessionId]
@@ -1520,6 +1743,9 @@ function FloPayCheckout({
1520
1743
  });
1521
1744
  if (nextActionError) {
1522
1745
  setModeError(nextActionError.message ?? "3DS authentication failed.");
1746
+ emitDecline("card", nextActionError.message ?? "3DS authentication failed.", {
1747
+ code: nextActionError.code
1748
+ });
1523
1749
  return false;
1524
1750
  }
1525
1751
  if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
@@ -1542,6 +1768,9 @@ function FloPayCheckout({
1542
1768
  });
1543
1769
  if (error) {
1544
1770
  setModeError(error.message ?? "PayPal authorization failed.");
1771
+ emitDecline("paypal", error.message ?? "PayPal authorization failed.", {
1772
+ code: error.code
1773
+ });
1545
1774
  return false;
1546
1775
  }
1547
1776
  onCompleteRef.current?.({ status: "succeeded" });
@@ -1549,16 +1778,24 @@ function FloPayCheckout({
1549
1778
  }
1550
1779
  return false;
1551
1780
  },
1552
- [resolvedBillingUrl, resolvedSessionId]
1781
+ [resolvedBillingUrl, resolvedSessionId, emitDecline]
1553
1782
  );
1554
1783
  const inflightRef = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
1555
1784
  const initializedHashRef = (0, import_react7.useRef)(null);
1785
+ const deferInlineSessionUntilCardClick = Boolean(
1786
+ effectiveCreateSession && !children && layout === "buttons" && onBeforeButtonClick && (effectiveCreateSession.checkoutMode ?? checkoutModeProp ?? "full") === "full"
1787
+ );
1556
1788
  function hashCreateParams(params) {
1557
1789
  const key = JSON.stringify({
1558
1790
  c: params?.clientId,
1791
+ successUrl: params?.successUrl,
1792
+ cancelUrl: params?.cancelUrl,
1559
1793
  i: params?.items?.map((x) => `${x.providerItemId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
1560
1794
  s: params?.subscriptions?.map((x) => `${x.providerPlanId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
1561
- e: params?.account.email,
1795
+ account: params?.account,
1796
+ couponCodes: params?.couponCodes,
1797
+ tagsData: params?.tagsData,
1798
+ utmMetadata: params?.utmMetadata,
1562
1799
  m: params?.checkoutMode ?? "full"
1563
1800
  });
1564
1801
  let h = 0;
@@ -1568,138 +1805,167 @@ function FloPayCheckout({
1568
1805
  return `flopay_session_${Math.abs(h).toString(36)}`;
1569
1806
  }
1570
1807
  const createSessionHash = (0, import_react7.useMemo)(
1571
- () => createSessionParams ? hashCreateParams(createSessionParams) : "",
1572
- // eslint-disable-next-line react-hooks/exhaustive-deps
1573
- [
1574
- createSessionParams?.clientId,
1575
- createSessionParams?.account?.email,
1576
- createSessionParams?.checkoutMode,
1577
- JSON.stringify(createSessionParams?.items),
1578
- JSON.stringify(createSessionParams?.subscriptions)
1579
- ]
1808
+ () => effectiveCreateSession ? hashCreateParams(effectiveCreateSession) : "",
1809
+ [effectiveCreateSession]
1580
1810
  );
1581
- const createSessionParamsRef = (0, import_react7.useRef)(createSessionParams);
1582
- createSessionParamsRef.current = createSessionParams;
1811
+ const createSessionParamsRef = (0, import_react7.useRef)(effectiveCreateSession);
1812
+ createSessionParamsRef.current = effectiveCreateSession;
1813
+ (0, import_react7.useEffect)(() => {
1814
+ setResolvedSessionId(sessionIdProp ?? "");
1815
+ }, [sessionIdProp]);
1816
+ async function resolveInlineSession(params, cacheKey) {
1817
+ ensureInlineSessionReady(params);
1818
+ const api = new import_js.PaymentAPI(resolvedBillingUrl);
1819
+ let sid = typeof window !== "undefined" ? window.sessionStorage.getItem(cacheKey) : null;
1820
+ let realResult = null;
1821
+ if (sid) {
1822
+ try {
1823
+ realResult = await api.getUnifiedCheckoutSession(sid);
1824
+ if (realResult.data.session?.status === "complete") {
1825
+ if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1826
+ sid = null;
1827
+ realResult = null;
1828
+ }
1829
+ } catch {
1830
+ if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1831
+ sid = null;
1832
+ }
1833
+ }
1834
+ if (!sid) {
1835
+ realResult = await api.createAndFetchSession(params);
1836
+ sid = realResult.data.session?.id ?? "";
1837
+ if (sid && typeof window !== "undefined") {
1838
+ window.sessionStorage.setItem(cacheKey, sid);
1839
+ }
1840
+ }
1841
+ return { sid: sid ?? "", result: realResult };
1842
+ }
1843
+ const bootstrapInlineSession = (0, import_react7.useCallback)(
1844
+ async (patch) => {
1845
+ const baseParams = createSessionParamsRef.current;
1846
+ if (!baseParams) {
1847
+ throw new import_shared6.FloPayError("createSession is required to bootstrap checkout.", "validation_error");
1848
+ }
1849
+ const mergedParams = mergeInlineSessionPatch(baseParams, patch);
1850
+ const cacheKey = hashCreateParams(mergedParams);
1851
+ let promise = inflightRef.current.get(cacheKey);
1852
+ if (!promise) {
1853
+ promise = resolveInlineSession(mergedParams, cacheKey);
1854
+ inflightRef.current.set(cacheKey, promise);
1855
+ }
1856
+ let resolved;
1857
+ try {
1858
+ resolved = await promise;
1859
+ } finally {
1860
+ inflightRef.current.delete(cacheKey);
1861
+ }
1862
+ if (patch) {
1863
+ setCreateSessionPatch((prev) => mergeInlineSessionPatches(prev, patch));
1864
+ }
1865
+ const { sid, result: realResult } = resolved;
1866
+ setUnified(realResult);
1867
+ if (realResult.data.session) {
1868
+ setSession(realResult.data.session);
1869
+ }
1870
+ if (sid) {
1871
+ setResolvedSessionId(sid);
1872
+ }
1873
+ let publishableKey;
1874
+ if (realResult.provider === "stripe") {
1875
+ publishableKey = realResult.data.stripe?.publishableKey;
1876
+ }
1877
+ if (!publishableKey) publishableKey = fallbackPublishableKey;
1878
+ if (!publishableKey) {
1879
+ throw new import_shared6.FloPayError(
1880
+ "No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
1881
+ "validation_error"
1882
+ );
1883
+ }
1884
+ const instance = await (0, import_js.loadFloPay)(publishableKey, {
1885
+ billingApiUrl: resolvedBillingUrl,
1886
+ locale
1887
+ });
1888
+ flopayRef.current = instance;
1889
+ setFloPay(instance);
1890
+ initializedHashRef.current = cacheKey;
1891
+ setIsLoading(false);
1892
+ return resolved;
1893
+ },
1894
+ [fallbackPublishableKey, locale, resolvedBillingUrl]
1895
+ );
1896
+ const handleDeferredCardButtonClick = (0, import_react7.useCallback)(async () => {
1897
+ if (cardBootstrapPending) return;
1898
+ setLoadError(null);
1899
+ setCardBootstrapPending(true);
1900
+ try {
1901
+ const beforeClickResult = await onBeforeButtonClick?.({
1902
+ method: "card",
1903
+ createSession: effectiveCreateSession
1904
+ });
1905
+ if (beforeClickResult === false) {
1906
+ setDeferredCardOpen(false);
1907
+ return;
1908
+ }
1909
+ const patch = beforeClickResult && typeof beforeClickResult === "object" ? beforeClickResult : void 0;
1910
+ const baseParams = createSessionParamsRef.current;
1911
+ if (!baseParams) {
1912
+ throw new import_shared6.FloPayError(
1913
+ "createSession is required to bootstrap checkout.",
1914
+ "validation_error"
1915
+ );
1916
+ }
1917
+ ensureInlineSessionReady(mergeInlineSessionPatch(baseParams, patch));
1918
+ setDeferredCardOpen(true);
1919
+ onButtonClick?.("card");
1920
+ await bootstrapInlineSession(patch);
1921
+ } catch (err) {
1922
+ setDeferredCardOpen(false);
1923
+ const floPayErr = err instanceof import_shared6.FloPayError ? err : new import_shared6.FloPayError(
1924
+ err instanceof Error ? err.message : "Failed to start card checkout.",
1925
+ "api_error"
1926
+ );
1927
+ setLoadError(floPayErr);
1928
+ onErrorRef.current?.(floPayErr);
1929
+ } finally {
1930
+ setCardBootstrapPending(false);
1931
+ }
1932
+ }, [
1933
+ bootstrapInlineSession,
1934
+ cardBootstrapPending,
1935
+ effectiveCreateSession,
1936
+ onButtonClick,
1937
+ onBeforeButtonClick
1938
+ ]);
1583
1939
  (0, import_react7.useEffect)(() => {
1584
1940
  let cancelled = false;
1585
1941
  setLoadError(null);
1586
1942
  if (createSessionHash) {
1943
+ if (deferInlineSessionUntilCardClick) {
1944
+ if (initializedHashRef.current !== createSessionHash) {
1945
+ setSession(null);
1946
+ setUnified(null);
1947
+ setFloPay(null);
1948
+ setResolvedSessionId("");
1949
+ setDeferredCardOpen(false);
1950
+ flopayRef.current = null;
1951
+ }
1952
+ setCurrentMode(createSessionParamsRef.current?.checkoutMode ?? checkoutModeProp ?? "full");
1953
+ setIsLoading(false);
1954
+ return () => {
1955
+ cancelled = true;
1956
+ };
1957
+ }
1587
1958
  if (initializedHashRef.current === createSessionHash) return;
1588
1959
  const params = createSessionParamsRef.current;
1589
- const totalAmount = [
1590
- ...(params.items ?? []).map((i) => i.overrideAmount ?? i.totalAmount ?? 0),
1591
- ...(params.subscriptions ?? []).map((s) => s.overrideAmount ?? s.totalAmount ?? 0)
1592
- ].reduce((sum, v) => sum + v, 0);
1593
- const currency = params.items?.[0]?.currency ?? params.subscriptions?.[0]?.currency ?? "USD";
1594
- const syntheticSession = {
1595
- id: "",
1596
- clientSecret: "",
1597
- mode: "payment",
1598
- amount: Math.round(totalAmount * 100),
1599
- currency,
1600
- status: "open",
1601
- customer: {
1602
- id: params.account.userId,
1603
- email: params.account.email,
1604
- firstName: params.account.firstName,
1605
- lastName: params.account.lastName
1606
- },
1607
- successUrl: params.successUrl,
1608
- cancelUrl: params.cancelUrl,
1609
- checkoutMode: params.checkoutMode ?? "full",
1610
- items: (params.items ?? []).map((i, idx) => ({
1611
- uuid: `synthetic-item-${idx}`,
1612
- checkoutSessionId: "",
1613
- providerItemId: i.providerItemId,
1614
- providerItemName: i.providerItemName ?? i.providerItemId,
1615
- quantity: i.quantity ?? 1,
1616
- totalAmount: i.totalAmount,
1617
- overrideAmount: i.overrideAmount ?? null,
1618
- currency: i.currency ?? currency
1619
- })),
1620
- subscriptions: (params.subscriptions ?? []).map((s, idx) => ({
1621
- uuid: `synthetic-sub-${idx}`,
1622
- checkoutSessionId: "",
1623
- providerPlanId: s.providerPlanId,
1624
- providerPlanName: s.providerPlanName ?? s.providerPlanId,
1625
- quantity: s.quantity ?? 1,
1626
- totalAmount: s.totalAmount,
1627
- overrideAmount: s.overrideAmount ?? null,
1628
- currency: s.currency ?? currency
1629
- }))
1630
- };
1631
- setSession(syntheticSession);
1960
+ setSession(buildSyntheticSession(params, checkoutModeProp));
1632
1961
  setCurrentMode(params.checkoutMode ?? checkoutModeProp ?? "full");
1633
1962
  setIsLoading(false);
1634
- const cacheKey = createSessionHash;
1635
- async function resolveSession() {
1636
- const api = new import_js.PaymentAPI(resolvedBillingUrl);
1637
- let sid = typeof window !== "undefined" ? window.sessionStorage.getItem(cacheKey) : null;
1638
- let realResult = null;
1639
- if (sid) {
1640
- try {
1641
- realResult = await api.getUnifiedCheckoutSession(sid);
1642
- if (realResult.data.session?.status === "complete") {
1643
- if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1644
- sid = null;
1645
- realResult = null;
1646
- }
1647
- } catch {
1648
- if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1649
- sid = null;
1650
- }
1651
- }
1652
- if (!sid) {
1653
- realResult = await api.createAndFetchSession(createSessionParamsRef.current);
1654
- sid = realResult.data.session?.id ?? "";
1655
- if (sid && typeof window !== "undefined") {
1656
- window.sessionStorage.setItem(cacheKey, sid);
1657
- }
1658
- }
1659
- return { sid: sid ?? "", result: realResult };
1660
- }
1661
1963
  (async () => {
1662
1964
  try {
1663
- let promise = inflightRef.current.get(cacheKey);
1664
- if (!promise) {
1665
- promise = resolveSession();
1666
- inflightRef.current.set(cacheKey, promise);
1667
- }
1668
- let resolved;
1669
- try {
1670
- resolved = await promise;
1671
- } finally {
1672
- inflightRef.current.delete(cacheKey);
1673
- }
1674
- if (cancelled) return;
1675
- const { sid, result: realResult } = resolved;
1676
- if (realResult) {
1677
- setUnified(realResult);
1678
- if (realResult.data.session) setSession(realResult.data.session);
1679
- }
1680
- if (sid) {
1681
- setResolvedSessionId(sid);
1682
- }
1683
- let pk;
1684
- if (realResult?.provider === "stripe") {
1685
- pk = realResult.data.stripe?.publishableKey;
1686
- }
1687
- if (!pk) pk = fallbackPublishableKey;
1688
- if (!pk) {
1689
- throw new import_shared5.FloPayError(
1690
- "No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
1691
- "validation_error"
1692
- );
1693
- }
1694
- const instance = await (0, import_js.loadFloPay)(pk, { billingApiUrl: resolvedBillingUrl, locale });
1695
- if (!cancelled) {
1696
- flopayRef.current = instance;
1697
- setFloPay(instance);
1698
- initializedHashRef.current = cacheKey;
1699
- }
1965
+ await bootstrapInlineSession();
1700
1966
  } catch (err) {
1701
1967
  if (cancelled) return;
1702
- const floPayErr = err instanceof import_shared5.FloPayError ? err : new import_shared5.FloPayError(err instanceof Error ? err.message : "Failed to create session", "api_error");
1968
+ const floPayErr = err instanceof import_shared6.FloPayError ? err : new import_shared6.FloPayError(err instanceof Error ? err.message : "Failed to create session", "api_error");
1703
1969
  setLoadError(floPayErr);
1704
1970
  }
1705
1971
  })();
@@ -1717,7 +1983,7 @@ function FloPayCheckout({
1717
1983
  const sess = result.data.session ?? null;
1718
1984
  setSession(sess);
1719
1985
  if (!sess) {
1720
- throw new import_shared5.FloPayError("No session data returned", "api_error");
1986
+ throw new import_shared6.FloPayError("No session data returned", "api_error");
1721
1987
  }
1722
1988
  if (sess.status === "complete") {
1723
1989
  setIsLoading(false);
@@ -1756,7 +2022,7 @@ function FloPayCheckout({
1756
2022
  if (!cancelled) setIsLoading(false);
1757
2023
  } catch (err) {
1758
2024
  if (cancelled) return;
1759
- const floPayErr = err instanceof import_shared5.FloPayError ? err : new import_shared5.FloPayError(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
2025
+ const floPayErr = err instanceof import_shared6.FloPayError ? err : new import_shared6.FloPayError(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
1760
2026
  setLoadError(floPayErr);
1761
2027
  setIsLoading(false);
1762
2028
  }
@@ -1768,7 +2034,7 @@ function FloPayCheckout({
1768
2034
  }
1769
2035
  if (!publishableKey) publishableKey = fallbackPublishableKey;
1770
2036
  if (!publishableKey) {
1771
- throw new import_shared5.FloPayError(
2037
+ throw new import_shared6.FloPayError(
1772
2038
  "No publishable key found in session response. Provide a fallbackPublishableKey prop or ensure the session includes gatewayData.publishableKey.",
1773
2039
  "validation_error"
1774
2040
  );
@@ -1784,7 +2050,7 @@ function FloPayCheckout({
1784
2050
  return () => {
1785
2051
  cancelled = true;
1786
2052
  };
1787
- }, [resolvedSessionId, createSessionHash, resolvedBillingUrl, fallbackPublishableKey, locale, checkoutModeProp]);
2053
+ }, [resolvedSessionId, createSessionHash, checkoutModeProp, deferInlineSessionUntilCardClick, bootstrapInlineSession]);
1788
2054
  const handleConfirmCheckout = (0, import_react7.useCallback)(async () => {
1789
2055
  if (confirmProcessing || !session) return;
1790
2056
  setConfirmProcessing(true);
@@ -1799,17 +2065,18 @@ function FloPayCheckout({
1799
2065
  setCurrentMode("full");
1800
2066
  }
1801
2067
  } catch (err) {
1802
- const floPayErr = err instanceof import_shared5.FloPayError ? err : new import_shared5.FloPayError(
2068
+ const floPayErr = err instanceof import_shared6.FloPayError ? err : new import_shared6.FloPayError(
1803
2069
  err instanceof Error ? err.message : "Payment failed",
1804
2070
  "api_error"
1805
2071
  );
1806
2072
  setModeError(floPayErr.message);
1807
2073
  onError?.(floPayErr);
2074
+ emitDecline("card", floPayErr);
1808
2075
  setCurrentMode("full");
1809
2076
  } finally {
1810
2077
  setConfirmProcessing(false);
1811
2078
  }
1812
- }, [confirmProcessing, session, processPaymentForMode, handleRedirectResult, onError]);
2079
+ }, [confirmProcessing, session, processPaymentForMode, handleRedirectResult, onError, emitDecline]);
1813
2080
  const providerOptions = (0, import_react7.useMemo)(() => {
1814
2081
  if (!unified || !session) return void 0;
1815
2082
  const opts = {
@@ -1820,7 +2087,7 @@ function FloPayCheckout({
1820
2087
  if (unified.provider === "stripe" && unified.data.stripe?.clientSecret) {
1821
2088
  opts.clientSecret = unified.data.stripe.clientSecret;
1822
2089
  } else {
1823
- const displayTotal = (0, import_shared5.buildCheckoutDisplayData)(session).total;
2090
+ const displayTotal = (0, import_shared6.buildCheckoutDisplayData)(session).total;
1824
2091
  opts.amount = Math.round(displayTotal * 100) || session.amount;
1825
2092
  opts.currency = session.currency?.toLowerCase();
1826
2093
  }
@@ -1835,6 +2102,8 @@ function FloPayCheckout({
1835
2102
  }),
1836
2103
  [session, isLoading, loadError, currentMode]
1837
2104
  );
2105
+ const shouldShowInterimButtons = Boolean(createSessionParams) && layout === "buttons" && (!flopay || !providerOptions);
2106
+ const shouldKeepDeferredInterimVisible = shouldShowInterimButtons && deferInlineSessionUntilCardClick;
1838
2107
  if (isLoading) {
1839
2108
  if (loadingNode) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: loadingNode });
1840
2109
  if (layout === "buttons") {
@@ -1863,7 +2132,7 @@ function FloPayCheckout({
1863
2132
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
1864
2133
  ] });
1865
2134
  }
1866
- if (loadError) {
2135
+ if (loadError && !shouldKeepDeferredInterimVisible) {
1867
2136
  if (errorNode) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: errorNode(loadError) });
1868
2137
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1869
2138
  "div",
@@ -1878,14 +2147,18 @@ function FloPayCheckout({
1878
2147
  }
1879
2148
  );
1880
2149
  }
1881
- if ((!flopay || !providerOptions) && createSessionParams && layout === "buttons") {
2150
+ if (shouldShowInterimButtons) {
1882
2151
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1883
2152
  InterimButtonsView,
1884
2153
  {
1885
2154
  onButtonClick,
1886
- showPayPal,
1887
- showApplePay,
1888
- showGooglePay,
2155
+ onCardButtonClick: deferInlineSessionUntilCardClick ? handleDeferredCardButtonClick : void 0,
2156
+ cardLoading: cardBootstrapPending,
2157
+ cardOpen: deferInlineSessionUntilCardClick ? deferredCardOpen : void 0,
2158
+ errorMessage: shouldKeepDeferredInterimVisible ? loadError?.message ?? null : null,
2159
+ showPayPal: deferInlineSessionUntilCardClick ? false : showPayPal,
2160
+ showApplePay: deferInlineSessionUntilCardClick ? false : showApplePay,
2161
+ showGooglePay: deferInlineSessionUntilCardClick ? false : showGooglePay,
1889
2162
  buttonsTheme,
1890
2163
  buttonsStyles,
1891
2164
  cardButtonContent,
@@ -1967,10 +2240,11 @@ function FloPayCheckout({
1967
2240
  userId: session?.customer?.id,
1968
2241
  firstName: session?.customer?.firstName,
1969
2242
  lastName: session?.customer?.lastName,
1970
- totalAmount: session ? Math.round((0, import_shared5.buildCheckoutDisplayData)(session).total * 100) : 0,
2243
+ totalAmount: session ? Math.round((0, import_shared6.buildCheckoutDisplayData)(session).total * 100) : 0,
1971
2244
  currency: session?.currency?.toLowerCase() ?? "usd",
1972
2245
  onComplete,
1973
2246
  onError,
2247
+ onDecline,
1974
2248
  showPayPal,
1975
2249
  showApplePay,
1976
2250
  showGooglePay,
@@ -1981,10 +2255,12 @@ function FloPayCheckout({
1981
2255
  cardBackButtonContent,
1982
2256
  cardTitleContent,
1983
2257
  onButtonClick,
2258
+ onBeforeButtonClick,
1984
2259
  enableAVS,
1985
2260
  avsLayout,
1986
2261
  country: session?.customer?.country,
1987
2262
  zip: session?.customer?.zip,
2263
+ initialCardOpen: deferredCardOpen,
1988
2264
  submitLabel,
1989
2265
  className
1990
2266
  }
@@ -2017,6 +2293,10 @@ function SessionInjector({
2017
2293
  }
2018
2294
  function InterimButtonsView({
2019
2295
  onButtonClick,
2296
+ onCardButtonClick,
2297
+ cardLoading = false,
2298
+ cardOpen,
2299
+ errorMessage,
2020
2300
  showPayPal,
2021
2301
  showApplePay,
2022
2302
  showGooglePay,
@@ -2027,8 +2307,14 @@ function InterimButtonsView({
2027
2307
  cardTitleContent
2028
2308
  }) {
2029
2309
  const [showCardForm, setShowCardForm] = (0, import_react7.useState)(false);
2310
+ const isCardOpenControlled = typeof cardOpen === "boolean";
2311
+ (0, import_react7.useEffect)(() => {
2312
+ if (isCardOpenControlled) {
2313
+ setShowCardForm(cardOpen);
2314
+ }
2315
+ }, [cardOpen, isCardOpenControlled]);
2030
2316
  const bStyles = (0, import_react7.useMemo)(() => {
2031
- const base = (0, import_shared5.resolveButtonsLayoutTheme)(buttonsTheme);
2317
+ const base = (0, import_shared6.resolveButtonsLayoutTheme)(buttonsTheme);
2032
2318
  if (!stylesOverride) return base;
2033
2319
  return {
2034
2320
  ...base,
@@ -2064,20 +2350,26 @@ function InterimButtonsView({
2064
2350
  "button",
2065
2351
  {
2066
2352
  type: "button",
2067
- onClick: () => setShowCardForm(false),
2353
+ onClick: () => {
2354
+ if (!isCardOpenControlled) {
2355
+ setShowCardForm(false);
2356
+ }
2357
+ },
2068
2358
  "aria-label": "Back to payment methods",
2359
+ disabled: isCardOpenControlled || cardLoading,
2069
2360
  style: {
2070
2361
  display: "inline-flex",
2071
2362
  alignItems: "center",
2072
2363
  gap: hideBackButtonLabel ? 0 : "0.5rem",
2073
2364
  background: "none",
2074
2365
  border: "none",
2075
- cursor: "pointer",
2076
2366
  color: "#4b5563",
2077
2367
  fontSize: "0.85rem",
2078
2368
  fontWeight: 500,
2079
2369
  padding: 0,
2080
2370
  flexShrink: 0,
2371
+ opacity: isCardOpenControlled || cardLoading ? 0.6 : 1,
2372
+ cursor: isCardOpenControlled || cardLoading ? "not-allowed" : "pointer",
2081
2373
  ...bStyles.backButton
2082
2374
  },
2083
2375
  children: [
@@ -2137,10 +2429,16 @@ function InterimButtonsView({
2137
2429
  "button",
2138
2430
  {
2139
2431
  type: "button",
2140
- onClick: () => {
2432
+ onClick: async () => {
2433
+ if (cardLoading) return;
2434
+ if (onCardButtonClick) {
2435
+ await onCardButtonClick();
2436
+ return;
2437
+ }
2141
2438
  onButtonClick?.("card");
2142
2439
  setShowCardForm(true);
2143
2440
  },
2441
+ disabled: cardLoading,
2144
2442
  style: {
2145
2443
  width: "100%",
2146
2444
  padding: "0.9rem 1rem",
@@ -2150,7 +2448,7 @@ function InterimButtonsView({
2150
2448
  borderRadius: "8px",
2151
2449
  fontSize: bStyles.cardButtonFontSize ?? "0.95rem",
2152
2450
  fontWeight: 600,
2153
- cursor: "pointer",
2451
+ cursor: cardLoading ? "not-allowed" : "pointer",
2154
2452
  display: "flex",
2155
2453
  alignItems: "center",
2156
2454
  justifyContent: "center",
@@ -2158,6 +2456,7 @@ function InterimButtonsView({
2158
2456
  boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
2159
2457
  transition: "transform 0.1s",
2160
2458
  position: "relative",
2459
+ opacity: cardLoading ? 0.6 : 1,
2161
2460
  ...bStyles.cardButton
2162
2461
  },
2163
2462
  onMouseDown: (e) => {
@@ -2169,12 +2468,29 @@ function InterimButtonsView({
2169
2468
  children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CardButtonContentSlot, { content: cardButtonContent })
2170
2469
  }
2171
2470
  ),
2471
+ errorMessage && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: {
2472
+ margin: "0.25rem 0",
2473
+ padding: "0.625rem 0.875rem",
2474
+ background: "#FEF2F2",
2475
+ border: "1px solid #FECACA",
2476
+ borderRadius: "8px",
2477
+ color: "#991B1B",
2478
+ fontSize: "0.85rem",
2479
+ fontWeight: 600,
2480
+ display: "flex",
2481
+ alignItems: "center",
2482
+ gap: "0.5rem",
2483
+ ...bStyles.errorBanner
2484
+ }, children: [
2485
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime5.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" }) }),
2486
+ errorMessage
2487
+ ] }),
2172
2488
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: `@keyframes flopay-interim-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }` })
2173
2489
  ] });
2174
2490
  }
2175
2491
 
2176
2492
  // src/checkout-form.tsx
2177
- var import_shared6 = require("@flopay/shared");
2493
+ var import_shared7 = require("@flopay/shared");
2178
2494
  var import_react8 = require("react");
2179
2495
  var import_jsx_runtime6 = require("react/jsx-runtime");
2180
2496
  var WALLET_RESUME_KEY2 = "flopay_wallet_resume";
@@ -2190,6 +2506,7 @@ function CheckoutFormInner({
2190
2506
  userId,
2191
2507
  onComplete,
2192
2508
  onError,
2509
+ onDecline,
2193
2510
  onTokenizedBody,
2194
2511
  layout = "auto",
2195
2512
  submitLabel = "Pay",
@@ -2221,6 +2538,12 @@ function CheckoutFormInner({
2221
2538
  },
2222
2539
  [onErrorChange]
2223
2540
  );
2541
+ const emitDecline = (0, import_react8.useCallback)(
2542
+ (input, overrides) => {
2543
+ onDecline?.(buildDeclineEvent("card", input, overrides));
2544
+ },
2545
+ [onDecline]
2546
+ );
2224
2547
  const processPaymentInternal = (0, import_react8.useCallback)(
2225
2548
  async (tokenizedBody) => {
2226
2549
  setProcessing(true);
@@ -2267,6 +2590,7 @@ function CheckoutFormInner({
2267
2590
  if (result.error) {
2268
2591
  updateError(result.error.message);
2269
2592
  onError?.(result.error);
2593
+ emitDecline(result.error);
2270
2594
  return;
2271
2595
  }
2272
2596
  if (result.status === "succeeded" || result.status === "processing") {
@@ -2301,13 +2625,17 @@ function CheckoutFormInner({
2301
2625
  }
2302
2626
  const errorMessage = json?.message ?? "Payment failed. Please try again.";
2303
2627
  updateError(errorMessage);
2628
+ emitDecline(errorMessage, {
2629
+ code: json?.code,
2630
+ declineCode: json?.declineCode ?? json?.gatewayDeclineReason
2631
+ });
2304
2632
  } catch (err) {
2305
2633
  updateError(err instanceof Error ? err.message : "An unexpected error occurred");
2306
2634
  } finally {
2307
2635
  setProcessing(false);
2308
2636
  }
2309
2637
  },
2310
- [baseUrl, sessionId, userId, email, firstName, lastName, chv, flopay, onComplete, onError, updateError]
2638
+ [baseUrl, sessionId, userId, email, firstName, lastName, chv, flopay, onComplete, onError, updateError, emitDecline]
2311
2639
  );
2312
2640
  const dispatchTokenizedBody = (0, import_react8.useCallback)(
2313
2641
  (tokenizedBody) => {
@@ -2331,6 +2659,7 @@ function CheckoutFormInner({
2331
2659
  if (result.error) {
2332
2660
  updateError(result.error.message);
2333
2661
  onError?.(result.error);
2662
+ emitDecline(result.error);
2334
2663
  } else if (result.status === "succeeded" || result.status === "processing") {
2335
2664
  dispatchTokenizedBody({
2336
2665
  id: result.paymentIntentId,
@@ -2344,7 +2673,7 @@ function CheckoutFormInner({
2344
2673
  setIs3DSActive(false);
2345
2674
  }
2346
2675
  }
2347
- }), [flopay, dispatchTokenizedBody, onError, updateError]);
2676
+ }), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
2348
2677
  (0, import_react8.useEffect)(() => {
2349
2678
  if (typeof window === "undefined") return;
2350
2679
  const stored = localStorage.getItem(WALLET_RESUME_KEY2);
@@ -2382,7 +2711,7 @@ function CheckoutFormInner({
2382
2711
  return;
2383
2712
  }
2384
2713
  if (!sessionId || !email) {
2385
- throw new import_shared6.FloPayError("Missing sessionId or email", "validation_error");
2714
+ throw new import_shared7.FloPayError("Missing sessionId or email", "validation_error");
2386
2715
  }
2387
2716
  const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
2388
2717
  method: "POST",
@@ -2394,16 +2723,18 @@ function CheckoutFormInner({
2394
2723
  isPaypal: false
2395
2724
  })
2396
2725
  });
2397
- if (!intentResponse.ok) throw new import_shared6.FloPayError("Failed to create payment intent", "api_error");
2726
+ if (!intentResponse.ok) throw new import_shared7.FloPayError("Failed to create payment intent", "api_error");
2398
2727
  const intentJson = await intentResponse.json();
2399
2728
  const intentClientSecret = intentJson.data?.id;
2400
- if (!intentClientSecret) throw new import_shared6.FloPayError("No client_secret in payment intent response", "api_error");
2729
+ if (!intentClientSecret) throw new import_shared7.FloPayError("No client_secret in payment intent response", "api_error");
2401
2730
  const confirmResult = await flopay.confirmCardPayment({
2402
2731
  clientSecret: intentClientSecret,
2403
2732
  paymentMethodId: pmResult.paymentMethodId
2404
2733
  });
2405
2734
  if (confirmResult.error) {
2406
2735
  updateError(confirmResult.error.message);
2736
+ onError?.(confirmResult.error);
2737
+ emitDecline(confirmResult.error);
2407
2738
  return;
2408
2739
  }
2409
2740
  dispatchTokenizedBody({
@@ -2420,7 +2751,7 @@ function CheckoutFormInner({
2420
2751
  }
2421
2752
  }
2422
2753
  },
2423
- [flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError]
2754
+ [flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError, emitDecline]
2424
2755
  );
2425
2756
  const isReady = flopay !== null && elements !== null;
2426
2757
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("form", { onSubmit: handleSubmit, className, style: { position: "relative" }, children: [
@@ -2452,7 +2783,7 @@ function CheckoutFormInner({
2452
2783
  }
2453
2784
 
2454
2785
  // src/paypal-button.tsx
2455
- var import_shared7 = require("@flopay/shared");
2786
+ var import_shared8 = require("@flopay/shared");
2456
2787
  var import_react9 = require("react");
2457
2788
  var import_jsx_runtime7 = require("react/jsx-runtime");
2458
2789
  function PayPalButton({
@@ -2572,7 +2903,7 @@ function PayPalButton({
2572
2903
  setSubmitting(true);
2573
2904
  onErrorChange?.(null);
2574
2905
  if (!sessionId || !email) {
2575
- throw new import_shared7.FloPayError("Missing sessionId or email for PayPal payment", "validation_error");
2906
+ throw new import_shared8.FloPayError("Missing sessionId or email for PayPal payment", "validation_error");
2576
2907
  }
2577
2908
  const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
2578
2909
  method: "POST",
@@ -2584,10 +2915,10 @@ function PayPalButton({
2584
2915
  isPaypal: "true"
2585
2916
  })
2586
2917
  });
2587
- if (!intentResponse.ok) throw new import_shared7.FloPayError("Failed to create payment intent", "api_error");
2918
+ if (!intentResponse.ok) throw new import_shared8.FloPayError("Failed to create payment intent", "api_error");
2588
2919
  const intentJson = await intentResponse.json();
2589
2920
  const intentClientSecret = intentJson.data?.id;
2590
- if (!intentClientSecret) throw new import_shared7.FloPayError("No client_secret in response", "api_error");
2921
+ if (!intentClientSecret) throw new import_shared8.FloPayError("No client_secret in response", "api_error");
2591
2922
  const result = await flopay.confirmPayment({
2592
2923
  clientSecret: intentClientSecret,
2593
2924
  returnUrl: window.location.href