@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.mjs CHANGED
@@ -69,7 +69,7 @@ function FloPayProvider({
69
69
  // src/flopay-checkout.tsx
70
70
  import React5, { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo3, useRef as useRef3, useState as useState3 } from "react";
71
71
  import { loadFloPay, PaymentAPI } from "@flopay/js";
72
- import { FloPayError as FloPayError2, resolveBillingApiUrl as resolveBillingApiUrl3, buildCheckoutDisplayData, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme2 } from "@flopay/shared";
72
+ import { FloPayError as FloPayError3, resolveBillingApiUrl as resolveBillingApiUrl3, buildCheckoutDisplayData, resolveButtonsLayoutTheme as resolveButtonsLayoutTheme2 } from "@flopay/shared";
73
73
 
74
74
  // src/card-button-content.tsx
75
75
  import "react";
@@ -227,8 +227,116 @@ function useBillingApiUrl() {
227
227
  return ctx.billingApiUrl || resolveBillingApiUrl2();
228
228
  }
229
229
 
230
- // src/split-card-form.tsx
230
+ // src/checkout-utils.ts
231
231
  import { FloPayError } from "@flopay/shared";
232
+ function mergeInlineSessionPatches(base, patch) {
233
+ if (!patch) return base;
234
+ if (!base) return patch;
235
+ return {
236
+ account: { ...base.account ?? {}, ...patch.account ?? {} },
237
+ couponCodes: patch.couponCodes ?? base.couponCodes,
238
+ tagsData: {
239
+ ...base.tagsData ?? {},
240
+ ...patch.tagsData ?? {}
241
+ },
242
+ utmMetadata: patch.utmMetadata ?? base.utmMetadata
243
+ };
244
+ }
245
+ function mergeInlineSessionPatch(params, patch) {
246
+ if (!patch) return params;
247
+ return {
248
+ ...params,
249
+ account: {
250
+ ...params.account,
251
+ ...patch.account ?? {}
252
+ },
253
+ couponCodes: patch.couponCodes ?? params.couponCodes,
254
+ tagsData: {
255
+ ...params.tagsData ?? {},
256
+ ...patch.tagsData ?? {}
257
+ },
258
+ utmMetadata: patch.utmMetadata ?? params.utmMetadata
259
+ };
260
+ }
261
+ function buildSyntheticSession(params, checkoutModeOverride) {
262
+ const totalAmount = [
263
+ ...(params.items ?? []).map((item) => item.overrideAmount ?? item.totalAmount ?? 0),
264
+ ...(params.subscriptions ?? []).map((subscription) => subscription.overrideAmount ?? subscription.totalAmount ?? 0)
265
+ ].reduce((sum, value) => sum + value, 0);
266
+ const currency = params.items?.[0]?.currency ?? params.subscriptions?.[0]?.currency ?? "USD";
267
+ return {
268
+ id: "",
269
+ clientSecret: "",
270
+ mode: "payment",
271
+ amount: Math.round(totalAmount * 100),
272
+ currency,
273
+ status: "open",
274
+ customer: {
275
+ id: params.account.userId,
276
+ email: params.account.email ?? "",
277
+ firstName: params.account.firstName,
278
+ lastName: params.account.lastName,
279
+ gender: params.account.gender ?? void 0,
280
+ city: params.account.city ?? void 0,
281
+ state: params.account.state ?? void 0,
282
+ country: params.account.country ?? void 0,
283
+ zip: params.account.zip ?? void 0
284
+ },
285
+ successUrl: params.successUrl,
286
+ cancelUrl: params.cancelUrl,
287
+ checkoutMode: params.checkoutMode ?? checkoutModeOverride ?? "full",
288
+ items: (params.items ?? []).map((item, idx) => ({
289
+ uuid: `synthetic-item-${idx}`,
290
+ checkoutSessionId: "",
291
+ providerItemId: item.providerItemId,
292
+ providerItemName: item.providerItemName ?? item.providerItemId,
293
+ quantity: item.quantity ?? 1,
294
+ totalAmount: item.totalAmount,
295
+ overrideAmount: item.overrideAmount ?? null,
296
+ currency: item.currency ?? currency
297
+ })),
298
+ subscriptions: (params.subscriptions ?? []).map((subscription, idx) => ({
299
+ uuid: `synthetic-sub-${idx}`,
300
+ checkoutSessionId: "",
301
+ providerPlanId: subscription.providerPlanId,
302
+ providerPlanName: subscription.providerPlanName ?? subscription.providerPlanId,
303
+ quantity: subscription.quantity ?? 1,
304
+ totalAmount: subscription.totalAmount,
305
+ overrideAmount: subscription.overrideAmount ?? null,
306
+ currency: subscription.currency ?? currency
307
+ }))
308
+ };
309
+ }
310
+ function ensureInlineSessionReady(params) {
311
+ if (!params.account.email?.trim()) {
312
+ throw new FloPayError(
313
+ "Email is required before continuing with card checkout.",
314
+ "validation_error",
315
+ { param: "createSession.account.email" }
316
+ );
317
+ }
318
+ }
319
+ function mergeAccountPatch(base, patch) {
320
+ if (!patch) return base;
321
+ return {
322
+ ...base,
323
+ ...patch
324
+ };
325
+ }
326
+ function buildDeclineEvent(method, input, overrides) {
327
+ const message = typeof input === "string" ? input : input.message;
328
+ const code = typeof input === "string" ? overrides?.code : overrides?.code ?? input.code;
329
+ const declineCode = typeof input === "string" ? overrides?.declineCode : overrides?.declineCode ?? input.declineCode;
330
+ return {
331
+ method,
332
+ message,
333
+ ...code ? { code } : {},
334
+ ...declineCode ? { declineCode } : {}
335
+ };
336
+ }
337
+
338
+ // src/split-card-form.tsx
339
+ import { FloPayError as FloPayError2 } from "@flopay/shared";
232
340
  import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
233
341
  var WALLET_RESUME_KEY = "flopay_wallet_resume";
234
342
  var FLOPAY_KEYFRAMES = `
@@ -366,7 +474,8 @@ function PayPalButtonInner({
366
474
  onTokenizedBody,
367
475
  onErrorChange,
368
476
  isProcessing = false,
369
- onButtonClick
477
+ onButtonClick,
478
+ onDecline
370
479
  }) {
371
480
  const stripe = useStripeRaw();
372
481
  const elements = useStripeElements();
@@ -386,7 +495,9 @@ function PayPalButtonInner({
386
495
  try {
387
496
  setSubmitting(true);
388
497
  if (redirectStatus === "failed") {
389
- onErrorChange?.("PayPal payment was declined. Please try again.");
498
+ const message = "PayPal payment was declined. Please try again.";
499
+ onErrorChange?.(message);
500
+ onDecline?.(buildDeclineEvent("paypal", message));
390
501
  return;
391
502
  }
392
503
  const { paymentIntent, error } = await stripe.retrievePaymentIntent(clientSecret);
@@ -408,7 +519,9 @@ function PayPalButtonInner({
408
519
  url.searchParams.delete("redirect_status");
409
520
  window.history.replaceState({}, "", url.toString());
410
521
  } else {
411
- onErrorChange?.("PayPal payment was not completed. Please try again.");
522
+ const message = "PayPal payment was not completed. Please try again.";
523
+ onErrorChange?.(message);
524
+ onDecline?.(buildDeclineEvent("paypal", message));
412
525
  }
413
526
  } catch (err) {
414
527
  onErrorChange?.(err instanceof Error ? err.message : "Failed to complete PayPal payment.");
@@ -416,7 +529,7 @@ function PayPalButtonInner({
416
529
  setSubmitting(false);
417
530
  }
418
531
  })();
419
- }, [stripe, onTokenizedBody, onErrorChange]);
532
+ }, [stripe, onTokenizedBody, onErrorChange, onDecline]);
420
533
  const handlePayPalConfirm = useCallback(async (_event) => {
421
534
  if (!stripe || !elements) return;
422
535
  onButtonClick?.("paypal");
@@ -457,7 +570,11 @@ function PayPalButtonInner({
457
570
  redirect: "if_required"
458
571
  });
459
572
  if (confirmError) {
460
- onErrorChange?.(confirmError.message ?? "PayPal payment failed.");
573
+ const message = confirmError.message ?? "PayPal payment failed.";
574
+ onErrorChange?.(message);
575
+ onDecline?.(buildDeclineEvent("paypal", message, {
576
+ code: confirmError.code
577
+ }));
461
578
  return;
462
579
  }
463
580
  const confirmedPmId = typeof paymentIntent?.payment_method === "string" ? paymentIntent.payment_method : paymentIntent?.payment_method?.id;
@@ -472,7 +589,7 @@ function PayPalButtonInner({
472
589
  } finally {
473
590
  setSubmitting(false);
474
591
  }
475
- }, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]);
592
+ }, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline]);
476
593
  return /* @__PURE__ */ jsxs2(Fragment2, { children: [
477
594
  /* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(
478
595
  ExpressCheckoutElement,
@@ -481,6 +598,9 @@ function PayPalButtonInner({
481
598
  onLoadError: () => {
482
599
  },
483
600
  onConfirm: handlePayPalConfirm,
601
+ onCancel: () => {
602
+ onDecline?.(buildDeclineEvent("paypal", "PayPal checkout was cancelled."));
603
+ },
484
604
  options: {
485
605
  buttonType: { paypal: "paypal" },
486
606
  paymentMethods: {
@@ -503,13 +623,15 @@ function WalletButtonInner({
503
623
  showGooglePay = true,
504
624
  onTokenizedBody,
505
625
  onErrorChange,
506
- onButtonClick
626
+ onButtonClick,
627
+ onDecline
507
628
  }) {
508
629
  const stripe = useStripeRaw();
509
630
  const elements = useStripeElements();
510
631
  const [ready, setReady] = useState2(false);
511
632
  const [submitting, setSubmitting] = useState2(false);
512
633
  const baseUrl = billingApiUrl.replace(/\/+$/, "");
634
+ const lastWalletMethodRef = useRef2("card");
513
635
  const handleWalletConfirm = useCallback(
514
636
  async (_event) => {
515
637
  if (!stripe || !elements) return;
@@ -550,7 +672,12 @@ function WalletButtonInner({
550
672
  { payment_method: paymentMethod.id }
551
673
  );
552
674
  if (confirmError) {
553
- onErrorChange?.(confirmError.message ?? "Wallet payment failed.");
675
+ const method = walletType === "apple_pay" ? "apple_pay" : "google_pay";
676
+ const message = confirmError.message ?? "Wallet payment failed.";
677
+ onErrorChange?.(message);
678
+ onDecline?.(buildDeclineEvent(method, message, {
679
+ code: confirmError.code
680
+ }));
554
681
  return;
555
682
  }
556
683
  onTokenizedBody({
@@ -564,7 +691,7 @@ function WalletButtonInner({
564
691
  setSubmitting(false);
565
692
  }
566
693
  },
567
- [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]
694
+ [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange, onDecline]
568
695
  );
569
696
  return /* @__PURE__ */ jsxs2(Fragment2, { children: [
570
697
  /* @__PURE__ */ jsx4("div", { children: /* @__PURE__ */ jsx4(
@@ -573,7 +700,14 @@ function WalletButtonInner({
573
700
  onReady: () => setReady(true),
574
701
  onLoadError: () => {
575
702
  },
703
+ onClick: (event) => {
704
+ lastWalletMethodRef.current = event.expressPaymentType === "apple_pay" ? "apple_pay" : "google_pay";
705
+ event.resolve();
706
+ },
576
707
  onConfirm: handleWalletConfirm,
708
+ onCancel: () => {
709
+ onDecline?.(buildDeclineEvent(lastWalletMethodRef.current, "Wallet checkout was cancelled."));
710
+ },
577
711
  options: {
578
712
  buttonType: { applePay: "plain", googlePay: "plain" },
579
713
  paymentMethods: {
@@ -598,6 +732,7 @@ function SplitCardFormInner({
598
732
  userId,
599
733
  onComplete,
600
734
  onError,
735
+ onDecline,
601
736
  onTokenizedBody,
602
737
  firstName,
603
738
  lastName,
@@ -620,6 +755,7 @@ function SplitCardFormInner({
620
755
  cardBackButtonContent,
621
756
  cardTitleContent,
622
757
  onButtonClick,
758
+ onBeforeButtonClick,
623
759
  enableAVS = false,
624
760
  avsLayout: avsLayoutProp = "row",
625
761
  country: countryProp,
@@ -628,6 +764,7 @@ function SplitCardFormInner({
628
764
  onZipChange,
629
765
  totalAmount = 0,
630
766
  currency = "usd",
767
+ initialCardOpen = false,
631
768
  innerRef
632
769
  }) {
633
770
  const flopay = useFloPay();
@@ -638,9 +775,10 @@ function SplitCardFormInner({
638
775
  const [is3DSActive, setIs3DSActive] = useState2(false);
639
776
  const [selectedCountry, setSelectedCountry] = useState2(countryProp ?? "US");
640
777
  const [zipCode, setZipCode] = useState2(zipProp ?? "");
778
+ const [accountPatch, setAccountPatch] = useState2({});
641
779
  const zipCodeRef = useRef2(zipProp ?? "");
642
780
  const selectedCountryRef = useRef2(countryProp ?? "US");
643
- const [viewState, setViewState] = useState2("buttons");
781
+ const [viewState, setViewState] = useState2(initialCardOpen ? "card" : "buttons");
644
782
  const showCardForm = viewState === "expanding" || viewState === "card";
645
783
  const TRANSITION_MS = 280;
646
784
  const expandToCard = useCallback(() => {
@@ -651,6 +789,11 @@ function SplitCardFormInner({
651
789
  setViewState("collapsing");
652
790
  setTimeout(() => setViewState("buttons"), TRANSITION_MS);
653
791
  }, []);
792
+ useEffect3(() => {
793
+ if (layout === "buttons" && initialCardOpen) {
794
+ setViewState("card");
795
+ }
796
+ }, [layout, initialCardOpen]);
654
797
  const [fullName, setFullName] = useState2("");
655
798
  const [formReady, setFormReady] = useState2(false);
656
799
  const [overlayStatus, setOverlayStatus] = useState2(null);
@@ -674,6 +817,14 @@ function SplitCardFormInner({
674
817
  const isSubmitting = externalProcessing ?? processing;
675
818
  const isSelfContained = !onTokenizedBody;
676
819
  const baseUrl = resolvedBillingApiUrl.replace(/\/+$/, "");
820
+ const resolvedAccount = useMemo2(() => mergeAccountPatch({
821
+ userId,
822
+ email,
823
+ firstName,
824
+ lastName,
825
+ country: countryProp,
826
+ zip: zipProp
827
+ }, accountPatch), [userId, email, firstName, lastName, countryProp, zipProp, accountPatch]);
677
828
  const stripeInstance = useMemo2(() => {
678
829
  if (!flopay) return null;
679
830
  return flopay.getRawProvider();
@@ -699,6 +850,12 @@ function SplitCardFormInner({
699
850
  },
700
851
  [onErrorChange]
701
852
  );
853
+ const emitDecline = useCallback(
854
+ (method, input, overrides) => {
855
+ onDecline?.(buildDeclineEvent(method, input, overrides));
856
+ },
857
+ [onDecline]
858
+ );
702
859
  const showWallets = showApplePay || showGooglePay;
703
860
  const handleNameChange = useCallback((value) => {
704
861
  setFullName(value);
@@ -706,6 +863,30 @@ function SplitCardFormInner({
706
863
  onFirstNameChange?.(parts[0] ?? "");
707
864
  onLastNameChange?.(parts.length > 1 ? parts.slice(1).join(" ") : "");
708
865
  }, [onFirstNameChange, onLastNameChange]);
866
+ const runBeforeCardButtonClick = useCallback(async () => {
867
+ if (!onBeforeButtonClick) return true;
868
+ try {
869
+ const result = await onBeforeButtonClick({
870
+ method: "card",
871
+ sessionId: sessionId || void 0
872
+ });
873
+ if (result === false) {
874
+ return false;
875
+ }
876
+ if (result?.account) {
877
+ setAccountPatch((prev) => ({ ...prev ?? {}, ...result.account }));
878
+ }
879
+ return true;
880
+ } catch (err) {
881
+ const floPayErr = err instanceof FloPayError2 ? err : new FloPayError2(
882
+ err instanceof Error ? err.message : "Card before-click hook failed.",
883
+ "validation_error"
884
+ );
885
+ updateError(floPayErr.message);
886
+ onError?.(floPayErr);
887
+ return false;
888
+ }
889
+ }, [onBeforeButtonClick, sessionId, updateError, onError]);
709
890
  const processPaymentInternal = useCallback(
710
891
  async (tokenizedBody) => {
711
892
  if (processingRef.current) return;
@@ -718,16 +899,16 @@ function SplitCardFormInner({
718
899
  method: "POST",
719
900
  headers: {
720
901
  "Content-Type": "application/json",
721
- "x-user-id": userId ?? ""
902
+ "x-user-id": resolvedAccount.userId ?? ""
722
903
  },
723
904
  body: JSON.stringify({
724
905
  sessionId,
725
906
  tokenizedData: tokenizedBody,
726
907
  accountData: {
727
- userId: userId ?? "",
728
- email: email ?? "",
729
- firstName: firstName ?? fullName.trim().split(/\s+/)[0] ?? "",
730
- lastName: lastName ?? fullName.trim().split(/\s+/).slice(1).join(" ") ?? "",
908
+ userId: resolvedAccount.userId ?? "",
909
+ email: resolvedAccount.email ?? "",
910
+ firstName: resolvedAccount.firstName ?? fullName.trim().split(/\s+/)[0] ?? "",
911
+ lastName: resolvedAccount.lastName ?? fullName.trim().split(/\s+/).slice(1).join(" ") ?? "",
731
912
  ...enableAVS ? { zip: zipCodeRef.current, country: selectedCountryRef.current } : {}
732
913
  },
733
914
  chv
@@ -760,6 +941,7 @@ function SplitCardFormInner({
760
941
  setOverlayStatus("error");
761
942
  updateError(result.error.message);
762
943
  onError?.(result.error);
944
+ emitDecline("card", result.error);
763
945
  return;
764
946
  }
765
947
  if (result.status === "succeeded" || result.status === "processing") {
@@ -802,7 +984,11 @@ function SplitCardFormInner({
802
984
  });
803
985
  if (confirmError) {
804
986
  setOverlayStatus("error");
805
- updateError(confirmError.message ?? "PayPal payment failed.");
987
+ const message2 = confirmError.message ?? "PayPal payment failed.";
988
+ updateError(message2);
989
+ emitDecline("paypal", message2, {
990
+ code: confirmError.code
991
+ });
806
992
  }
807
993
  } catch (err) {
808
994
  setOverlayStatus("error");
@@ -811,7 +997,12 @@ function SplitCardFormInner({
811
997
  return;
812
998
  }
813
999
  setOverlayStatus("error");
814
- updateError(json?.message ?? "Payment failed. Please try again.");
1000
+ const message = json?.message ?? "Payment failed. Please try again.";
1001
+ updateError(message);
1002
+ emitDecline(tokenizedBody.isPaypal ? "paypal" : "card", message, {
1003
+ code: json?.code ?? json?.gatewayErrorCode,
1004
+ declineCode: json?.declineCode ?? json?.gatewayDeclineReason
1005
+ });
815
1006
  await new Promise((r) => setTimeout(r, 1500));
816
1007
  } catch (err) {
817
1008
  setOverlayStatus("error");
@@ -823,7 +1014,7 @@ function SplitCardFormInner({
823
1014
  processingRef.current = false;
824
1015
  }
825
1016
  },
826
- [baseUrl, sessionId, userId, email, firstName, lastName, fullName, chv, flopay, onComplete, onError, updateError]
1017
+ [baseUrl, sessionId, resolvedAccount, fullName, chv, flopay, onComplete, onError, updateError, emitDecline]
827
1018
  );
828
1019
  const dispatchTokenizedBody = useCallback(
829
1020
  (tokenizedBody) => {
@@ -847,6 +1038,7 @@ function SplitCardFormInner({
847
1038
  if (result.error) {
848
1039
  updateError(result.error.message);
849
1040
  onError?.(result.error);
1041
+ emitDecline("card", result.error);
850
1042
  } else if (result.status === "succeeded" || result.status === "processing") {
851
1043
  dispatchTokenizedBody({
852
1044
  id: result.paymentIntentId,
@@ -860,7 +1052,7 @@ function SplitCardFormInner({
860
1052
  setIs3DSActive(false);
861
1053
  }
862
1054
  }
863
- }), [flopay, dispatchTokenizedBody, onError, updateError]);
1055
+ }), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
864
1056
  useEffect3(() => {
865
1057
  if (typeof window === "undefined") return;
866
1058
  const stored = localStorage.getItem(WALLET_RESUME_KEY);
@@ -883,7 +1075,9 @@ function SplitCardFormInner({
883
1075
  async (e) => {
884
1076
  e.preventDefault();
885
1077
  if (!flopay || !elements || isSubmitting || processingRef.current) return;
886
- onButtonClick?.("card");
1078
+ if (layout !== "buttons") {
1079
+ onButtonClick?.("card");
1080
+ }
887
1081
  setProcessing(true);
888
1082
  setOverlayStatus("processing");
889
1083
  updateError(null);
@@ -911,23 +1105,23 @@ function SplitCardFormInner({
911
1105
  updateError(pmResult.error?.message ?? "Failed to create payment method.");
912
1106
  return;
913
1107
  }
914
- if (!sessionId || !email) {
915
- throw new FloPayError("Missing sessionId or email", "validation_error");
1108
+ if (!sessionId || !resolvedAccount.email) {
1109
+ throw new FloPayError2("Missing sessionId or email", "validation_error");
916
1110
  }
917
1111
  const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
918
1112
  method: "POST",
919
1113
  headers: { "Content-Type": "application/json" },
920
1114
  body: JSON.stringify({
921
1115
  sessionId,
922
- email,
1116
+ email: resolvedAccount.email,
923
1117
  paymentMethodType: pmResult.paymentMethodId,
924
1118
  isPaypal: false
925
1119
  })
926
1120
  });
927
- if (!intentResponse.ok) throw new FloPayError("Failed to create payment intent", "api_error");
1121
+ if (!intentResponse.ok) throw new FloPayError2("Failed to create payment intent", "api_error");
928
1122
  const intentJson = await intentResponse.json();
929
1123
  const intentClientSecret = intentJson.data?.id;
930
- if (!intentClientSecret) throw new FloPayError("No client_secret in payment intent response", "api_error");
1124
+ if (!intentClientSecret) throw new FloPayError2("No client_secret in payment intent response", "api_error");
931
1125
  const confirmResult = await flopay.confirmCardPayment({
932
1126
  clientSecret: intentClientSecret,
933
1127
  paymentMethodId: pmResult.paymentMethodId
@@ -935,6 +1129,8 @@ function SplitCardFormInner({
935
1129
  if (confirmResult.error) {
936
1130
  setOverlayStatus("error");
937
1131
  updateError(confirmResult.error.message);
1132
+ onError?.(confirmResult.error);
1133
+ emitDecline("card", confirmResult.error);
938
1134
  await new Promise((r) => setTimeout(r, 1500));
939
1135
  return;
940
1136
  }
@@ -955,7 +1151,7 @@ function SplitCardFormInner({
955
1151
  }
956
1152
  }
957
1153
  },
958
- [flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError]
1154
+ [flopay, elements, isSubmitting, sessionId, resolvedAccount.email, baseUrl, isSelfContained, dispatchTokenizedBody, onButtonClick, onError, updateError, emitDecline, layout]
959
1155
  );
960
1156
  const isReady = flopay !== null && elements !== null;
961
1157
  if (!isReady) {
@@ -1240,32 +1436,36 @@ function SplitCardFormInner({
1240
1436
  PayPalButtonInner,
1241
1437
  {
1242
1438
  sessionId,
1243
- email,
1439
+ email: resolvedAccount.email,
1244
1440
  billingApiUrl: resolvedBillingApiUrl,
1245
1441
  onTokenizedBody: dispatchTokenizedBody,
1246
1442
  onErrorChange: updateError,
1247
1443
  isProcessing: isSubmitting,
1248
- onButtonClick
1444
+ onButtonClick,
1445
+ onDecline
1249
1446
  }
1250
1447
  ) }) : showPayPal ? /* @__PURE__ */ jsx4("div", { style: { height: 44, borderRadius: 8, background: "#e5e7eb", animation: "flopay-pulse 1.5s ease-in-out infinite" } }) : null,
1251
1448
  showWallets && stripeInstance ? /* @__PURE__ */ jsx4(StripeElements, { stripe: stripeInstance, options: walletOptions, children: /* @__PURE__ */ jsx4(
1252
1449
  WalletButtonInner,
1253
1450
  {
1254
1451
  sessionId,
1255
- email,
1452
+ email: resolvedAccount.email,
1256
1453
  billingApiUrl: resolvedBillingApiUrl,
1257
1454
  showApplePay,
1258
1455
  showGooglePay,
1259
1456
  onTokenizedBody: dispatchTokenizedBody,
1260
1457
  onErrorChange: updateError,
1261
- onButtonClick
1458
+ onButtonClick,
1459
+ onDecline
1262
1460
  }
1263
1461
  ) }) : showWallets ? /* @__PURE__ */ jsx4("div", { style: { height: 44, borderRadius: 8, background: "#e5e7eb", animation: "flopay-pulse 1.5s ease-in-out infinite" } }) : null,
1264
1462
  /* @__PURE__ */ jsx4(
1265
1463
  "button",
1266
1464
  {
1267
1465
  type: "button",
1268
- onClick: () => {
1466
+ onClick: async () => {
1467
+ const shouldContinue = await runBeforeCardButtonClick();
1468
+ if (!shouldContinue) return;
1269
1469
  onButtonClick?.("card");
1270
1470
  expandToCard();
1271
1471
  },
@@ -1330,23 +1530,25 @@ function SplitCardFormInner({
1330
1530
  WalletButtonInner,
1331
1531
  {
1332
1532
  sessionId,
1333
- email,
1533
+ email: resolvedAccount.email,
1334
1534
  billingApiUrl: resolvedBillingApiUrl,
1335
1535
  showApplePay,
1336
1536
  showGooglePay,
1337
1537
  onTokenizedBody: dispatchTokenizedBody,
1338
- onErrorChange: updateError
1538
+ onErrorChange: updateError,
1539
+ onDecline
1339
1540
  }
1340
1541
  ) }),
1341
1542
  showPayPal && stripeInstance && /* @__PURE__ */ jsx4(StripeElements, { stripe: stripeInstance, options: paypalOptions, children: /* @__PURE__ */ jsx4(
1342
1543
  PayPalButtonInner,
1343
1544
  {
1344
1545
  sessionId,
1345
- email,
1546
+ email: resolvedAccount.email,
1346
1547
  billingApiUrl: resolvedBillingApiUrl,
1347
1548
  onTokenizedBody: dispatchTokenizedBody,
1348
1549
  onErrorChange: updateError,
1349
- isProcessing: isSubmitting
1550
+ isProcessing: isSubmitting,
1551
+ onDecline
1350
1552
  }
1351
1553
  ) }),
1352
1554
  (showWallets && stripeInstance || showPayPal && stripeInstance) && /* @__PURE__ */ jsxs2("div", { style: {
@@ -1378,6 +1580,7 @@ function FloPayCheckout({
1378
1580
  error: errorNode,
1379
1581
  onComplete,
1380
1582
  onError,
1583
+ onDecline,
1381
1584
  showPayPal = true,
1382
1585
  showApplePay = true,
1383
1586
  showGooglePay = true,
@@ -1388,6 +1591,7 @@ function FloPayCheckout({
1388
1591
  cardBackButtonContent,
1389
1592
  cardTitleContent,
1390
1593
  onButtonClick,
1594
+ onBeforeButtonClick,
1391
1595
  enableAVS,
1392
1596
  avsLayout,
1393
1597
  submitLabel,
@@ -1409,13 +1613,28 @@ function FloPayCheckout({
1409
1613
  const [currentMode, setCurrentMode] = useState3("full");
1410
1614
  const [confirmProcessing, setConfirmProcessing] = useState3(false);
1411
1615
  const [modeError, setModeError] = useState3(null);
1616
+ const [createSessionPatch, setCreateSessionPatch] = useState3(void 0);
1617
+ const [cardBootstrapPending, setCardBootstrapPending] = useState3(false);
1618
+ const [deferredCardOpen, setDeferredCardOpen] = useState3(false);
1412
1619
  const autoCheckoutAttempted = useRef3(false);
1413
1620
  const onCompleteRef = useRef3(onComplete);
1414
1621
  onCompleteRef.current = onComplete;
1415
1622
  const onErrorRef = useRef3(onError);
1416
1623
  onErrorRef.current = onError;
1624
+ const onDeclineRef = useRef3(onDecline);
1625
+ onDeclineRef.current = onDecline;
1417
1626
  const onSessionCompletedRef = useRef3(onSessionCompleted);
1418
1627
  onSessionCompletedRef.current = onSessionCompleted;
1628
+ const effectiveCreateSession = useMemo3(
1629
+ () => createSessionParams ? mergeInlineSessionPatch(createSessionParams, createSessionPatch) : void 0,
1630
+ [createSessionParams, createSessionPatch]
1631
+ );
1632
+ const emitDecline = useCallback2(
1633
+ (method, input, overrides) => {
1634
+ onDeclineRef.current?.(buildDeclineEvent(method, input, overrides));
1635
+ },
1636
+ []
1637
+ );
1419
1638
  const processPaymentForMode = useCallback2(
1420
1639
  async (sess) => {
1421
1640
  const baseUrl = resolvedBillingUrl.replace(/\/+$/, "");
@@ -1455,9 +1674,13 @@ function FloPayCheckout({
1455
1674
  // No client secret available
1456
1675
  };
1457
1676
  }
1458
- throw new FloPayError2(
1677
+ throw new FloPayError3(
1459
1678
  json?.message ?? "Payment failed. Please try again.",
1460
- "api_error"
1679
+ "api_error",
1680
+ {
1681
+ code: json?.code ?? json?.gatewayErrorCode,
1682
+ declineCode: json?.declineCode ?? json?.gatewayDeclineReason
1683
+ }
1461
1684
  );
1462
1685
  },
1463
1686
  [resolvedBillingUrl, resolvedSessionId]
@@ -1476,6 +1699,9 @@ function FloPayCheckout({
1476
1699
  });
1477
1700
  if (nextActionError) {
1478
1701
  setModeError(nextActionError.message ?? "3DS authentication failed.");
1702
+ emitDecline("card", nextActionError.message ?? "3DS authentication failed.", {
1703
+ code: nextActionError.code
1704
+ });
1479
1705
  return false;
1480
1706
  }
1481
1707
  if (paymentIntent && (paymentIntent.status === "requires_capture" || paymentIntent.status === "succeeded")) {
@@ -1498,6 +1724,9 @@ function FloPayCheckout({
1498
1724
  });
1499
1725
  if (error) {
1500
1726
  setModeError(error.message ?? "PayPal authorization failed.");
1727
+ emitDecline("paypal", error.message ?? "PayPal authorization failed.", {
1728
+ code: error.code
1729
+ });
1501
1730
  return false;
1502
1731
  }
1503
1732
  onCompleteRef.current?.({ status: "succeeded" });
@@ -1505,16 +1734,24 @@ function FloPayCheckout({
1505
1734
  }
1506
1735
  return false;
1507
1736
  },
1508
- [resolvedBillingUrl, resolvedSessionId]
1737
+ [resolvedBillingUrl, resolvedSessionId, emitDecline]
1509
1738
  );
1510
1739
  const inflightRef = useRef3(/* @__PURE__ */ new Map());
1511
1740
  const initializedHashRef = useRef3(null);
1741
+ const deferInlineSessionUntilCardClick = Boolean(
1742
+ effectiveCreateSession && !children && layout === "buttons" && onBeforeButtonClick && (effectiveCreateSession.checkoutMode ?? checkoutModeProp ?? "full") === "full"
1743
+ );
1512
1744
  function hashCreateParams(params) {
1513
1745
  const key = JSON.stringify({
1514
1746
  c: params?.clientId,
1747
+ successUrl: params?.successUrl,
1748
+ cancelUrl: params?.cancelUrl,
1515
1749
  i: params?.items?.map((x) => `${x.providerItemId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
1516
1750
  s: params?.subscriptions?.map((x) => `${x.providerPlanId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
1517
- e: params?.account.email,
1751
+ account: params?.account,
1752
+ couponCodes: params?.couponCodes,
1753
+ tagsData: params?.tagsData,
1754
+ utmMetadata: params?.utmMetadata,
1518
1755
  m: params?.checkoutMode ?? "full"
1519
1756
  });
1520
1757
  let h = 0;
@@ -1524,138 +1761,167 @@ function FloPayCheckout({
1524
1761
  return `flopay_session_${Math.abs(h).toString(36)}`;
1525
1762
  }
1526
1763
  const createSessionHash = useMemo3(
1527
- () => createSessionParams ? hashCreateParams(createSessionParams) : "",
1528
- // eslint-disable-next-line react-hooks/exhaustive-deps
1529
- [
1530
- createSessionParams?.clientId,
1531
- createSessionParams?.account?.email,
1532
- createSessionParams?.checkoutMode,
1533
- JSON.stringify(createSessionParams?.items),
1534
- JSON.stringify(createSessionParams?.subscriptions)
1535
- ]
1764
+ () => effectiveCreateSession ? hashCreateParams(effectiveCreateSession) : "",
1765
+ [effectiveCreateSession]
1536
1766
  );
1537
- const createSessionParamsRef = useRef3(createSessionParams);
1538
- createSessionParamsRef.current = createSessionParams;
1767
+ const createSessionParamsRef = useRef3(effectiveCreateSession);
1768
+ createSessionParamsRef.current = effectiveCreateSession;
1769
+ useEffect4(() => {
1770
+ setResolvedSessionId(sessionIdProp ?? "");
1771
+ }, [sessionIdProp]);
1772
+ async function resolveInlineSession(params, cacheKey) {
1773
+ ensureInlineSessionReady(params);
1774
+ const api = new PaymentAPI(resolvedBillingUrl);
1775
+ let sid = typeof window !== "undefined" ? window.sessionStorage.getItem(cacheKey) : null;
1776
+ let realResult = null;
1777
+ if (sid) {
1778
+ try {
1779
+ realResult = await api.getUnifiedCheckoutSession(sid);
1780
+ if (realResult.data.session?.status === "complete") {
1781
+ if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1782
+ sid = null;
1783
+ realResult = null;
1784
+ }
1785
+ } catch {
1786
+ if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1787
+ sid = null;
1788
+ }
1789
+ }
1790
+ if (!sid) {
1791
+ realResult = await api.createAndFetchSession(params);
1792
+ sid = realResult.data.session?.id ?? "";
1793
+ if (sid && typeof window !== "undefined") {
1794
+ window.sessionStorage.setItem(cacheKey, sid);
1795
+ }
1796
+ }
1797
+ return { sid: sid ?? "", result: realResult };
1798
+ }
1799
+ const bootstrapInlineSession = useCallback2(
1800
+ async (patch) => {
1801
+ const baseParams = createSessionParamsRef.current;
1802
+ if (!baseParams) {
1803
+ throw new FloPayError3("createSession is required to bootstrap checkout.", "validation_error");
1804
+ }
1805
+ const mergedParams = mergeInlineSessionPatch(baseParams, patch);
1806
+ const cacheKey = hashCreateParams(mergedParams);
1807
+ let promise = inflightRef.current.get(cacheKey);
1808
+ if (!promise) {
1809
+ promise = resolveInlineSession(mergedParams, cacheKey);
1810
+ inflightRef.current.set(cacheKey, promise);
1811
+ }
1812
+ let resolved;
1813
+ try {
1814
+ resolved = await promise;
1815
+ } finally {
1816
+ inflightRef.current.delete(cacheKey);
1817
+ }
1818
+ if (patch) {
1819
+ setCreateSessionPatch((prev) => mergeInlineSessionPatches(prev, patch));
1820
+ }
1821
+ const { sid, result: realResult } = resolved;
1822
+ setUnified(realResult);
1823
+ if (realResult.data.session) {
1824
+ setSession(realResult.data.session);
1825
+ }
1826
+ if (sid) {
1827
+ setResolvedSessionId(sid);
1828
+ }
1829
+ let publishableKey;
1830
+ if (realResult.provider === "stripe") {
1831
+ publishableKey = realResult.data.stripe?.publishableKey;
1832
+ }
1833
+ if (!publishableKey) publishableKey = fallbackPublishableKey;
1834
+ if (!publishableKey) {
1835
+ throw new FloPayError3(
1836
+ "No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
1837
+ "validation_error"
1838
+ );
1839
+ }
1840
+ const instance = await loadFloPay(publishableKey, {
1841
+ billingApiUrl: resolvedBillingUrl,
1842
+ locale
1843
+ });
1844
+ flopayRef.current = instance;
1845
+ setFloPay(instance);
1846
+ initializedHashRef.current = cacheKey;
1847
+ setIsLoading(false);
1848
+ return resolved;
1849
+ },
1850
+ [fallbackPublishableKey, locale, resolvedBillingUrl]
1851
+ );
1852
+ const handleDeferredCardButtonClick = useCallback2(async () => {
1853
+ if (cardBootstrapPending) return;
1854
+ setLoadError(null);
1855
+ setCardBootstrapPending(true);
1856
+ try {
1857
+ const beforeClickResult = await onBeforeButtonClick?.({
1858
+ method: "card",
1859
+ createSession: effectiveCreateSession
1860
+ });
1861
+ if (beforeClickResult === false) {
1862
+ setDeferredCardOpen(false);
1863
+ return;
1864
+ }
1865
+ const patch = beforeClickResult && typeof beforeClickResult === "object" ? beforeClickResult : void 0;
1866
+ const baseParams = createSessionParamsRef.current;
1867
+ if (!baseParams) {
1868
+ throw new FloPayError3(
1869
+ "createSession is required to bootstrap checkout.",
1870
+ "validation_error"
1871
+ );
1872
+ }
1873
+ ensureInlineSessionReady(mergeInlineSessionPatch(baseParams, patch));
1874
+ setDeferredCardOpen(true);
1875
+ onButtonClick?.("card");
1876
+ await bootstrapInlineSession(patch);
1877
+ } catch (err) {
1878
+ setDeferredCardOpen(false);
1879
+ const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(
1880
+ err instanceof Error ? err.message : "Failed to start card checkout.",
1881
+ "api_error"
1882
+ );
1883
+ setLoadError(floPayErr);
1884
+ onErrorRef.current?.(floPayErr);
1885
+ } finally {
1886
+ setCardBootstrapPending(false);
1887
+ }
1888
+ }, [
1889
+ bootstrapInlineSession,
1890
+ cardBootstrapPending,
1891
+ effectiveCreateSession,
1892
+ onButtonClick,
1893
+ onBeforeButtonClick
1894
+ ]);
1539
1895
  useEffect4(() => {
1540
1896
  let cancelled = false;
1541
1897
  setLoadError(null);
1542
1898
  if (createSessionHash) {
1899
+ if (deferInlineSessionUntilCardClick) {
1900
+ if (initializedHashRef.current !== createSessionHash) {
1901
+ setSession(null);
1902
+ setUnified(null);
1903
+ setFloPay(null);
1904
+ setResolvedSessionId("");
1905
+ setDeferredCardOpen(false);
1906
+ flopayRef.current = null;
1907
+ }
1908
+ setCurrentMode(createSessionParamsRef.current?.checkoutMode ?? checkoutModeProp ?? "full");
1909
+ setIsLoading(false);
1910
+ return () => {
1911
+ cancelled = true;
1912
+ };
1913
+ }
1543
1914
  if (initializedHashRef.current === createSessionHash) return;
1544
1915
  const params = createSessionParamsRef.current;
1545
- const totalAmount = [
1546
- ...(params.items ?? []).map((i) => i.overrideAmount ?? i.totalAmount ?? 0),
1547
- ...(params.subscriptions ?? []).map((s) => s.overrideAmount ?? s.totalAmount ?? 0)
1548
- ].reduce((sum, v) => sum + v, 0);
1549
- const currency = params.items?.[0]?.currency ?? params.subscriptions?.[0]?.currency ?? "USD";
1550
- const syntheticSession = {
1551
- id: "",
1552
- clientSecret: "",
1553
- mode: "payment",
1554
- amount: Math.round(totalAmount * 100),
1555
- currency,
1556
- status: "open",
1557
- customer: {
1558
- id: params.account.userId,
1559
- email: params.account.email,
1560
- firstName: params.account.firstName,
1561
- lastName: params.account.lastName
1562
- },
1563
- successUrl: params.successUrl,
1564
- cancelUrl: params.cancelUrl,
1565
- checkoutMode: params.checkoutMode ?? "full",
1566
- items: (params.items ?? []).map((i, idx) => ({
1567
- uuid: `synthetic-item-${idx}`,
1568
- checkoutSessionId: "",
1569
- providerItemId: i.providerItemId,
1570
- providerItemName: i.providerItemName ?? i.providerItemId,
1571
- quantity: i.quantity ?? 1,
1572
- totalAmount: i.totalAmount,
1573
- overrideAmount: i.overrideAmount ?? null,
1574
- currency: i.currency ?? currency
1575
- })),
1576
- subscriptions: (params.subscriptions ?? []).map((s, idx) => ({
1577
- uuid: `synthetic-sub-${idx}`,
1578
- checkoutSessionId: "",
1579
- providerPlanId: s.providerPlanId,
1580
- providerPlanName: s.providerPlanName ?? s.providerPlanId,
1581
- quantity: s.quantity ?? 1,
1582
- totalAmount: s.totalAmount,
1583
- overrideAmount: s.overrideAmount ?? null,
1584
- currency: s.currency ?? currency
1585
- }))
1586
- };
1587
- setSession(syntheticSession);
1916
+ setSession(buildSyntheticSession(params, checkoutModeProp));
1588
1917
  setCurrentMode(params.checkoutMode ?? checkoutModeProp ?? "full");
1589
1918
  setIsLoading(false);
1590
- const cacheKey = createSessionHash;
1591
- async function resolveSession() {
1592
- const api = new PaymentAPI(resolvedBillingUrl);
1593
- let sid = typeof window !== "undefined" ? window.sessionStorage.getItem(cacheKey) : null;
1594
- let realResult = null;
1595
- if (sid) {
1596
- try {
1597
- realResult = await api.getUnifiedCheckoutSession(sid);
1598
- if (realResult.data.session?.status === "complete") {
1599
- if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1600
- sid = null;
1601
- realResult = null;
1602
- }
1603
- } catch {
1604
- if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1605
- sid = null;
1606
- }
1607
- }
1608
- if (!sid) {
1609
- realResult = await api.createAndFetchSession(createSessionParamsRef.current);
1610
- sid = realResult.data.session?.id ?? "";
1611
- if (sid && typeof window !== "undefined") {
1612
- window.sessionStorage.setItem(cacheKey, sid);
1613
- }
1614
- }
1615
- return { sid: sid ?? "", result: realResult };
1616
- }
1617
1919
  (async () => {
1618
1920
  try {
1619
- let promise = inflightRef.current.get(cacheKey);
1620
- if (!promise) {
1621
- promise = resolveSession();
1622
- inflightRef.current.set(cacheKey, promise);
1623
- }
1624
- let resolved;
1625
- try {
1626
- resolved = await promise;
1627
- } finally {
1628
- inflightRef.current.delete(cacheKey);
1629
- }
1630
- if (cancelled) return;
1631
- const { sid, result: realResult } = resolved;
1632
- if (realResult) {
1633
- setUnified(realResult);
1634
- if (realResult.data.session) setSession(realResult.data.session);
1635
- }
1636
- if (sid) {
1637
- setResolvedSessionId(sid);
1638
- }
1639
- let pk;
1640
- if (realResult?.provider === "stripe") {
1641
- pk = realResult.data.stripe?.publishableKey;
1642
- }
1643
- if (!pk) pk = fallbackPublishableKey;
1644
- if (!pk) {
1645
- throw new FloPayError2(
1646
- "No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
1647
- "validation_error"
1648
- );
1649
- }
1650
- const instance = await loadFloPay(pk, { billingApiUrl: resolvedBillingUrl, locale });
1651
- if (!cancelled) {
1652
- flopayRef.current = instance;
1653
- setFloPay(instance);
1654
- initializedHashRef.current = cacheKey;
1655
- }
1921
+ await bootstrapInlineSession();
1656
1922
  } catch (err) {
1657
1923
  if (cancelled) return;
1658
- const floPayErr = err instanceof FloPayError2 ? err : new FloPayError2(err instanceof Error ? err.message : "Failed to create session", "api_error");
1924
+ const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(err instanceof Error ? err.message : "Failed to create session", "api_error");
1659
1925
  setLoadError(floPayErr);
1660
1926
  }
1661
1927
  })();
@@ -1673,7 +1939,7 @@ function FloPayCheckout({
1673
1939
  const sess = result.data.session ?? null;
1674
1940
  setSession(sess);
1675
1941
  if (!sess) {
1676
- throw new FloPayError2("No session data returned", "api_error");
1942
+ throw new FloPayError3("No session data returned", "api_error");
1677
1943
  }
1678
1944
  if (sess.status === "complete") {
1679
1945
  setIsLoading(false);
@@ -1712,7 +1978,7 @@ function FloPayCheckout({
1712
1978
  if (!cancelled) setIsLoading(false);
1713
1979
  } catch (err) {
1714
1980
  if (cancelled) return;
1715
- const floPayErr = err instanceof FloPayError2 ? err : new FloPayError2(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
1981
+ const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
1716
1982
  setLoadError(floPayErr);
1717
1983
  setIsLoading(false);
1718
1984
  }
@@ -1724,7 +1990,7 @@ function FloPayCheckout({
1724
1990
  }
1725
1991
  if (!publishableKey) publishableKey = fallbackPublishableKey;
1726
1992
  if (!publishableKey) {
1727
- throw new FloPayError2(
1993
+ throw new FloPayError3(
1728
1994
  "No publishable key found in session response. Provide a fallbackPublishableKey prop or ensure the session includes gatewayData.publishableKey.",
1729
1995
  "validation_error"
1730
1996
  );
@@ -1740,7 +2006,7 @@ function FloPayCheckout({
1740
2006
  return () => {
1741
2007
  cancelled = true;
1742
2008
  };
1743
- }, [resolvedSessionId, createSessionHash, resolvedBillingUrl, fallbackPublishableKey, locale, checkoutModeProp]);
2009
+ }, [resolvedSessionId, createSessionHash, checkoutModeProp, deferInlineSessionUntilCardClick, bootstrapInlineSession]);
1744
2010
  const handleConfirmCheckout = useCallback2(async () => {
1745
2011
  if (confirmProcessing || !session) return;
1746
2012
  setConfirmProcessing(true);
@@ -1755,17 +2021,18 @@ function FloPayCheckout({
1755
2021
  setCurrentMode("full");
1756
2022
  }
1757
2023
  } catch (err) {
1758
- const floPayErr = err instanceof FloPayError2 ? err : new FloPayError2(
2024
+ const floPayErr = err instanceof FloPayError3 ? err : new FloPayError3(
1759
2025
  err instanceof Error ? err.message : "Payment failed",
1760
2026
  "api_error"
1761
2027
  );
1762
2028
  setModeError(floPayErr.message);
1763
2029
  onError?.(floPayErr);
2030
+ emitDecline("card", floPayErr);
1764
2031
  setCurrentMode("full");
1765
2032
  } finally {
1766
2033
  setConfirmProcessing(false);
1767
2034
  }
1768
- }, [confirmProcessing, session, processPaymentForMode, handleRedirectResult, onError]);
2035
+ }, [confirmProcessing, session, processPaymentForMode, handleRedirectResult, onError, emitDecline]);
1769
2036
  const providerOptions = useMemo3(() => {
1770
2037
  if (!unified || !session) return void 0;
1771
2038
  const opts = {
@@ -1791,6 +2058,8 @@ function FloPayCheckout({
1791
2058
  }),
1792
2059
  [session, isLoading, loadError, currentMode]
1793
2060
  );
2061
+ const shouldShowInterimButtons = Boolean(createSessionParams) && layout === "buttons" && (!flopay || !providerOptions);
2062
+ const shouldKeepDeferredInterimVisible = shouldShowInterimButtons && deferInlineSessionUntilCardClick;
1794
2063
  if (isLoading) {
1795
2064
  if (loadingNode) return /* @__PURE__ */ jsx5(Fragment3, { children: loadingNode });
1796
2065
  if (layout === "buttons") {
@@ -1819,7 +2088,7 @@ function FloPayCheckout({
1819
2088
  /* @__PURE__ */ jsx5("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
1820
2089
  ] });
1821
2090
  }
1822
- if (loadError) {
2091
+ if (loadError && !shouldKeepDeferredInterimVisible) {
1823
2092
  if (errorNode) return /* @__PURE__ */ jsx5(Fragment3, { children: errorNode(loadError) });
1824
2093
  return /* @__PURE__ */ jsx5(
1825
2094
  "div",
@@ -1834,14 +2103,18 @@ function FloPayCheckout({
1834
2103
  }
1835
2104
  );
1836
2105
  }
1837
- if ((!flopay || !providerOptions) && createSessionParams && layout === "buttons") {
2106
+ if (shouldShowInterimButtons) {
1838
2107
  return /* @__PURE__ */ jsx5(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ jsx5(
1839
2108
  InterimButtonsView,
1840
2109
  {
1841
2110
  onButtonClick,
1842
- showPayPal,
1843
- showApplePay,
1844
- showGooglePay,
2111
+ onCardButtonClick: deferInlineSessionUntilCardClick ? handleDeferredCardButtonClick : void 0,
2112
+ cardLoading: cardBootstrapPending,
2113
+ cardOpen: deferInlineSessionUntilCardClick ? deferredCardOpen : void 0,
2114
+ errorMessage: shouldKeepDeferredInterimVisible ? loadError?.message ?? null : null,
2115
+ showPayPal: deferInlineSessionUntilCardClick ? false : showPayPal,
2116
+ showApplePay: deferInlineSessionUntilCardClick ? false : showApplePay,
2117
+ showGooglePay: deferInlineSessionUntilCardClick ? false : showGooglePay,
1845
2118
  buttonsTheme,
1846
2119
  buttonsStyles,
1847
2120
  cardButtonContent,
@@ -1927,6 +2200,7 @@ function FloPayCheckout({
1927
2200
  currency: session?.currency?.toLowerCase() ?? "usd",
1928
2201
  onComplete,
1929
2202
  onError,
2203
+ onDecline,
1930
2204
  showPayPal,
1931
2205
  showApplePay,
1932
2206
  showGooglePay,
@@ -1937,10 +2211,12 @@ function FloPayCheckout({
1937
2211
  cardBackButtonContent,
1938
2212
  cardTitleContent,
1939
2213
  onButtonClick,
2214
+ onBeforeButtonClick,
1940
2215
  enableAVS,
1941
2216
  avsLayout,
1942
2217
  country: session?.customer?.country,
1943
2218
  zip: session?.customer?.zip,
2219
+ initialCardOpen: deferredCardOpen,
1944
2220
  submitLabel,
1945
2221
  className
1946
2222
  }
@@ -1973,6 +2249,10 @@ function SessionInjector({
1973
2249
  }
1974
2250
  function InterimButtonsView({
1975
2251
  onButtonClick,
2252
+ onCardButtonClick,
2253
+ cardLoading = false,
2254
+ cardOpen,
2255
+ errorMessage,
1976
2256
  showPayPal,
1977
2257
  showApplePay,
1978
2258
  showGooglePay,
@@ -1983,6 +2263,12 @@ function InterimButtonsView({
1983
2263
  cardTitleContent
1984
2264
  }) {
1985
2265
  const [showCardForm, setShowCardForm] = useState3(false);
2266
+ const isCardOpenControlled = typeof cardOpen === "boolean";
2267
+ useEffect4(() => {
2268
+ if (isCardOpenControlled) {
2269
+ setShowCardForm(cardOpen);
2270
+ }
2271
+ }, [cardOpen, isCardOpenControlled]);
1986
2272
  const bStyles = useMemo3(() => {
1987
2273
  const base = resolveButtonsLayoutTheme2(buttonsTheme);
1988
2274
  if (!stylesOverride) return base;
@@ -2020,20 +2306,26 @@ function InterimButtonsView({
2020
2306
  "button",
2021
2307
  {
2022
2308
  type: "button",
2023
- onClick: () => setShowCardForm(false),
2309
+ onClick: () => {
2310
+ if (!isCardOpenControlled) {
2311
+ setShowCardForm(false);
2312
+ }
2313
+ },
2024
2314
  "aria-label": "Back to payment methods",
2315
+ disabled: isCardOpenControlled || cardLoading,
2025
2316
  style: {
2026
2317
  display: "inline-flex",
2027
2318
  alignItems: "center",
2028
2319
  gap: hideBackButtonLabel ? 0 : "0.5rem",
2029
2320
  background: "none",
2030
2321
  border: "none",
2031
- cursor: "pointer",
2032
2322
  color: "#4b5563",
2033
2323
  fontSize: "0.85rem",
2034
2324
  fontWeight: 500,
2035
2325
  padding: 0,
2036
2326
  flexShrink: 0,
2327
+ opacity: isCardOpenControlled || cardLoading ? 0.6 : 1,
2328
+ cursor: isCardOpenControlled || cardLoading ? "not-allowed" : "pointer",
2037
2329
  ...bStyles.backButton
2038
2330
  },
2039
2331
  children: [
@@ -2093,10 +2385,16 @@ function InterimButtonsView({
2093
2385
  "button",
2094
2386
  {
2095
2387
  type: "button",
2096
- onClick: () => {
2388
+ onClick: async () => {
2389
+ if (cardLoading) return;
2390
+ if (onCardButtonClick) {
2391
+ await onCardButtonClick();
2392
+ return;
2393
+ }
2097
2394
  onButtonClick?.("card");
2098
2395
  setShowCardForm(true);
2099
2396
  },
2397
+ disabled: cardLoading,
2100
2398
  style: {
2101
2399
  width: "100%",
2102
2400
  padding: "0.9rem 1rem",
@@ -2106,7 +2404,7 @@ function InterimButtonsView({
2106
2404
  borderRadius: "8px",
2107
2405
  fontSize: bStyles.cardButtonFontSize ?? "0.95rem",
2108
2406
  fontWeight: 600,
2109
- cursor: "pointer",
2407
+ cursor: cardLoading ? "not-allowed" : "pointer",
2110
2408
  display: "flex",
2111
2409
  alignItems: "center",
2112
2410
  justifyContent: "center",
@@ -2114,6 +2412,7 @@ function InterimButtonsView({
2114
2412
  boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
2115
2413
  transition: "transform 0.1s",
2116
2414
  position: "relative",
2415
+ opacity: cardLoading ? 0.6 : 1,
2117
2416
  ...bStyles.cardButton
2118
2417
  },
2119
2418
  onMouseDown: (e) => {
@@ -2125,12 +2424,29 @@ function InterimButtonsView({
2125
2424
  children: /* @__PURE__ */ jsx5(CardButtonContentSlot, { content: cardButtonContent })
2126
2425
  }
2127
2426
  ),
2427
+ errorMessage && /* @__PURE__ */ jsxs3("div", { style: {
2428
+ margin: "0.25rem 0",
2429
+ padding: "0.625rem 0.875rem",
2430
+ background: "#FEF2F2",
2431
+ border: "1px solid #FECACA",
2432
+ borderRadius: "8px",
2433
+ color: "#991B1B",
2434
+ fontSize: "0.85rem",
2435
+ fontWeight: 600,
2436
+ display: "flex",
2437
+ alignItems: "center",
2438
+ gap: "0.5rem",
2439
+ ...bStyles.errorBanner
2440
+ }, children: [
2441
+ /* @__PURE__ */ jsx5("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ jsx5("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" }) }),
2442
+ errorMessage
2443
+ ] }),
2128
2444
  /* @__PURE__ */ jsx5("style", { children: `@keyframes flopay-interim-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }` })
2129
2445
  ] });
2130
2446
  }
2131
2447
 
2132
2448
  // src/checkout-form.tsx
2133
- import { FloPayError as FloPayError3 } from "@flopay/shared";
2449
+ import { FloPayError as FloPayError4 } from "@flopay/shared";
2134
2450
  import { forwardRef as forwardRef2, useCallback as useCallback3, useEffect as useEffect5, useImperativeHandle as useImperativeHandle2, useState as useState4 } from "react";
2135
2451
  import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
2136
2452
  var WALLET_RESUME_KEY2 = "flopay_wallet_resume";
@@ -2146,6 +2462,7 @@ function CheckoutFormInner({
2146
2462
  userId,
2147
2463
  onComplete,
2148
2464
  onError,
2465
+ onDecline,
2149
2466
  onTokenizedBody,
2150
2467
  layout = "auto",
2151
2468
  submitLabel = "Pay",
@@ -2177,6 +2494,12 @@ function CheckoutFormInner({
2177
2494
  },
2178
2495
  [onErrorChange]
2179
2496
  );
2497
+ const emitDecline = useCallback3(
2498
+ (input, overrides) => {
2499
+ onDecline?.(buildDeclineEvent("card", input, overrides));
2500
+ },
2501
+ [onDecline]
2502
+ );
2180
2503
  const processPaymentInternal = useCallback3(
2181
2504
  async (tokenizedBody) => {
2182
2505
  setProcessing(true);
@@ -2223,6 +2546,7 @@ function CheckoutFormInner({
2223
2546
  if (result.error) {
2224
2547
  updateError(result.error.message);
2225
2548
  onError?.(result.error);
2549
+ emitDecline(result.error);
2226
2550
  return;
2227
2551
  }
2228
2552
  if (result.status === "succeeded" || result.status === "processing") {
@@ -2257,13 +2581,17 @@ function CheckoutFormInner({
2257
2581
  }
2258
2582
  const errorMessage = json?.message ?? "Payment failed. Please try again.";
2259
2583
  updateError(errorMessage);
2584
+ emitDecline(errorMessage, {
2585
+ code: json?.code,
2586
+ declineCode: json?.declineCode ?? json?.gatewayDeclineReason
2587
+ });
2260
2588
  } catch (err) {
2261
2589
  updateError(err instanceof Error ? err.message : "An unexpected error occurred");
2262
2590
  } finally {
2263
2591
  setProcessing(false);
2264
2592
  }
2265
2593
  },
2266
- [baseUrl, sessionId, userId, email, firstName, lastName, chv, flopay, onComplete, onError, updateError]
2594
+ [baseUrl, sessionId, userId, email, firstName, lastName, chv, flopay, onComplete, onError, updateError, emitDecline]
2267
2595
  );
2268
2596
  const dispatchTokenizedBody = useCallback3(
2269
2597
  (tokenizedBody) => {
@@ -2287,6 +2615,7 @@ function CheckoutFormInner({
2287
2615
  if (result.error) {
2288
2616
  updateError(result.error.message);
2289
2617
  onError?.(result.error);
2618
+ emitDecline(result.error);
2290
2619
  } else if (result.status === "succeeded" || result.status === "processing") {
2291
2620
  dispatchTokenizedBody({
2292
2621
  id: result.paymentIntentId,
@@ -2300,7 +2629,7 @@ function CheckoutFormInner({
2300
2629
  setIs3DSActive(false);
2301
2630
  }
2302
2631
  }
2303
- }), [flopay, dispatchTokenizedBody, onError, updateError]);
2632
+ }), [flopay, dispatchTokenizedBody, onError, updateError, emitDecline]);
2304
2633
  useEffect5(() => {
2305
2634
  if (typeof window === "undefined") return;
2306
2635
  const stored = localStorage.getItem(WALLET_RESUME_KEY2);
@@ -2338,7 +2667,7 @@ function CheckoutFormInner({
2338
2667
  return;
2339
2668
  }
2340
2669
  if (!sessionId || !email) {
2341
- throw new FloPayError3("Missing sessionId or email", "validation_error");
2670
+ throw new FloPayError4("Missing sessionId or email", "validation_error");
2342
2671
  }
2343
2672
  const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
2344
2673
  method: "POST",
@@ -2350,16 +2679,18 @@ function CheckoutFormInner({
2350
2679
  isPaypal: false
2351
2680
  })
2352
2681
  });
2353
- if (!intentResponse.ok) throw new FloPayError3("Failed to create payment intent", "api_error");
2682
+ if (!intentResponse.ok) throw new FloPayError4("Failed to create payment intent", "api_error");
2354
2683
  const intentJson = await intentResponse.json();
2355
2684
  const intentClientSecret = intentJson.data?.id;
2356
- if (!intentClientSecret) throw new FloPayError3("No client_secret in payment intent response", "api_error");
2685
+ if (!intentClientSecret) throw new FloPayError4("No client_secret in payment intent response", "api_error");
2357
2686
  const confirmResult = await flopay.confirmCardPayment({
2358
2687
  clientSecret: intentClientSecret,
2359
2688
  paymentMethodId: pmResult.paymentMethodId
2360
2689
  });
2361
2690
  if (confirmResult.error) {
2362
2691
  updateError(confirmResult.error.message);
2692
+ onError?.(confirmResult.error);
2693
+ emitDecline(confirmResult.error);
2363
2694
  return;
2364
2695
  }
2365
2696
  dispatchTokenizedBody({
@@ -2376,7 +2707,7 @@ function CheckoutFormInner({
2376
2707
  }
2377
2708
  }
2378
2709
  },
2379
- [flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError]
2710
+ [flopay, elements, isSubmitting, sessionId, email, baseUrl, isSelfContained, dispatchTokenizedBody, onError, updateError, emitDecline]
2380
2711
  );
2381
2712
  const isReady = flopay !== null && elements !== null;
2382
2713
  return /* @__PURE__ */ jsxs4("form", { onSubmit: handleSubmit, className, style: { position: "relative" }, children: [
@@ -2408,7 +2739,7 @@ function CheckoutFormInner({
2408
2739
  }
2409
2740
 
2410
2741
  // src/paypal-button.tsx
2411
- import { FloPayError as FloPayError4 } from "@flopay/shared";
2742
+ import { FloPayError as FloPayError5 } from "@flopay/shared";
2412
2743
  import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef4, useState as useState5 } from "react";
2413
2744
  import { Fragment as Fragment5, jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
2414
2745
  function PayPalButton({
@@ -2528,7 +2859,7 @@ function PayPalButton({
2528
2859
  setSubmitting(true);
2529
2860
  onErrorChange?.(null);
2530
2861
  if (!sessionId || !email) {
2531
- throw new FloPayError4("Missing sessionId or email for PayPal payment", "validation_error");
2862
+ throw new FloPayError5("Missing sessionId or email for PayPal payment", "validation_error");
2532
2863
  }
2533
2864
  const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
2534
2865
  method: "POST",
@@ -2540,10 +2871,10 @@ function PayPalButton({
2540
2871
  isPaypal: "true"
2541
2872
  })
2542
2873
  });
2543
- if (!intentResponse.ok) throw new FloPayError4("Failed to create payment intent", "api_error");
2874
+ if (!intentResponse.ok) throw new FloPayError5("Failed to create payment intent", "api_error");
2544
2875
  const intentJson = await intentResponse.json();
2545
2876
  const intentClientSecret = intentJson.data?.id;
2546
- if (!intentClientSecret) throw new FloPayError4("No client_secret in response", "api_error");
2877
+ if (!intentClientSecret) throw new FloPayError5("No client_secret in response", "api_error");
2547
2878
  const result = await flopay.confirmPayment({
2548
2879
  clientSecret: intentClientSecret,
2549
2880
  returnUrl: window.location.href