@flopay/react 0.1.5 → 0.2.1

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_react6 = __toESM(require("react"), 1);
120
120
  var import_js = require("@flopay/js");
121
- var import_shared4 = require("@flopay/shared");
121
+ var import_shared5 = require("@flopay/shared");
122
122
 
123
123
  // src/elements.tsx
124
124
  var import_react3 = require("react");
@@ -182,6 +182,7 @@ var AddressElement = createElementComponent("address", "AddressElement");
182
182
 
183
183
  // src/split-card-form.tsx
184
184
  var import_react_stripe_js = require("@stripe/react-stripe-js");
185
+ var import_shared3 = require("@flopay/shared");
185
186
  var import_react5 = require("react");
186
187
 
187
188
  // src/hooks.ts
@@ -204,10 +205,33 @@ function useBillingApiUrl() {
204
205
  }
205
206
 
206
207
  // src/split-card-form.tsx
207
- var import_shared3 = require("@flopay/shared");
208
+ var import_shared4 = require("@flopay/shared");
208
209
  var import_jsx_runtime3 = require("react/jsx-runtime");
209
210
  var WALLET_RESUME_KEY = "flopay_wallet_resume";
210
- function ProcessingOverlay({ status }) {
211
+ var FLOPAY_KEYFRAMES = `
212
+ @keyframes flopay-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
213
+ @keyframes flopay-fade-in { 0% { opacity: 0; } 100% { opacity: 1; } }
214
+ @keyframes flopay-buttons-enter {
215
+ 0% { opacity: 0; transform: translateX(-16px) scale(0.97); }
216
+ 100% { opacity: 1; transform: translateX(0) scale(1); }
217
+ }
218
+ @keyframes flopay-buttons-exit {
219
+ 0% { opacity: 1; transform: translateX(0) scale(1); }
220
+ 100% { opacity: 0; transform: translateX(-16px) scale(0.97); }
221
+ }
222
+ @keyframes flopay-card-enter {
223
+ 0% { opacity: 0; transform: translateX(16px) scale(0.97); }
224
+ 100% { opacity: 1; transform: translateX(0) scale(1); }
225
+ }
226
+ @keyframes flopay-card-exit {
227
+ 0% { opacity: 1; transform: translateX(0) scale(1); }
228
+ 100% { opacity: 0; transform: translateX(16px) scale(0.97); }
229
+ }
230
+ `;
231
+ function FloPayKeyframes() {
232
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: FLOPAY_KEYFRAMES });
233
+ }
234
+ function ProcessingOverlay({ status, errorMessage }) {
211
235
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
212
236
  position: "fixed",
213
237
  inset: 0,
@@ -283,6 +307,14 @@ function ProcessingOverlay({ status }) {
283
307
  status === "success" && "PAYMENT SUCCESSFUL",
284
308
  status === "error" && "PAYMENT FAILED"
285
309
  ] }),
310
+ status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: {
311
+ fontSize: 13,
312
+ color: "#6b7280",
313
+ fontWeight: 400,
314
+ maxWidth: 260,
315
+ lineHeight: 1.4,
316
+ margin: 0
317
+ }, children: errorMessage }),
286
318
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: `
287
319
  @keyframes flopay-spin { to { transform: rotate(360deg); } }
288
320
  @keyframes flopay-pop { 0% { transform: scale(0); } 100% { transform: scale(1); } }
@@ -302,7 +334,8 @@ function PayPalButtonInner({
302
334
  billingApiUrl,
303
335
  onTokenizedBody,
304
336
  onErrorChange,
305
- isProcessing = false
337
+ isProcessing = false,
338
+ onButtonClick
306
339
  }) {
307
340
  const stripe = (0, import_react_stripe_js.useStripe)();
308
341
  const elements = (0, import_react_stripe_js.useElements)();
@@ -355,6 +388,7 @@ function PayPalButtonInner({
355
388
  }, [stripe, onTokenizedBody, onErrorChange]);
356
389
  const handlePayPalConfirm = (0, import_react5.useCallback)(async (_event) => {
357
390
  if (!stripe || !elements) return;
391
+ onButtonClick?.("paypal");
358
392
  try {
359
393
  setSubmitting(true);
360
394
  onErrorChange?.(null);
@@ -409,7 +443,7 @@ function PayPalButtonInner({
409
443
  }
410
444
  }, [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]);
411
445
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
412
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginBottom: ready ? "0.5rem" : 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
446
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
413
447
  import_react_stripe_js.ExpressCheckoutElement,
414
448
  {
415
449
  onReady: () => setReady(true),
@@ -437,7 +471,8 @@ function WalletButtonInner({
437
471
  showApplePay = true,
438
472
  showGooglePay = true,
439
473
  onTokenizedBody,
440
- onErrorChange
474
+ onErrorChange,
475
+ onButtonClick
441
476
  }) {
442
477
  const stripe = (0, import_react_stripe_js.useStripe)();
443
478
  const elements = (0, import_react_stripe_js.useElements)();
@@ -447,6 +482,8 @@ function WalletButtonInner({
447
482
  const handleWalletConfirm = (0, import_react5.useCallback)(
448
483
  async (_event) => {
449
484
  if (!stripe || !elements) return;
485
+ const walletType = _event.expressPaymentType;
486
+ onButtonClick?.(walletType === "apple_pay" ? "apple_pay" : "google_pay");
450
487
  try {
451
488
  setSubmitting(true);
452
489
  onErrorChange?.(null);
@@ -499,7 +536,7 @@ function WalletButtonInner({
499
536
  [stripe, elements, sessionId, email, baseUrl, onTokenizedBody, onErrorChange]
500
537
  );
501
538
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
502
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { marginBottom: ready ? "0.5rem" : 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
539
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
503
540
  import_react_stripe_js.ExpressCheckoutElement,
504
541
  {
505
542
  onReady: () => setReady(true),
@@ -509,10 +546,12 @@ function WalletButtonInner({
509
546
  options: {
510
547
  buttonType: { applePay: "plain", googlePay: "plain" },
511
548
  paymentMethods: {
512
- applePay: showApplePay ? "auto" : "never",
513
- googlePay: showGooglePay ? "auto" : "never",
549
+ applePay: showApplePay ? "always" : "never",
550
+ googlePay: showGooglePay ? "always" : "never",
514
551
  paypal: "never",
515
- link: "never"
552
+ link: "never",
553
+ amazonPay: "never",
554
+ klarna: "never"
516
555
  },
517
556
  layout: { overflow: "never" }
518
557
  }
@@ -543,6 +582,10 @@ function SplitCardFormInner({
543
582
  showPayPal = true,
544
583
  showApplePay = true,
545
584
  showGooglePay = true,
585
+ layout = "default",
586
+ buttonsTheme,
587
+ buttonsStyles: buttonsStylesOverride,
588
+ onButtonClick,
546
589
  totalAmount = 0,
547
590
  currency = "usd",
548
591
  innerRef
@@ -553,12 +596,37 @@ function SplitCardFormInner({
553
596
  const [processing, setProcessing] = (0, import_react5.useState)(false);
554
597
  const [error, setError] = (0, import_react5.useState)(null);
555
598
  const [is3DSActive, setIs3DSActive] = (0, import_react5.useState)(false);
599
+ const [viewState, setViewState] = (0, import_react5.useState)("buttons");
600
+ const showCardForm = viewState === "expanding" || viewState === "card";
601
+ const TRANSITION_MS = 280;
602
+ const expandToCard = (0, import_react5.useCallback)(() => {
603
+ setViewState("expanding");
604
+ setTimeout(() => setViewState("card"), TRANSITION_MS);
605
+ }, []);
606
+ const collapseToButtons = (0, import_react5.useCallback)(() => {
607
+ setViewState("collapsing");
608
+ setTimeout(() => setViewState("buttons"), TRANSITION_MS);
609
+ }, []);
556
610
  const [fullName, setFullName] = (0, import_react5.useState)("");
557
611
  const [formReady, setFormReady] = (0, import_react5.useState)(false);
558
612
  const [overlayStatus, setOverlayStatus] = (0, import_react5.useState)(null);
559
613
  const processingRef = (0, import_react5.useRef)(false);
560
614
  const resolvedBillingApiUrl = billingApiUrl || contextBillingUrl;
561
615
  const displayError = externalError ?? error;
616
+ const bStyles = (0, import_react5.useMemo)(() => {
617
+ const base = (0, import_shared3.resolveButtonsLayoutTheme)(buttonsTheme);
618
+ if (!buttonsStylesOverride) return base;
619
+ return {
620
+ ...base,
621
+ ...buttonsStylesOverride,
622
+ cardButton: { ...base.cardButton, ...buttonsStylesOverride.cardButton },
623
+ cardFormContainer: { ...base.cardFormContainer, ...buttonsStylesOverride.cardFormContainer },
624
+ backButton: { ...base.backButton, ...buttonsStylesOverride.backButton },
625
+ backButtonIcon: { ...base.backButtonIcon, ...buttonsStylesOverride.backButtonIcon },
626
+ submitButton: { ...base.submitButton, ...buttonsStylesOverride.submitButton },
627
+ title: { ...base.title, ...buttonsStylesOverride.title }
628
+ };
629
+ }, [buttonsTheme, buttonsStylesOverride]);
562
630
  const isSubmitting = externalProcessing ?? processing;
563
631
  const isSelfContained = !onTokenizedBody;
564
632
  const baseUrl = resolvedBillingApiUrl.replace(/\/+$/, "");
@@ -770,6 +838,7 @@ function SplitCardFormInner({
770
838
  async (e) => {
771
839
  e.preventDefault();
772
840
  if (!flopay || !elements || isSubmitting || processingRef.current) return;
841
+ onButtonClick?.("card");
773
842
  setProcessing(true);
774
843
  setOverlayStatus("processing");
775
844
  updateError(null);
@@ -787,7 +856,7 @@ function SplitCardFormInner({
787
856
  return;
788
857
  }
789
858
  if (!sessionId || !email) {
790
- throw new import_shared3.FloPayError("Missing sessionId or email", "validation_error");
859
+ throw new import_shared4.FloPayError("Missing sessionId or email", "validation_error");
791
860
  }
792
861
  const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
793
862
  method: "POST",
@@ -799,10 +868,10 @@ function SplitCardFormInner({
799
868
  isPaypal: false
800
869
  })
801
870
  });
802
- if (!intentResponse.ok) throw new import_shared3.FloPayError("Failed to create payment intent", "api_error");
871
+ if (!intentResponse.ok) throw new import_shared4.FloPayError("Failed to create payment intent", "api_error");
803
872
  const intentJson = await intentResponse.json();
804
873
  const intentClientSecret = intentJson.data?.id;
805
- if (!intentClientSecret) throw new import_shared3.FloPayError("No client_secret in payment intent response", "api_error");
874
+ if (!intentClientSecret) throw new import_shared4.FloPayError("No client_secret in payment intent response", "api_error");
806
875
  const confirmResult = await flopay.confirmCardPayment({
807
876
  clientSecret: intentClientSecret,
808
877
  paymentMethodId: pmResult.paymentMethodId
@@ -836,8 +905,299 @@ function SplitCardFormInner({
836
905
  if (!isReady) {
837
906
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { "data-testid": "flopay-loading", "aria-busy": "true", children: "Loading payment form..." });
838
907
  }
908
+ const isButtons = layout === "buttons";
909
+ const resolvedBorder = isButtons ? bStyles.cardInputBorder ?? "#e5e7eb" : "#A4A4FF";
910
+ const cardBg = isButtons ? bStyles.cardFormContainer?.backgroundColor ?? "white" : "#EDEDFF";
911
+ const cardInputBg = isButtons ? bStyles.cardInputBackground ?? "white" : "white";
912
+ const stripeElementStyle = isButtons && (bStyles.cardInputColor || bStyles.cardInputPlaceholderColor || bStyles.cardInputFontSize) ? {
913
+ style: {
914
+ base: {
915
+ ...bStyles.cardInputColor ? { color: bStyles.cardInputColor } : {},
916
+ ...bStyles.cardInputFontSize ? { fontSize: bStyles.cardInputFontSize } : {},
917
+ ...bStyles.cardInputPlaceholderColor ? { "::placeholder": { color: bStyles.cardInputPlaceholderColor } } : {}
918
+ },
919
+ invalid: { color: "#ef4444" }
920
+ }
921
+ } : {};
922
+ const cardFormBlock = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
923
+ backgroundColor: cardBg,
924
+ borderRadius: "8px",
925
+ padding: isButtons ? "0" : "1rem",
926
+ ...isButtons ? bStyles.cardFormContainer : {},
927
+ ...isButtons ? { padding: bStyles.cardFormContainer?.padding ?? "0" } : {}
928
+ }, children: [
929
+ isButtons && showCardForm && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
930
+ display: "flex",
931
+ alignItems: "center",
932
+ padding: "0.75rem 0 0.625rem"
933
+ }, children: [
934
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
935
+ "button",
936
+ {
937
+ type: "button",
938
+ onClick: collapseToButtons,
939
+ style: {
940
+ display: "inline-flex",
941
+ alignItems: "center",
942
+ gap: "0.5rem",
943
+ background: "none",
944
+ border: "none",
945
+ cursor: "pointer",
946
+ color: "#4b5563",
947
+ fontSize: bStyles.backButtonFontSize ?? "0.85rem",
948
+ fontWeight: 500,
949
+ padding: 0,
950
+ transition: "color 0.15s",
951
+ flexShrink: 0,
952
+ ...bStyles.backButton
953
+ },
954
+ "aria-label": "Back to payment methods",
955
+ children: [
956
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: {
957
+ display: "inline-flex",
958
+ alignItems: "center",
959
+ justifyContent: "center",
960
+ width: 28,
961
+ height: 28,
962
+ borderRadius: "50%",
963
+ backgroundColor: "#f3f4f6",
964
+ transition: "background-color 0.15s",
965
+ ...bStyles.backButtonIcon
966
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M15 18l-6-6 6-6" }) }) }),
967
+ "Go back"
968
+ ]
969
+ }
970
+ ),
971
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
972
+ flex: 1,
973
+ textAlign: "center",
974
+ fontWeight: 600,
975
+ fontSize: bStyles.titleFontSize ?? "1.05rem",
976
+ color: "#262833",
977
+ paddingRight: 80,
978
+ ...bStyles.title
979
+ }, children: "Secure card checkout" })
980
+ ] }),
981
+ !isButtons && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { textAlign: "center", fontWeight: 600, fontSize: "1.1rem", padding: "0.5rem 0", color: "#262833" }, children: "Secure card checkout" }),
982
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
983
+ backgroundColor: cardInputBg,
984
+ border: `1px solid ${resolvedBorder}`,
985
+ borderTopLeftRadius: "8px",
986
+ borderTopRightRadius: "8px",
987
+ padding: "10px"
988
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardNumberElement, { onReady: () => setFormReady(true), options: stripeElementStyle }) }),
989
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex" }, children: [
990
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
991
+ flex: 1,
992
+ backgroundColor: cardInputBg,
993
+ border: `1px solid ${resolvedBorder}`,
994
+ borderTop: "none",
995
+ borderRight: "none",
996
+ borderBottomLeftRadius: "8px",
997
+ padding: "10px"
998
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardExpiryElement, { options: stripeElementStyle }) }),
999
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
1000
+ flex: 1,
1001
+ backgroundColor: cardInputBg,
1002
+ border: `1px solid ${resolvedBorder}`,
1003
+ borderTop: "none",
1004
+ borderBottomRightRadius: "8px",
1005
+ padding: "10px"
1006
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardCvcElement, { options: stripeElementStyle }) })
1007
+ ] }),
1008
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
1009
+ backgroundColor: cardInputBg,
1010
+ border: `1px solid ${resolvedBorder}`,
1011
+ borderRadius: "8px",
1012
+ marginTop: "0.5rem",
1013
+ padding: "10px"
1014
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1015
+ "input",
1016
+ {
1017
+ placeholder: "Full Name on Card",
1018
+ autoComplete: "cc-name",
1019
+ value: fullName,
1020
+ onChange: (e) => handleNameChange(e.target.value),
1021
+ disabled: isSubmitting,
1022
+ required: true,
1023
+ style: {
1024
+ width: "100%",
1025
+ border: "none",
1026
+ outline: "none",
1027
+ background: "transparent",
1028
+ fontSize: bStyles.cardInputFontSize ?? "16px",
1029
+ fontFamily: "Poppins, sans-serif",
1030
+ color: bStyles.cardInputColor ?? "#262833",
1031
+ ...isButtons && bStyles.nameInput ? bStyles.nameInput : {}
1032
+ }
1033
+ }
1034
+ ) }),
1035
+ displayError && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "alert", "data-testid": "flopay-error", style: {
1036
+ margin: "0.75rem 0",
1037
+ padding: "0.625rem 0.875rem",
1038
+ background: "#FEF2F2",
1039
+ border: "1px solid #FECACA",
1040
+ borderRadius: "8px",
1041
+ color: "#991B1B",
1042
+ fontSize: "0.85rem",
1043
+ fontWeight: 600,
1044
+ display: "flex",
1045
+ alignItems: "center",
1046
+ gap: "0.5rem",
1047
+ ...isButtons && bStyles.errorBanner ? bStyles.errorBanner : {}
1048
+ }, children: [
1049
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.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" }) }),
1050
+ displayError
1051
+ ] }),
1052
+ children ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1053
+ "button",
1054
+ {
1055
+ type: "submit",
1056
+ disabled: !formReady || isSubmitting,
1057
+ "data-testid": "flopay-submit",
1058
+ style: {
1059
+ width: "100%",
1060
+ padding: "0.875rem",
1061
+ marginTop: "1rem",
1062
+ backgroundColor: "#4A49FF",
1063
+ color: "white",
1064
+ border: "none",
1065
+ borderRadius: "8px",
1066
+ fontSize: isButtons && bStyles.submitButtonFontSize ? bStyles.submitButtonFontSize : "1rem",
1067
+ fontWeight: 600,
1068
+ cursor: !formReady || isSubmitting ? "not-allowed" : "pointer",
1069
+ opacity: !formReady || isSubmitting ? 0.5 : 1,
1070
+ ...isButtons ? bStyles.submitButton : {}
1071
+ },
1072
+ children: isSubmitting ? "PROCESSING..." : submitLabel
1073
+ }
1074
+ ),
1075
+ !isButtons && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
1076
+ backgroundColor: "#EFF9F0",
1077
+ borderRadius: "8px",
1078
+ padding: "0.75rem",
1079
+ marginTop: "0.75rem",
1080
+ textAlign: "center",
1081
+ fontSize: "0.85rem",
1082
+ fontWeight: 600,
1083
+ color: "#7DAD3A"
1084
+ }, children: "Secure Card Checkout" })
1085
+ ] });
1086
+ if (layout === "buttons") {
1087
+ const isButtonsView = viewState === "buttons" || viewState === "expanding";
1088
+ const isCardView = viewState === "expanding" || viewState === "card" || viewState === "collapsing";
1089
+ const buttonsAnim = viewState === "expanding" ? `flopay-buttons-exit ${TRANSITION_MS}ms cubic-bezier(0.4, 0, 0.2, 1) both` : viewState === "collapsing" ? `flopay-buttons-enter ${TRANSITION_MS}ms cubic-bezier(0, 0, 0.2, 1) both` : void 0;
1090
+ const cardAnim = viewState === "expanding" ? `flopay-card-enter ${TRANSITION_MS}ms cubic-bezier(0, 0, 0.2, 1) both` : viewState === "collapsing" ? `flopay-card-exit ${TRANSITION_MS}ms cubic-bezier(0.4, 0, 0.2, 1) both` : void 0;
1091
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { onSubmit: handleSubmit, className, style: { position: "relative" }, children: [
1092
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FloPayKeyframes, {}),
1093
+ overlayStatus && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProcessingOverlay, { status: overlayStatus, errorMessage: displayError }),
1094
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "grid" }, children: [
1095
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: {
1096
+ gridArea: "1 / 1",
1097
+ display: "flex",
1098
+ flexDirection: "column",
1099
+ gap: "0.5rem",
1100
+ // When card form is showing (and not transitioning), hide but keep mounted
1101
+ ...!isButtonsView && !buttonsAnim ? { visibility: "hidden", position: "absolute", pointerEvents: "none", width: "100%" } : {},
1102
+ ...buttonsAnim ? { animation: buttonsAnim, pointerEvents: "none" } : {}
1103
+ }, children: [
1104
+ showPayPal && stripeInstance ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_stripe_js.Elements, { stripe: stripeInstance, options: paypalOptions, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1105
+ PayPalButtonInner,
1106
+ {
1107
+ sessionId,
1108
+ email,
1109
+ billingApiUrl: resolvedBillingApiUrl,
1110
+ onTokenizedBody: dispatchTokenizedBody,
1111
+ onErrorChange: updateError,
1112
+ isProcessing: isSubmitting,
1113
+ onButtonClick
1114
+ }
1115
+ ) }) : showPayPal ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { height: 44, borderRadius: 8, background: "#e5e7eb", animation: "flopay-pulse 1.5s ease-in-out infinite" } }) : null,
1116
+ showWallets && stripeInstance ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_stripe_js.Elements, { stripe: stripeInstance, options: walletOptions, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1117
+ WalletButtonInner,
1118
+ {
1119
+ sessionId,
1120
+ email,
1121
+ billingApiUrl: resolvedBillingApiUrl,
1122
+ showApplePay,
1123
+ showGooglePay,
1124
+ onTokenizedBody: dispatchTokenizedBody,
1125
+ onErrorChange: updateError,
1126
+ onButtonClick
1127
+ }
1128
+ ) }) : showWallets ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { height: 44, borderRadius: 8, background: "#e5e7eb", animation: "flopay-pulse 1.5s ease-in-out infinite" } }) : null,
1129
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1130
+ "button",
1131
+ {
1132
+ type: "button",
1133
+ onClick: () => {
1134
+ onButtonClick?.("card");
1135
+ expandToCard();
1136
+ },
1137
+ style: {
1138
+ width: "100%",
1139
+ padding: "0.9rem 1rem",
1140
+ backgroundColor: "white",
1141
+ color: "#262833",
1142
+ border: "1px solid #d1d5db",
1143
+ borderRadius: "8px",
1144
+ fontSize: bStyles.cardButtonFontSize ?? "0.95rem",
1145
+ fontWeight: 600,
1146
+ cursor: "pointer",
1147
+ display: "flex",
1148
+ alignItems: "center",
1149
+ justifyContent: "center",
1150
+ gap: "0.625rem",
1151
+ transition: "border-color 0.2s, box-shadow 0.2s, transform 0.1s",
1152
+ boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
1153
+ position: "relative",
1154
+ ...bStyles.cardButton
1155
+ },
1156
+ onMouseDown: (e) => {
1157
+ e.currentTarget.style.transform = "scale(0.985)";
1158
+ },
1159
+ onMouseUp: (e) => {
1160
+ e.currentTarget.style.transform = "scale(1)";
1161
+ },
1162
+ children: [
1163
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1164
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { width: "20", height: "14", x: "2", y: "5", rx: "2" }),
1165
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("line", { x1: "2", x2: "22", y1: "10", y2: "10" })
1166
+ ] }),
1167
+ "Credit / Debit Card",
1168
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#9ca3af", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { position: "absolute", right: "1rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M9 18l6-6-6-6" }) })
1169
+ ]
1170
+ }
1171
+ ),
1172
+ displayError && viewState === "buttons" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { role: "alert", "data-testid": "flopay-error", style: {
1173
+ margin: "0.25rem 0",
1174
+ padding: "0.625rem 0.875rem",
1175
+ background: "#FEF2F2",
1176
+ border: "1px solid #FECACA",
1177
+ borderRadius: "8px",
1178
+ color: "#991B1B",
1179
+ fontSize: "0.85rem",
1180
+ fontWeight: 600,
1181
+ display: "flex",
1182
+ alignItems: "center",
1183
+ gap: "0.5rem",
1184
+ ...bStyles.errorBanner ? bStyles.errorBanner : {}
1185
+ }, children: [
1186
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", style: { flexShrink: 0 }, children: /* @__PURE__ */ (0, import_jsx_runtime3.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" }) }),
1187
+ displayError
1188
+ ] })
1189
+ ] }),
1190
+ isCardView && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
1191
+ gridArea: "1 / 1",
1192
+ ...cardAnim ? { animation: cardAnim } : {},
1193
+ ...viewState === "collapsing" ? { pointerEvents: "none" } : {}
1194
+ }, children: cardFormBlock })
1195
+ ] })
1196
+ ] });
1197
+ }
839
1198
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { onSubmit: handleSubmit, className, style: { position: "relative" }, children: [
840
- overlayStatus && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProcessingOverlay, { status: overlayStatus }),
1199
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FloPayKeyframes, {}),
1200
+ overlayStatus && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ProcessingOverlay, { status: overlayStatus, errorMessage: displayError }),
841
1201
  showWallets && stripeInstance && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react_stripe_js.Elements, { stripe: stripeInstance, options: walletOptions, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
842
1202
  WalletButtonInner,
843
1203
  {
@@ -873,100 +1233,15 @@ function SplitCardFormInner({
873
1233
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "or pay with card" }),
874
1234
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { flex: 1, height: 1, backgroundColor: "#ddd" } })
875
1235
  ] }),
876
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { backgroundColor: "#EDEDFF", borderRadius: "8px", padding: "1rem" }, children: [
877
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { textAlign: "center", fontWeight: 600, fontSize: "1.1rem", padding: "0.5rem 0", color: "#262833" }, children: "Secure card checkout" }),
878
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
879
- backgroundColor: "white",
880
- border: "1px solid #A4A4FF",
881
- borderTopLeftRadius: "8px",
882
- borderTopRightRadius: "8px",
883
- padding: "10px"
884
- }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardNumberElement, { onReady: () => setFormReady(true) }) }),
885
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex" }, children: [
886
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
887
- flex: 1,
888
- backgroundColor: "white",
889
- border: "1px solid #A4A4FF",
890
- borderTop: "none",
891
- borderRight: "none",
892
- borderBottomLeftRadius: "8px",
893
- padding: "10px"
894
- }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardExpiryElement, {}) }),
895
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
896
- flex: 1,
897
- backgroundColor: "white",
898
- border: "1px solid #A4A4FF",
899
- borderTop: "none",
900
- borderBottomRightRadius: "8px",
901
- padding: "10px"
902
- }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CardCvcElement, {}) })
903
- ] }),
904
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
905
- backgroundColor: "white",
906
- border: "1px solid #A4A4FF",
907
- borderRadius: "8px",
908
- marginTop: "0.5rem",
909
- padding: "10px"
910
- }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
911
- "input",
912
- {
913
- placeholder: "Full Name on Card",
914
- autoComplete: "cc-name",
915
- value: fullName,
916
- onChange: (e) => handleNameChange(e.target.value),
917
- disabled: isSubmitting,
918
- required: true,
919
- style: {
920
- width: "100%",
921
- border: "none",
922
- outline: "none",
923
- fontSize: "16px",
924
- fontFamily: "Poppins, sans-serif",
925
- color: "#262833"
926
- }
927
- }
928
- ) }),
929
- displayError && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", "data-testid": "flopay-error", style: { color: "red", margin: "0.75rem 0", fontSize: "0.9rem" }, children: displayError }),
930
- children ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
931
- "button",
932
- {
933
- type: "submit",
934
- disabled: !formReady || isSubmitting,
935
- "data-testid": "flopay-submit",
936
- style: {
937
- width: "100%",
938
- padding: "0.875rem",
939
- marginTop: "1rem",
940
- backgroundColor: "#4A49FF",
941
- color: "white",
942
- border: "none",
943
- borderRadius: "8px",
944
- fontSize: "1rem",
945
- fontWeight: 600,
946
- cursor: !formReady || isSubmitting ? "not-allowed" : "pointer",
947
- opacity: !formReady || isSubmitting ? 0.5 : 1
948
- },
949
- children: isSubmitting ? "PROCESSING..." : submitLabel
950
- }
951
- ),
952
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: {
953
- backgroundColor: "#EFF9F0",
954
- borderRadius: "8px",
955
- padding: "0.75rem",
956
- marginTop: "0.75rem",
957
- textAlign: "center",
958
- fontSize: "0.85rem",
959
- fontWeight: 600,
960
- color: "#7DAD3A"
961
- }, children: "Secure Card Checkout" })
962
- ] })
1236
+ cardFormBlock
963
1237
  ] });
964
1238
  }
965
1239
 
966
1240
  // src/flopay-checkout.tsx
967
1241
  var import_jsx_runtime4 = require("react/jsx-runtime");
968
1242
  function FloPayCheckout({
969
- sessionId,
1243
+ sessionId: sessionIdProp,
1244
+ createSession: createSessionParams,
970
1245
  billingApiUrl,
971
1246
  appearance,
972
1247
  locale,
@@ -978,6 +1253,10 @@ function FloPayCheckout({
978
1253
  showPayPal = true,
979
1254
  showApplePay = true,
980
1255
  showGooglePay = true,
1256
+ layout,
1257
+ buttonsTheme,
1258
+ buttonsStyles,
1259
+ onButtonClick,
981
1260
  submitLabel,
982
1261
  className,
983
1262
  children,
@@ -986,11 +1265,12 @@ function FloPayCheckout({
986
1265
  renderConfirmButton,
987
1266
  onSessionCompleted
988
1267
  }) {
989
- const resolvedBillingUrl = (0, import_shared4.resolveBillingApiUrl)(billingApiUrl);
1268
+ const resolvedBillingUrl = (0, import_shared5.resolveBillingApiUrl)(billingApiUrl);
990
1269
  const [unified, setUnified] = (0, import_react6.useState)(null);
991
1270
  const [flopay, setFloPay] = (0, import_react6.useState)(null);
992
1271
  const flopayRef = (0, import_react6.useRef)(null);
993
1272
  const [session, setSession] = (0, import_react6.useState)(null);
1273
+ const [resolvedSessionId, setResolvedSessionId] = (0, import_react6.useState)(sessionIdProp ?? "");
994
1274
  const [isLoading, setIsLoading] = (0, import_react6.useState)(true);
995
1275
  const [loadError, setLoadError] = (0, import_react6.useState)(null);
996
1276
  const [currentMode, setCurrentMode] = (0, import_react6.useState)("full");
@@ -1013,7 +1293,7 @@ function FloPayCheckout({
1013
1293
  "x-user-id": sess.customer?.id ?? ""
1014
1294
  },
1015
1295
  body: JSON.stringify({
1016
- sessionId,
1296
+ sessionId: resolvedSessionId,
1017
1297
  tokenizedData: { id: void 0 },
1018
1298
  accountData: {
1019
1299
  userId: sess.customer?.id ?? "",
@@ -1042,12 +1322,12 @@ function FloPayCheckout({
1042
1322
  // No client secret available
1043
1323
  };
1044
1324
  }
1045
- throw new import_shared4.FloPayError(
1325
+ throw new import_shared5.FloPayError(
1046
1326
  json?.message ?? "Payment failed. Please try again.",
1047
1327
  "api_error"
1048
1328
  );
1049
1329
  },
1050
- [resolvedBillingUrl, sessionId]
1330
+ [resolvedBillingUrl, resolvedSessionId]
1051
1331
  );
1052
1332
  const handleRedirectResult = (0, import_react6.useCallback)(
1053
1333
  async (redirectResult, sess, options) => {
@@ -1092,22 +1372,175 @@ function FloPayCheckout({
1092
1372
  }
1093
1373
  return false;
1094
1374
  },
1095
- [resolvedBillingUrl, sessionId]
1375
+ [resolvedBillingUrl, resolvedSessionId]
1376
+ );
1377
+ const inflightRef = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
1378
+ const initializedHashRef = (0, import_react6.useRef)(null);
1379
+ function hashCreateParams(params) {
1380
+ const key = JSON.stringify({
1381
+ c: params?.clientId,
1382
+ i: params?.items?.map((x) => `${x.providerItemId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
1383
+ s: params?.subscriptions?.map((x) => `${x.providerPlanId}:${x.totalAmount}:${x.overrideAmount ?? ""}:${x.quantity ?? 1}`).sort(),
1384
+ e: params?.account.email,
1385
+ m: params?.checkoutMode ?? "full"
1386
+ });
1387
+ let h = 0;
1388
+ for (let i = 0; i < key.length; i++) {
1389
+ h = (h << 5) - h + key.charCodeAt(i) | 0;
1390
+ }
1391
+ return `flopay_session_${Math.abs(h).toString(36)}`;
1392
+ }
1393
+ const createSessionHash = (0, import_react6.useMemo)(
1394
+ () => createSessionParams ? hashCreateParams(createSessionParams) : "",
1395
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1396
+ [
1397
+ createSessionParams?.clientId,
1398
+ createSessionParams?.account?.email,
1399
+ createSessionParams?.checkoutMode,
1400
+ JSON.stringify(createSessionParams?.items),
1401
+ JSON.stringify(createSessionParams?.subscriptions)
1402
+ ]
1096
1403
  );
1404
+ const createSessionParamsRef = (0, import_react6.useRef)(createSessionParams);
1405
+ createSessionParamsRef.current = createSessionParams;
1097
1406
  (0, import_react6.useEffect)(() => {
1098
1407
  let cancelled = false;
1099
- setIsLoading(true);
1100
1408
  setLoadError(null);
1409
+ if (createSessionHash) {
1410
+ if (initializedHashRef.current === createSessionHash) return;
1411
+ const params = createSessionParamsRef.current;
1412
+ const totalAmount = [
1413
+ ...(params.items ?? []).map((i) => i.overrideAmount ?? i.totalAmount ?? 0),
1414
+ ...(params.subscriptions ?? []).map((s) => s.overrideAmount ?? s.totalAmount ?? 0)
1415
+ ].reduce((sum, v) => sum + v, 0);
1416
+ const currency = params.items?.[0]?.currency ?? params.subscriptions?.[0]?.currency ?? "USD";
1417
+ const syntheticSession = {
1418
+ id: "",
1419
+ clientSecret: "",
1420
+ mode: "payment",
1421
+ amount: Math.round(totalAmount * 100),
1422
+ currency,
1423
+ status: "open",
1424
+ customer: {
1425
+ id: params.account.userId,
1426
+ email: params.account.email,
1427
+ firstName: params.account.firstName,
1428
+ lastName: params.account.lastName
1429
+ },
1430
+ successUrl: params.successUrl,
1431
+ cancelUrl: params.cancelUrl,
1432
+ checkoutMode: params.checkoutMode ?? "full",
1433
+ items: (params.items ?? []).map((i, idx) => ({
1434
+ uuid: `synthetic-item-${idx}`,
1435
+ checkoutSessionId: "",
1436
+ providerItemId: i.providerItemId,
1437
+ providerItemName: i.providerItemName ?? i.providerItemId,
1438
+ quantity: i.quantity ?? 1,
1439
+ totalAmount: i.totalAmount,
1440
+ overrideAmount: i.overrideAmount ?? null,
1441
+ currency: i.currency ?? currency
1442
+ })),
1443
+ subscriptions: (params.subscriptions ?? []).map((s, idx) => ({
1444
+ uuid: `synthetic-sub-${idx}`,
1445
+ checkoutSessionId: "",
1446
+ providerPlanId: s.providerPlanId,
1447
+ providerPlanName: s.providerPlanName ?? s.providerPlanId,
1448
+ quantity: s.quantity ?? 1,
1449
+ totalAmount: s.totalAmount,
1450
+ overrideAmount: s.overrideAmount ?? null,
1451
+ currency: s.currency ?? currency
1452
+ }))
1453
+ };
1454
+ setSession(syntheticSession);
1455
+ setCurrentMode(params.checkoutMode ?? checkoutModeProp ?? "full");
1456
+ setIsLoading(false);
1457
+ const cacheKey = createSessionHash;
1458
+ async function resolveSession() {
1459
+ const api = new import_js.PaymentAPI(resolvedBillingUrl);
1460
+ let sid = typeof window !== "undefined" ? window.sessionStorage.getItem(cacheKey) : null;
1461
+ let realResult = null;
1462
+ if (sid) {
1463
+ try {
1464
+ realResult = await api.getUnifiedCheckoutSession(sid);
1465
+ if (realResult.data.session?.status === "complete") {
1466
+ if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1467
+ sid = null;
1468
+ realResult = null;
1469
+ }
1470
+ } catch {
1471
+ if (typeof window !== "undefined") window.sessionStorage.removeItem(cacheKey);
1472
+ sid = null;
1473
+ }
1474
+ }
1475
+ if (!sid) {
1476
+ realResult = await api.createAndFetchSession(createSessionParamsRef.current);
1477
+ sid = realResult.data.session?.id ?? "";
1478
+ if (sid && typeof window !== "undefined") {
1479
+ window.sessionStorage.setItem(cacheKey, sid);
1480
+ }
1481
+ }
1482
+ return { sid: sid ?? "", result: realResult };
1483
+ }
1484
+ (async () => {
1485
+ try {
1486
+ let promise = inflightRef.current.get(cacheKey);
1487
+ if (!promise) {
1488
+ promise = resolveSession();
1489
+ inflightRef.current.set(cacheKey, promise);
1490
+ }
1491
+ let resolved;
1492
+ try {
1493
+ resolved = await promise;
1494
+ } finally {
1495
+ inflightRef.current.delete(cacheKey);
1496
+ }
1497
+ if (cancelled) return;
1498
+ const { sid, result: realResult } = resolved;
1499
+ if (realResult) {
1500
+ setUnified(realResult);
1501
+ if (realResult.data.session) setSession(realResult.data.session);
1502
+ }
1503
+ if (sid) {
1504
+ setResolvedSessionId(sid);
1505
+ }
1506
+ let pk;
1507
+ if (realResult?.provider === "stripe") {
1508
+ pk = realResult.data.stripe?.publishableKey;
1509
+ }
1510
+ if (!pk) pk = fallbackPublishableKey;
1511
+ if (!pk) {
1512
+ throw new import_shared5.FloPayError(
1513
+ "No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
1514
+ "validation_error"
1515
+ );
1516
+ }
1517
+ const instance = await (0, import_js.loadFloPay)(pk, { billingApiUrl: resolvedBillingUrl, locale });
1518
+ if (!cancelled) {
1519
+ flopayRef.current = instance;
1520
+ setFloPay(instance);
1521
+ initializedHashRef.current = cacheKey;
1522
+ }
1523
+ } catch (err) {
1524
+ if (cancelled) return;
1525
+ const floPayErr = err instanceof import_shared5.FloPayError ? err : new import_shared5.FloPayError(err instanceof Error ? err.message : "Failed to create session", "api_error");
1526
+ setLoadError(floPayErr);
1527
+ }
1528
+ })();
1529
+ return () => {
1530
+ cancelled = true;
1531
+ };
1532
+ }
1533
+ setIsLoading(true);
1101
1534
  async function init() {
1102
1535
  try {
1103
1536
  const api = new import_js.PaymentAPI(resolvedBillingUrl);
1104
- const result = await api.getUnifiedCheckoutSession(sessionId);
1537
+ const result = await api.getUnifiedCheckoutSession(resolvedSessionId);
1105
1538
  if (cancelled) return;
1106
1539
  setUnified(result);
1107
1540
  const sess = result.data.session ?? null;
1108
1541
  setSession(sess);
1109
1542
  if (!sess) {
1110
- throw new import_shared4.FloPayError("No session data returned", "api_error");
1543
+ throw new import_shared5.FloPayError("No session data returned", "api_error");
1111
1544
  }
1112
1545
  if (sess.status === "complete") {
1113
1546
  setIsLoading(false);
@@ -1119,7 +1552,7 @@ function FloPayCheckout({
1119
1552
  const hasPayPalRedirectParams = typeof window !== "undefined" && new URLSearchParams(window.location.search).has("payment_intent");
1120
1553
  if (effectiveMode === "auto" && !autoCheckoutAttempted.current && !hasPayPalRedirectParams) {
1121
1554
  autoCheckoutAttempted.current = true;
1122
- const stripeInitPromise = initStripe(result, sess);
1555
+ const stripeInitPromise = initStripe(result);
1123
1556
  try {
1124
1557
  const redirectResult = await processPaymentForMode(sess);
1125
1558
  if (!redirectResult) {
@@ -1130,9 +1563,7 @@ function FloPayCheckout({
1130
1563
  await stripeInitPromise;
1131
1564
  const handled = await handleRedirectResult(redirectResult, sess, { attempt3DS: true });
1132
1565
  if (!cancelled) {
1133
- if (!handled) {
1134
- setCurrentMode("full");
1135
- }
1566
+ if (!handled) setCurrentMode("full");
1136
1567
  setIsLoading(false);
1137
1568
  }
1138
1569
  return;
@@ -1144,26 +1575,23 @@ function FloPayCheckout({
1144
1575
  return;
1145
1576
  }
1146
1577
  }
1147
- await initStripe(result, sess);
1578
+ await initStripe(result);
1148
1579
  if (!cancelled) setIsLoading(false);
1149
1580
  } catch (err) {
1150
1581
  if (cancelled) return;
1151
- const floPayErr = err instanceof import_shared4.FloPayError ? err : new import_shared4.FloPayError(
1152
- err instanceof Error ? err.message : "Failed to initialize checkout",
1153
- "api_error"
1154
- );
1582
+ const floPayErr = err instanceof import_shared5.FloPayError ? err : new import_shared5.FloPayError(err instanceof Error ? err.message : "Failed to initialize checkout", "api_error");
1155
1583
  setLoadError(floPayErr);
1156
1584
  setIsLoading(false);
1157
1585
  }
1158
1586
  }
1159
- async function initStripe(result, _sess) {
1587
+ async function initStripe(result) {
1160
1588
  let publishableKey;
1161
1589
  if (result.provider === "stripe") {
1162
1590
  publishableKey = result.data.stripe?.publishableKey;
1163
1591
  }
1164
1592
  if (!publishableKey) publishableKey = fallbackPublishableKey;
1165
1593
  if (!publishableKey) {
1166
- throw new import_shared4.FloPayError(
1594
+ throw new import_shared5.FloPayError(
1167
1595
  "No publishable key found in session response. Provide a fallbackPublishableKey prop or ensure the session includes gatewayData.publishableKey.",
1168
1596
  "validation_error"
1169
1597
  );
@@ -1179,7 +1607,7 @@ function FloPayCheckout({
1179
1607
  return () => {
1180
1608
  cancelled = true;
1181
1609
  };
1182
- }, [sessionId, resolvedBillingUrl, fallbackPublishableKey, locale, checkoutModeProp]);
1610
+ }, [resolvedSessionId, createSessionHash, resolvedBillingUrl, fallbackPublishableKey, locale, checkoutModeProp]);
1183
1611
  const handleConfirmCheckout = (0, import_react6.useCallback)(async () => {
1184
1612
  if (confirmProcessing || !session) return;
1185
1613
  setConfirmProcessing(true);
@@ -1194,7 +1622,7 @@ function FloPayCheckout({
1194
1622
  setCurrentMode("full");
1195
1623
  }
1196
1624
  } catch (err) {
1197
- const floPayErr = err instanceof import_shared4.FloPayError ? err : new import_shared4.FloPayError(
1625
+ const floPayErr = err instanceof import_shared5.FloPayError ? err : new import_shared5.FloPayError(
1198
1626
  err instanceof Error ? err.message : "Payment failed",
1199
1627
  "api_error"
1200
1628
  );
@@ -1215,7 +1643,7 @@ function FloPayCheckout({
1215
1643
  if (unified.provider === "stripe" && unified.data.stripe?.clientSecret) {
1216
1644
  opts.clientSecret = unified.data.stripe.clientSecret;
1217
1645
  } else {
1218
- const displayTotal = (0, import_shared4.buildCheckoutDisplayData)(session).total;
1646
+ const displayTotal = (0, import_shared5.buildCheckoutDisplayData)(session).total;
1219
1647
  opts.amount = Math.round(displayTotal * 100) || session.amount;
1220
1648
  opts.currency = session.currency?.toLowerCase();
1221
1649
  }
@@ -1231,32 +1659,32 @@ function FloPayCheckout({
1231
1659
  [session, isLoading, loadError, currentMode]
1232
1660
  );
1233
1661
  if (isLoading) {
1234
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: loadingNode ?? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1235
- "div",
1236
- {
1237
- style: {
1238
- display: "flex",
1239
- justifyContent: "center",
1240
- padding: 32
1241
- },
1242
- children: [
1243
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1244
- "div",
1245
- {
1246
- style: {
1247
- width: 24,
1248
- height: 24,
1249
- border: "2px solid #e5e7eb",
1250
- borderTopColor: "#6b7280",
1251
- borderRadius: "50%",
1252
- animation: "spin 0.6s linear infinite"
1253
- }
1254
- }
1255
- ),
1256
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
1257
- ]
1258
- }
1259
- ) });
1662
+ if (loadingNode) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: loadingNode });
1663
+ if (layout === "buttons") {
1664
+ const skeletonBar = (h) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
1665
+ height: h,
1666
+ borderRadius: 8,
1667
+ background: "#e5e7eb",
1668
+ animation: "flopay-loading-pulse 1.5s ease-in-out infinite"
1669
+ } });
1670
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
1671
+ showPayPal && skeletonBar(44),
1672
+ (showApplePay || showGooglePay) && skeletonBar(44),
1673
+ skeletonBar(48),
1674
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `@keyframes flopay-loading-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }` })
1675
+ ] });
1676
+ }
1677
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", justifyContent: "center", padding: 32 }, children: [
1678
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
1679
+ width: 24,
1680
+ height: 24,
1681
+ border: "2px solid #e5e7eb",
1682
+ borderTopColor: "#6b7280",
1683
+ borderRadius: "50%",
1684
+ animation: "spin 0.6s linear infinite"
1685
+ } }),
1686
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
1687
+ ] });
1260
1688
  }
1261
1689
  if (loadError) {
1262
1690
  if (errorNode) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: errorNode(loadError) });
@@ -1273,6 +1701,19 @@ function FloPayCheckout({
1273
1701
  }
1274
1702
  );
1275
1703
  }
1704
+ if ((!flopay || !providerOptions) && createSessionParams && layout === "buttons") {
1705
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1706
+ InterimButtonsView,
1707
+ {
1708
+ onButtonClick,
1709
+ showPayPal,
1710
+ showApplePay,
1711
+ showGooglePay,
1712
+ buttonsTheme,
1713
+ buttonsStyles
1714
+ }
1715
+ ) });
1716
+ }
1276
1717
  if (!flopay || !providerOptions) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, {});
1277
1718
  if (currentMode === "confirm") {
1278
1719
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CheckoutContext.Provider, { value: checkoutValue, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(FloPayProvider, { flopay, options: providerOptions, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className, children: [
@@ -1333,7 +1774,7 @@ function FloPayCheckout({
1333
1774
  children ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1334
1775
  SessionInjector,
1335
1776
  {
1336
- sessionId,
1777
+ sessionId: resolvedSessionId,
1337
1778
  billingApiUrl: resolvedBillingUrl,
1338
1779
  session,
1339
1780
  children
@@ -1341,18 +1782,22 @@ function FloPayCheckout({
1341
1782
  ) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1342
1783
  SplitCardForm,
1343
1784
  {
1344
- sessionId,
1785
+ sessionId: resolvedSessionId,
1345
1786
  email: session?.customer?.email,
1346
1787
  userId: session?.customer?.id,
1347
1788
  firstName: session?.customer?.firstName,
1348
1789
  lastName: session?.customer?.lastName,
1349
- totalAmount: session ? Math.round((0, import_shared4.buildCheckoutDisplayData)(session).total * 100) : 0,
1790
+ totalAmount: session ? Math.round((0, import_shared5.buildCheckoutDisplayData)(session).total * 100) : 0,
1350
1791
  currency: session?.currency?.toLowerCase() ?? "usd",
1351
1792
  onComplete,
1352
1793
  onError,
1353
1794
  showPayPal,
1354
1795
  showApplePay,
1355
1796
  showGooglePay,
1797
+ layout,
1798
+ buttonsTheme,
1799
+ buttonsStyles,
1800
+ onButtonClick,
1356
1801
  submitLabel,
1357
1802
  className
1358
1803
  }
@@ -1383,9 +1828,165 @@ function SessionInjector({
1383
1828
  return import_react6.default.cloneElement(child, injected);
1384
1829
  }) });
1385
1830
  }
1831
+ function InterimButtonsView({
1832
+ onButtonClick,
1833
+ showPayPal,
1834
+ showApplePay,
1835
+ showGooglePay,
1836
+ buttonsTheme,
1837
+ buttonsStyles: stylesOverride
1838
+ }) {
1839
+ const [showCardForm, setShowCardForm] = (0, import_react6.useState)(false);
1840
+ const bStyles = (0, import_react6.useMemo)(() => {
1841
+ const base = (0, import_shared5.resolveButtonsLayoutTheme)(buttonsTheme);
1842
+ if (!stylesOverride) return base;
1843
+ return {
1844
+ ...base,
1845
+ ...stylesOverride,
1846
+ cardButton: { ...base.cardButton, ...stylesOverride.cardButton },
1847
+ cardFormContainer: { ...base.cardFormContainer, ...stylesOverride.cardFormContainer },
1848
+ submitButton: { ...base.submitButton, ...stylesOverride.submitButton },
1849
+ title: { ...base.title, ...stylesOverride.title }
1850
+ };
1851
+ }, [buttonsTheme, stylesOverride]);
1852
+ const skeleton = (h) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
1853
+ height: h,
1854
+ borderRadius: 8,
1855
+ background: "#e5e7eb",
1856
+ animation: "flopay-interim-pulse 1.5s ease-in-out infinite"
1857
+ } });
1858
+ if (showCardForm) {
1859
+ const inputBorder = bStyles.cardInputBorder ?? "#e5e7eb";
1860
+ const inputBg = bStyles.cardInputBackground ?? "white";
1861
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: {
1862
+ backgroundColor: bStyles.cardFormContainer?.backgroundColor ?? "white",
1863
+ borderRadius: "8px",
1864
+ animation: "flopay-interim-expand 0.35s cubic-bezier(0.4, 0, 0.2, 1) both",
1865
+ overflow: "hidden",
1866
+ ...bStyles.cardFormContainer
1867
+ }, children: [
1868
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", alignItems: "center", padding: "0.75rem 0 0.625rem" }, children: [
1869
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1870
+ "button",
1871
+ {
1872
+ type: "button",
1873
+ onClick: () => setShowCardForm(false),
1874
+ style: {
1875
+ display: "inline-flex",
1876
+ alignItems: "center",
1877
+ gap: "0.5rem",
1878
+ background: "none",
1879
+ border: "none",
1880
+ cursor: "pointer",
1881
+ color: "#4b5563",
1882
+ fontSize: "0.85rem",
1883
+ fontWeight: 500,
1884
+ padding: 0,
1885
+ flexShrink: 0,
1886
+ ...bStyles.backButton
1887
+ },
1888
+ children: [
1889
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: {
1890
+ display: "inline-flex",
1891
+ alignItems: "center",
1892
+ justifyContent: "center",
1893
+ width: 28,
1894
+ height: 28,
1895
+ borderRadius: "50%",
1896
+ backgroundColor: "#f3f4f6",
1897
+ ...bStyles.backButtonIcon
1898
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M15 18l-6-6 6-6" }) }) }),
1899
+ "Go back"
1900
+ ]
1901
+ }
1902
+ ),
1903
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
1904
+ flex: 1,
1905
+ textAlign: "center",
1906
+ fontWeight: 600,
1907
+ fontSize: "1.05rem",
1908
+ color: "#262833",
1909
+ paddingRight: 80,
1910
+ ...bStyles.title
1911
+ }, children: "Secure card checkout" })
1912
+ ] }),
1913
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { backgroundColor: inputBg, border: `1px solid ${inputBorder}`, borderTopLeftRadius: 8, borderTopRightRadius: 8, padding: 12, height: 45 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: "60%", height: 14, borderRadius: 4, background: "#e5e7eb", animation: "flopay-interim-pulse 1.5s ease-in-out infinite" } }) }),
1914
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex" }, children: [
1915
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { flex: 1, backgroundColor: inputBg, border: `1px solid ${inputBorder}`, borderTop: "none", borderRight: "none", borderBottomLeftRadius: 8, padding: 12, height: 45 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: "50%", height: 14, borderRadius: 4, background: "#e5e7eb", animation: "flopay-interim-pulse 1.5s ease-in-out infinite" } }) }),
1916
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { flex: 1, backgroundColor: inputBg, border: `1px solid ${inputBorder}`, borderTop: "none", borderBottomRightRadius: 8, padding: 12, height: 45 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: "40%", height: 14, borderRadius: 4, background: "#e5e7eb", animation: "flopay-interim-pulse 1.5s ease-in-out infinite" } }) })
1917
+ ] }),
1918
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { backgroundColor: inputBg, border: `1px solid ${inputBorder}`, borderRadius: 8, marginTop: 8, padding: 12, height: 45 }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: "45%", height: 14, borderRadius: 4, background: "#e5e7eb", animation: "flopay-interim-pulse 1.5s ease-in-out infinite" } }) }),
1919
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: {
1920
+ height: 50,
1921
+ borderRadius: 8,
1922
+ marginTop: 16,
1923
+ background: "#c8c8ff",
1924
+ animation: "flopay-interim-pulse 1.5s ease-in-out infinite",
1925
+ ...bStyles.submitButton,
1926
+ opacity: 0.5
1927
+ } }),
1928
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `
1929
+ @keyframes flopay-interim-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
1930
+ @keyframes flopay-interim-expand {
1931
+ 0% { opacity: 0; max-height: 0; transform: translateY(-12px); }
1932
+ 40% { opacity: 1; }
1933
+ 100% { opacity: 1; max-height: 600px; transform: translateY(0); }
1934
+ }
1935
+ ` })
1936
+ ] });
1937
+ }
1938
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
1939
+ showPayPal && skeleton(44),
1940
+ (showApplePay || showGooglePay) && skeleton(44),
1941
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1942
+ "button",
1943
+ {
1944
+ type: "button",
1945
+ onClick: () => {
1946
+ onButtonClick?.("card");
1947
+ setShowCardForm(true);
1948
+ },
1949
+ style: {
1950
+ width: "100%",
1951
+ padding: "0.9rem 1rem",
1952
+ backgroundColor: "white",
1953
+ color: "#262833",
1954
+ border: "1px solid #d1d5db",
1955
+ borderRadius: "8px",
1956
+ fontSize: bStyles.cardButtonFontSize ?? "0.95rem",
1957
+ fontWeight: 600,
1958
+ cursor: "pointer",
1959
+ display: "flex",
1960
+ alignItems: "center",
1961
+ justifyContent: "center",
1962
+ gap: "0.625rem",
1963
+ boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
1964
+ transition: "transform 0.1s",
1965
+ position: "relative",
1966
+ ...bStyles.cardButton
1967
+ },
1968
+ onMouseDown: (e) => {
1969
+ e.currentTarget.style.transform = "scale(0.985)";
1970
+ },
1971
+ onMouseUp: (e) => {
1972
+ e.currentTarget.style.transform = "scale(1)";
1973
+ },
1974
+ children: [
1975
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: [
1976
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("rect", { width: "20", height: "14", x: "2", y: "5", rx: "2" }),
1977
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("line", { x1: "2", x2: "22", y1: "10", y2: "10" })
1978
+ ] }),
1979
+ "Credit / Debit Card",
1980
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#9ca3af", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { position: "absolute", right: "1rem" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("path", { d: "M9 18l6-6-6-6" }) })
1981
+ ]
1982
+ }
1983
+ ),
1984
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `@keyframes flopay-interim-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }` })
1985
+ ] });
1986
+ }
1386
1987
 
1387
1988
  // src/checkout-form.tsx
1388
- var import_shared5 = require("@flopay/shared");
1989
+ var import_shared6 = require("@flopay/shared");
1389
1990
  var import_react7 = require("react");
1390
1991
  var import_jsx_runtime5 = require("react/jsx-runtime");
1391
1992
  var WALLET_RESUME_KEY2 = "flopay_wallet_resume";
@@ -1593,7 +2194,7 @@ function CheckoutFormInner({
1593
2194
  return;
1594
2195
  }
1595
2196
  if (!sessionId || !email) {
1596
- throw new import_shared5.FloPayError("Missing sessionId or email", "validation_error");
2197
+ throw new import_shared6.FloPayError("Missing sessionId or email", "validation_error");
1597
2198
  }
1598
2199
  const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
1599
2200
  method: "POST",
@@ -1605,10 +2206,10 @@ function CheckoutFormInner({
1605
2206
  isPaypal: false
1606
2207
  })
1607
2208
  });
1608
- if (!intentResponse.ok) throw new import_shared5.FloPayError("Failed to create payment intent", "api_error");
2209
+ if (!intentResponse.ok) throw new import_shared6.FloPayError("Failed to create payment intent", "api_error");
1609
2210
  const intentJson = await intentResponse.json();
1610
2211
  const intentClientSecret = intentJson.data?.id;
1611
- if (!intentClientSecret) throw new import_shared5.FloPayError("No client_secret in payment intent response", "api_error");
2212
+ if (!intentClientSecret) throw new import_shared6.FloPayError("No client_secret in payment intent response", "api_error");
1612
2213
  const confirmResult = await flopay.confirmCardPayment({
1613
2214
  clientSecret: intentClientSecret,
1614
2215
  paymentMethodId: pmResult.paymentMethodId
@@ -1663,7 +2264,7 @@ function CheckoutFormInner({
1663
2264
  }
1664
2265
 
1665
2266
  // src/paypal-button.tsx
1666
- var import_shared6 = require("@flopay/shared");
2267
+ var import_shared7 = require("@flopay/shared");
1667
2268
  var import_react8 = require("react");
1668
2269
  var import_jsx_runtime6 = require("react/jsx-runtime");
1669
2270
  function PayPalButton({
@@ -1783,7 +2384,7 @@ function PayPalButton({
1783
2384
  setSubmitting(true);
1784
2385
  onErrorChange?.(null);
1785
2386
  if (!sessionId || !email) {
1786
- throw new import_shared6.FloPayError("Missing sessionId or email for PayPal payment", "validation_error");
2387
+ throw new import_shared7.FloPayError("Missing sessionId or email for PayPal payment", "validation_error");
1787
2388
  }
1788
2389
  const intentResponse = await fetch(`${baseUrl}/v1/checkouts/payments/intents`, {
1789
2390
  method: "POST",
@@ -1795,10 +2396,10 @@ function PayPalButton({
1795
2396
  isPaypal: "true"
1796
2397
  })
1797
2398
  });
1798
- if (!intentResponse.ok) throw new import_shared6.FloPayError("Failed to create payment intent", "api_error");
2399
+ if (!intentResponse.ok) throw new import_shared7.FloPayError("Failed to create payment intent", "api_error");
1799
2400
  const intentJson = await intentResponse.json();
1800
2401
  const intentClientSecret = intentJson.data?.id;
1801
- if (!intentClientSecret) throw new import_shared6.FloPayError("No client_secret in response", "api_error");
2402
+ if (!intentClientSecret) throw new import_shared7.FloPayError("No client_secret in response", "api_error");
1802
2403
  const result = await flopay.confirmPayment({
1803
2404
  clientSecret: intentClientSecret,
1804
2405
  returnUrl: window.location.href