@flopay/react 0.3.14 → 0.3.15

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/README.md CHANGED
@@ -178,7 +178,7 @@ Skip the backend API route — create the session directly in the component:
178
178
 
179
179
  The component POSTs to the billing API, gets the full session back, and renders the form — zero backend code needed.
180
180
 
181
- When you use `onBeforeButtonClick`, `createSession.account.email` can be filled in later for the card-button flow. PayPal and wallets still need all required data before they render.
181
+ When you use `onBeforeButtonClick`, `createSession.account.email` can still be filled in later for the card-button flow. PayPal and wallet buttons still bootstrap normally, but this hook does not run for them, so collect any data those methods require before the user uses them.
182
182
 
183
183
  ### Advanced: Manual Provider Setup
184
184
 
package/dist/index.cjs CHANGED
@@ -351,15 +351,6 @@ function buildSyntheticSession(params, checkoutModeOverride) {
351
351
  }))
352
352
  };
353
353
  }
354
- function ensureInlineSessionReady(params) {
355
- if (!params.account.email?.trim()) {
356
- throw new import_shared3.FloPayError(
357
- "Email is required before continuing with card checkout.",
358
- "validation_error",
359
- { param: "createSession.account.email" }
360
- );
361
- }
362
- }
363
354
  function mergeAccountPatch(base, patch) {
364
355
  if (!patch) return base;
365
356
  return {
@@ -821,6 +812,7 @@ function SplitCardFormInner({
821
812
  }) {
822
813
  const flopay = useFloPay();
823
814
  const elements = useElements();
815
+ const checkout = (0, import_react6.useContext)(CheckoutContext);
824
816
  const contextBillingUrl = useBillingApiUrl();
825
817
  const [processing, setProcessing] = (0, import_react6.useState)(false);
826
818
  const [error, setError] = (0, import_react6.useState)(null);
@@ -866,7 +858,8 @@ function SplitCardFormInner({
866
858
  title: { ...base.title, ...buttonsStylesOverride.title }
867
859
  };
868
860
  }, [buttonsTheme, buttonsStylesOverride]);
869
- const isSubmitting = externalProcessing ?? processing;
861
+ const isInlineSessionPatchProcessing = checkout.inlineSessionPatchProcessing ?? false;
862
+ const isSubmitting = (externalProcessing ?? processing) || isInlineSessionPatchProcessing;
870
863
  const isSelfContained = !onTokenizedBody;
871
864
  const baseUrl = resolvedBillingApiUrl.replace(/\/+$/, "");
872
865
  const resolvedAccount = (0, import_react6.useMemo)(() => mergeAccountPatch({
@@ -925,8 +918,11 @@ function SplitCardFormInner({
925
918
  if (result === false) {
926
919
  return false;
927
920
  }
928
- if (result?.account) {
929
- setAccountPatch((prev) => ({ ...prev ?? {}, ...result.account }));
921
+ if (result && typeof result === "object") {
922
+ await checkout.applyInlineSessionPatch?.(result);
923
+ if (result.account) {
924
+ setAccountPatch((prev) => ({ ...prev ?? {}, ...result.account }));
925
+ }
930
926
  }
931
927
  return true;
932
928
  } catch (err) {
@@ -938,7 +934,7 @@ function SplitCardFormInner({
938
934
  onError?.(floPayErr);
939
935
  return false;
940
936
  }
941
- }, [onBeforeButtonClick, sessionId, updateError, onError]);
937
+ }, [checkout.applyInlineSessionPatch, onBeforeButtonClick, sessionId, updateError, onError]);
942
938
  const processPaymentInternal = (0, import_react6.useCallback)(
943
939
  async (tokenizedBody) => {
944
940
  if (processingRef.current) return;
@@ -1547,11 +1543,13 @@ function SplitCardFormInner({
1547
1543
  {
1548
1544
  type: "button",
1549
1545
  onClick: async () => {
1546
+ if (isSubmitting) return;
1550
1547
  const shouldContinue = await runBeforeCardButtonClick();
1551
1548
  if (!shouldContinue) return;
1552
1549
  onButtonClick?.("card");
1553
1550
  expandToCard();
1554
1551
  },
1552
+ disabled: isSubmitting,
1555
1553
  style: {
1556
1554
  width: "100%",
1557
1555
  padding: "0.9rem 1rem",
@@ -1561,7 +1559,7 @@ function SplitCardFormInner({
1561
1559
  borderRadius: "8px",
1562
1560
  fontSize: bStyles.cardButtonFontSize ?? "0.95rem",
1563
1561
  fontWeight: 600,
1564
- cursor: "pointer",
1562
+ cursor: isSubmitting ? "not-allowed" : "pointer",
1565
1563
  display: "flex",
1566
1564
  alignItems: "center",
1567
1565
  justifyContent: "center",
@@ -1569,6 +1567,7 @@ function SplitCardFormInner({
1569
1567
  transition: "border-color 0.2s, box-shadow 0.2s, transform 0.1s",
1570
1568
  boxShadow: "0 1px 2px rgba(0,0,0,0.04)",
1571
1569
  position: "relative",
1570
+ opacity: isSubmitting ? 0.6 : 1,
1572
1571
  ...bStyles.cardButton
1573
1572
  },
1574
1573
  onMouseDown: (e) => {
@@ -1652,6 +1651,12 @@ function SplitCardFormInner({
1652
1651
 
1653
1652
  // src/flopay-checkout.tsx
1654
1653
  var import_jsx_runtime5 = require("react/jsx-runtime");
1654
+ function hasInlineSessionPatchData(patch) {
1655
+ if (!patch) return false;
1656
+ return Boolean(
1657
+ patch.account && Object.keys(patch.account).length > 0 || patch.couponCodes !== void 0 || patch.tagsData !== void 0 || patch.utmMetadata !== void 0
1658
+ );
1659
+ }
1655
1660
  function FloPayCheckout({
1656
1661
  sessionId: sessionIdProp,
1657
1662
  createSession: createSessionParams,
@@ -1699,7 +1704,6 @@ function FloPayCheckout({
1699
1704
  const [createSessionPatch, setCreateSessionPatch] = (0, import_react7.useState)(void 0);
1700
1705
  const [createSessionPatchBaseHash, setCreateSessionPatchBaseHash] = (0, import_react7.useState)("");
1701
1706
  const [cardBootstrapPending, setCardBootstrapPending] = (0, import_react7.useState)(false);
1702
- const [deferredCardOpen, setDeferredCardOpen] = (0, import_react7.useState)(false);
1703
1707
  const autoCheckoutAttempted = (0, import_react7.useRef)(false);
1704
1708
  const onCompleteRef = (0, import_react7.useRef)(onComplete);
1705
1709
  onCompleteRef.current = onComplete;
@@ -1834,9 +1838,6 @@ function FloPayCheckout({
1834
1838
  );
1835
1839
  const inflightRef = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
1836
1840
  const initializedHashRef = (0, import_react7.useRef)(null);
1837
- const deferInlineSessionUntilCardClick = Boolean(
1838
- effectiveCreateSession && !children && layout === "buttons" && onBeforeButtonClick && (effectiveCreateSession.checkoutMode ?? checkoutModeProp ?? "full") === "full"
1839
- );
1840
1841
  function hashCreateParams(params) {
1841
1842
  const key = JSON.stringify({
1842
1843
  c: params?.clientId,
@@ -1866,7 +1867,6 @@ function FloPayCheckout({
1866
1867
  setResolvedSessionId(sessionIdProp ?? "");
1867
1868
  }, [sessionIdProp]);
1868
1869
  async function resolveInlineSession(params, cacheKey) {
1869
- ensureInlineSessionReady(params);
1870
1870
  const api = new import_js.PaymentAPI(resolvedBillingUrl);
1871
1871
  let sid = typeof window !== "undefined" ? window.sessionStorage.getItem(cacheKey) : null;
1872
1872
  let realResult = null;
@@ -1911,104 +1911,64 @@ function FloPayCheckout({
1911
1911
  } finally {
1912
1912
  inflightRef.current.delete(cacheKey);
1913
1913
  }
1914
- if (patch) {
1915
- setCreateSessionPatchBaseHash(baseCreateSessionHash);
1916
- setCreateSessionPatch((prev) => mergeInlineSessionPatches(prev, patch));
1917
- }
1918
- const { sid, result: realResult } = resolved;
1919
- setUnified(realResult);
1920
- if (realResult.data.session) {
1921
- setSession(realResult.data.session);
1922
- }
1923
- if (sid) {
1924
- setResolvedSessionId(sid);
1925
- }
1926
- let publishableKey;
1927
- if (realResult.provider === "stripe") {
1928
- publishableKey = realResult.data.stripe?.publishableKey;
1929
- }
1930
- if (!publishableKey) publishableKey = fallbackPublishableKey;
1931
- if (!publishableKey) {
1932
- throw new import_shared6.FloPayError(
1933
- "No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
1934
- "validation_error"
1935
- );
1936
- }
1937
- const instance = await (0, import_js.loadFloPay)(publishableKey, {
1938
- billingApiUrl: resolvedBillingUrl,
1939
- locale
1940
- });
1941
- flopayRef.current = instance;
1942
- setFloPay(instance);
1943
1914
  initializedHashRef.current = cacheKey;
1944
- setIsLoading(false);
1915
+ try {
1916
+ if (patch) {
1917
+ setCreateSessionPatchBaseHash(baseCreateSessionHash);
1918
+ setCreateSessionPatch((prev) => mergeInlineSessionPatches(prev, patch));
1919
+ }
1920
+ const { sid, result: realResult } = resolved;
1921
+ setUnified(realResult);
1922
+ if (realResult.data.session) {
1923
+ setSession(realResult.data.session);
1924
+ }
1925
+ if (sid) {
1926
+ setResolvedSessionId(sid);
1927
+ }
1928
+ let publishableKey;
1929
+ if (realResult.provider === "stripe") {
1930
+ publishableKey = realResult.data.stripe?.publishableKey;
1931
+ }
1932
+ if (!publishableKey) publishableKey = fallbackPublishableKey;
1933
+ if (!publishableKey) {
1934
+ throw new import_shared6.FloPayError(
1935
+ "No publishable key found. Provide fallbackPublishableKey or ensure the session includes gatewayData.publishableKey.",
1936
+ "validation_error"
1937
+ );
1938
+ }
1939
+ const instance = await (0, import_js.loadFloPay)(publishableKey, {
1940
+ billingApiUrl: resolvedBillingUrl,
1941
+ locale
1942
+ });
1943
+ flopayRef.current = instance;
1944
+ setFloPay(instance);
1945
+ setIsLoading(false);
1946
+ } catch (err) {
1947
+ if (initializedHashRef.current === cacheKey) {
1948
+ initializedHashRef.current = null;
1949
+ }
1950
+ throw err;
1951
+ }
1945
1952
  return resolved;
1946
1953
  },
1947
1954
  [fallbackPublishableKey, locale, resolvedBillingUrl]
1948
1955
  );
1949
- const handleDeferredCardButtonClick = (0, import_react7.useCallback)(async () => {
1950
- if (cardBootstrapPending) return;
1951
- setLoadError(null);
1956
+ const handleInlineSessionPatch = (0, import_react7.useCallback)(async (patch) => {
1957
+ if (!hasInlineSessionPatchData(patch) || cardBootstrapPending) return;
1952
1958
  setCardBootstrapPending(true);
1953
1959
  try {
1954
- const beforeClickResult = await onBeforeButtonClick?.({
1955
- method: "card",
1956
- createSession: effectiveCreateSession
1957
- });
1958
- if (beforeClickResult === false) {
1959
- setDeferredCardOpen(false);
1960
- return;
1961
- }
1962
- const patch = beforeClickResult && typeof beforeClickResult === "object" ? beforeClickResult : void 0;
1963
- const baseParams = createSessionParamsRef.current;
1964
- if (!baseParams) {
1965
- throw new import_shared6.FloPayError(
1966
- "createSession is required to bootstrap checkout.",
1967
- "validation_error"
1968
- );
1969
- }
1970
- ensureInlineSessionReady(mergeInlineSessionPatch(baseParams, patch));
1971
- setDeferredCardOpen(true);
1972
- onButtonClick?.("card");
1973
1960
  await bootstrapInlineSession(patch);
1974
- } catch (err) {
1975
- setDeferredCardOpen(false);
1976
- const floPayErr = err instanceof import_shared6.FloPayError ? err : new import_shared6.FloPayError(
1977
- err instanceof Error ? err.message : "Failed to start card checkout.",
1978
- "api_error"
1979
- );
1980
- setLoadError(floPayErr);
1981
- onErrorRef.current?.(floPayErr);
1982
1961
  } finally {
1983
1962
  setCardBootstrapPending(false);
1984
1963
  }
1985
1964
  }, [
1986
1965
  bootstrapInlineSession,
1987
- baseCreateSessionHash,
1988
- cardBootstrapPending,
1989
- effectiveCreateSession,
1990
- onButtonClick,
1991
- onBeforeButtonClick
1966
+ cardBootstrapPending
1992
1967
  ]);
1993
1968
  (0, import_react7.useEffect)(() => {
1994
1969
  let cancelled = false;
1995
1970
  setLoadError(null);
1996
1971
  if (createSessionHash) {
1997
- if (deferInlineSessionUntilCardClick) {
1998
- if (initializedHashRef.current !== createSessionHash) {
1999
- setSession(null);
2000
- setUnified(null);
2001
- setFloPay(null);
2002
- setResolvedSessionId("");
2003
- setDeferredCardOpen(false);
2004
- flopayRef.current = null;
2005
- }
2006
- setCurrentMode(createSessionParamsRef.current?.checkoutMode ?? checkoutModeProp ?? "full");
2007
- setIsLoading(false);
2008
- return () => {
2009
- cancelled = true;
2010
- };
2011
- }
2012
1972
  if (initializedHashRef.current === createSessionHash) return;
2013
1973
  const params = createSessionParamsRef.current;
2014
1974
  setSession(buildSyntheticSession(params, checkoutModeProp));
@@ -2104,7 +2064,7 @@ function FloPayCheckout({
2104
2064
  return () => {
2105
2065
  cancelled = true;
2106
2066
  };
2107
- }, [resolvedSessionId, createSessionHash, checkoutModeProp, deferInlineSessionUntilCardClick, bootstrapInlineSession]);
2067
+ }, [resolvedSessionId, createSessionHash, checkoutModeProp, bootstrapInlineSession]);
2108
2068
  const handleConfirmCheckout = (0, import_react7.useCallback)(async () => {
2109
2069
  if (confirmProcessing || !session) return;
2110
2070
  setConfirmProcessing(true);
@@ -2147,17 +2107,29 @@ function FloPayCheckout({
2147
2107
  }
2148
2108
  return opts;
2149
2109
  }, [unified, session, appearance, resolvedBillingUrl]);
2110
+ const shouldHandleInlineSessionPatch = Boolean(
2111
+ createSessionParams && !children && layout === "buttons" && onBeforeButtonClick && (effectiveCreateSession?.checkoutMode ?? checkoutModeProp ?? "full") === "full"
2112
+ );
2150
2113
  const checkoutValue = (0, import_react7.useMemo)(
2151
2114
  () => ({
2152
2115
  session,
2153
2116
  loading: isLoading,
2154
2117
  error: loadError,
2155
- checkoutMode: currentMode
2118
+ checkoutMode: currentMode,
2119
+ applyInlineSessionPatch: shouldHandleInlineSessionPatch ? handleInlineSessionPatch : void 0,
2120
+ inlineSessionPatchProcessing: cardBootstrapPending
2156
2121
  }),
2157
- [session, isLoading, loadError, currentMode]
2122
+ [
2123
+ session,
2124
+ isLoading,
2125
+ loadError,
2126
+ currentMode,
2127
+ shouldHandleInlineSessionPatch,
2128
+ handleInlineSessionPatch,
2129
+ cardBootstrapPending
2130
+ ]
2158
2131
  );
2159
2132
  const shouldShowInterimButtons = Boolean(createSessionParams) && layout === "buttons" && (!flopay || !providerOptions);
2160
- const shouldKeepDeferredInterimVisible = shouldShowInterimButtons && deferInlineSessionUntilCardClick;
2161
2133
  if (isLoading) {
2162
2134
  if (loadingNode) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: loadingNode });
2163
2135
  if (layout === "buttons") {
@@ -2186,7 +2158,7 @@ function FloPayCheckout({
2186
2158
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("style", { children: `@keyframes spin { to { transform: rotate(360deg); } }` })
2187
2159
  ] });
2188
2160
  }
2189
- if (loadError && !shouldKeepDeferredInterimVisible) {
2161
+ if (loadError) {
2190
2162
  if (errorNode) return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: errorNode(loadError) });
2191
2163
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2192
2164
  "div",
@@ -2206,13 +2178,9 @@ function FloPayCheckout({
2206
2178
  InterimButtonsView,
2207
2179
  {
2208
2180
  onButtonClick,
2209
- onCardButtonClick: deferInlineSessionUntilCardClick ? handleDeferredCardButtonClick : void 0,
2210
- cardLoading: cardBootstrapPending,
2211
- cardOpen: deferInlineSessionUntilCardClick ? deferredCardOpen : void 0,
2212
- errorMessage: shouldKeepDeferredInterimVisible ? loadError?.message ?? null : null,
2213
- showPayPal: deferInlineSessionUntilCardClick ? false : showPayPal,
2214
- showApplePay: deferInlineSessionUntilCardClick ? false : showApplePay,
2215
- showGooglePay: deferInlineSessionUntilCardClick ? false : showGooglePay,
2181
+ showPayPal,
2182
+ showApplePay,
2183
+ showGooglePay,
2216
2184
  buttonsTheme,
2217
2185
  buttonsStyles,
2218
2186
  cardButtonContent,
@@ -2313,7 +2281,6 @@ function FloPayCheckout({
2313
2281
  enableAVS,
2314
2282
  avsLayout,
2315
2283
  country: session?.customer?.country,
2316
- initialCardOpen: deferredCardOpen,
2317
2284
  submitLabel,
2318
2285
  className
2319
2286
  }