@swype-org/react-sdk 0.1.87 → 0.1.89

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
@@ -728,10 +728,10 @@ function isSafari() {
728
728
  var POPUP_RESULT_TIMEOUT_MS = 12e4;
729
729
  var POPUP_CLOSED_POLL_MS = 500;
730
730
  var POPUP_CLOSED_GRACE_MS = 1e3;
731
- function createPasskeyViaPopup(options, existingCredentialIds = []) {
731
+ function createPasskeyViaPopup(options) {
732
732
  return new Promise((resolve, reject) => {
733
- const channelId = `swype-pk-${Date.now()}-${Math.random().toString(36).slice(2)}`;
734
- const payload = { ...options, channelId };
733
+ const verificationToken = crypto.randomUUID();
734
+ const payload = { ...options, verificationToken };
735
735
  const encoded = btoa(JSON.stringify(payload));
736
736
  const popupUrl = `${window.location.origin}/passkey-register#${encoded}`;
737
737
  const popup = window.open(popupUrl, "swype-passkey");
@@ -740,22 +740,21 @@ function createPasskeyViaPopup(options, existingCredentialIds = []) {
740
740
  return;
741
741
  }
742
742
  let settled = false;
743
- const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelId) : null;
744
743
  const timer = setTimeout(() => {
745
744
  cleanup();
746
745
  reject(new Error("Passkey creation timed out. Please try again."));
747
746
  }, POPUP_RESULT_TIMEOUT_MS);
748
- let closedGraceTimer = null;
749
747
  const closedPoll = setInterval(() => {
750
748
  if (popup.closed) {
751
749
  clearInterval(closedPoll);
752
- closedGraceTimer = setTimeout(() => {
750
+ setTimeout(() => {
753
751
  if (!settled) {
752
+ settled = true;
754
753
  cleanup();
755
- checkServerForNewPasskey(
754
+ checkServerForPasskeyByToken(
756
755
  options.authToken,
757
756
  options.apiBaseUrl,
758
- existingCredentialIds
757
+ verificationToken
759
758
  ).then((result) => {
760
759
  if (result) {
761
760
  resolve(result);
@@ -769,45 +768,18 @@ function createPasskeyViaPopup(options, existingCredentialIds = []) {
769
768
  }, POPUP_CLOSED_GRACE_MS);
770
769
  }
771
770
  }, POPUP_CLOSED_POLL_MS);
772
- function handleResult(data) {
773
- if (settled) return;
774
- if (!data || typeof data !== "object") return;
775
- if (data.type !== "swype:passkey-popup-result") return;
776
- settled = true;
777
- cleanup();
778
- if (data.error) {
779
- reject(new Error(data.error));
780
- } else if (data.result) {
781
- resolve(data.result);
782
- } else {
783
- reject(new Error("Invalid passkey popup response."));
784
- }
785
- }
786
- if (channel) {
787
- channel.onmessage = (event) => handleResult(event.data);
788
- }
789
- const postMessageHandler = (event) => {
790
- if (event.source !== popup) return;
791
- handleResult(event.data);
792
- };
793
- window.addEventListener("message", postMessageHandler);
794
771
  function cleanup() {
795
772
  clearTimeout(timer);
796
773
  clearInterval(closedPoll);
797
- if (closedGraceTimer) clearTimeout(closedGraceTimer);
798
- window.removeEventListener("message", postMessageHandler);
799
- channel?.close();
800
774
  }
801
775
  });
802
776
  }
803
777
  var VERIFY_POPUP_TIMEOUT_MS = 6e4;
804
778
  function findDevicePasskeyViaPopup(options) {
805
779
  return new Promise((resolve, reject) => {
806
- const channelId = `swype-pv-${Date.now()}-${Math.random().toString(36).slice(2)}`;
807
780
  const verificationToken = crypto.randomUUID();
808
781
  const payload = {
809
782
  ...options,
810
- channelId,
811
783
  verificationToken
812
784
  };
813
785
  const encoded = btoa(JSON.stringify(payload));
@@ -818,7 +790,6 @@ function findDevicePasskeyViaPopup(options) {
818
790
  return;
819
791
  }
820
792
  let settled = false;
821
- const channel = typeof BroadcastChannel !== "undefined" ? new BroadcastChannel(channelId) : null;
822
793
  const timer = setTimeout(() => {
823
794
  cleanup();
824
795
  resolve(null);
@@ -828,13 +799,14 @@ function findDevicePasskeyViaPopup(options) {
828
799
  clearInterval(closedPoll);
829
800
  setTimeout(() => {
830
801
  if (!settled) {
802
+ settled = true;
831
803
  cleanup();
832
- checkServerForVerifiedPasskey(
804
+ checkServerForPasskeyByToken(
833
805
  options.authToken,
834
806
  options.apiBaseUrl,
835
807
  verificationToken
836
- ).then((credentialId) => {
837
- resolve(credentialId);
808
+ ).then((result) => {
809
+ resolve(result?.credentialId ?? null);
838
810
  }).catch(() => {
839
811
  resolve(null);
840
812
  });
@@ -842,38 +814,13 @@ function findDevicePasskeyViaPopup(options) {
842
814
  }, POPUP_CLOSED_GRACE_MS);
843
815
  }
844
816
  }, POPUP_CLOSED_POLL_MS);
845
- function handleResult(data) {
846
- if (settled) return;
847
- if (!data || typeof data !== "object") return;
848
- if (data.type !== "swype:passkey-verify-result") return;
849
- settled = true;
850
- cleanup();
851
- if (data.error) {
852
- resolve(null);
853
- } else if (data.result && typeof data.result === "object") {
854
- const result = data.result;
855
- resolve(result.credentialId ?? null);
856
- } else {
857
- resolve(null);
858
- }
859
- }
860
- if (channel) {
861
- channel.onmessage = (event) => handleResult(event.data);
862
- }
863
- const postMessageHandler = (event) => {
864
- if (event.source !== popup) return;
865
- handleResult(event.data);
866
- };
867
- window.addEventListener("message", postMessageHandler);
868
817
  function cleanup() {
869
818
  clearTimeout(timer);
870
819
  clearInterval(closedPoll);
871
- window.removeEventListener("message", postMessageHandler);
872
- channel?.close();
873
820
  }
874
821
  });
875
822
  }
876
- async function checkServerForVerifiedPasskey(authToken, apiBaseUrl, verificationToken) {
823
+ async function checkServerForPasskeyByToken(authToken, apiBaseUrl, verificationToken) {
877
824
  if (!authToken || !apiBaseUrl) return null;
878
825
  const res = await fetch(`${apiBaseUrl}/v1/users/config`, {
879
826
  headers: { Authorization: `Bearer ${authToken}` }
@@ -882,19 +829,7 @@ async function checkServerForVerifiedPasskey(authToken, apiBaseUrl, verification
882
829
  const body = await res.json();
883
830
  const passkeys = body.config.passkeys ?? [];
884
831
  const matched = passkeys.find((p) => p.lastVerificationToken === verificationToken);
885
- return matched?.credentialId ?? null;
886
- }
887
- async function checkServerForNewPasskey(authToken, apiBaseUrl, existingCredentialIds) {
888
- if (!authToken || !apiBaseUrl) return null;
889
- const res = await fetch(`${apiBaseUrl}/v1/users/config`, {
890
- headers: { Authorization: `Bearer ${authToken}` }
891
- });
892
- if (!res.ok) return null;
893
- const body = await res.json();
894
- const passkeys = body.config.passkeys ?? [];
895
- const existingSet = new Set(existingCredentialIds);
896
- const newPasskey = passkeys.find((p) => !existingSet.has(p.credentialId));
897
- return newPasskey ?? null;
832
+ return matched ? { credentialId: matched.credentialId, publicKey: matched.publicKey } : null;
898
833
  }
899
834
 
900
835
  // src/hooks.ts
@@ -1648,43 +1583,6 @@ function useTransferSigning(pollIntervalMs = 2e3, options) {
1648
1583
  );
1649
1584
  return { signing, signPayload, error, signTransfer: signTransfer2 };
1650
1585
  }
1651
- function Spinner({ size = 40, label }) {
1652
- const { tokens } = useSwypeConfig();
1653
- return /* @__PURE__ */ jsxRuntime.jsxs(
1654
- "div",
1655
- {
1656
- style: {
1657
- display: "flex",
1658
- flexDirection: "column",
1659
- alignItems: "center",
1660
- gap: "12px"
1661
- },
1662
- children: [
1663
- /* @__PURE__ */ jsxRuntime.jsx(
1664
- "div",
1665
- {
1666
- style: {
1667
- width: size,
1668
- height: size,
1669
- border: `4px solid ${tokens.bgInput}`,
1670
- borderTopColor: tokens.accent,
1671
- borderRightColor: tokens.accent + "66",
1672
- borderRadius: "50%",
1673
- boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.1)",
1674
- animation: "swype-spin 0.9s linear infinite"
1675
- }
1676
- }
1677
- ),
1678
- label && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: tokens.textSecondary, fontSize: "0.875rem", margin: 0 }, children: label }),
1679
- /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
1680
- @keyframes swype-spin {
1681
- to { transform: rotate(360deg); }
1682
- }
1683
- ` })
1684
- ]
1685
- }
1686
- );
1687
- }
1688
1586
 
1689
1587
  // src/auth.ts
1690
1588
  var EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -1846,6 +1744,426 @@ function resolveDataLoadAction({
1846
1744
  }
1847
1745
  return "load";
1848
1746
  }
1747
+
1748
+ // src/paymentHelpers.ts
1749
+ var ACTIVE_CREDENTIAL_STORAGE_KEY = "swype_active_credential_id";
1750
+ var MOBILE_FLOW_STORAGE_KEY = "swype_mobile_flow";
1751
+ var MIN_SEND_AMOUNT_USD = 0.25;
1752
+ function persistMobileFlowState(data) {
1753
+ try {
1754
+ sessionStorage.setItem(MOBILE_FLOW_STORAGE_KEY, JSON.stringify(data));
1755
+ } catch {
1756
+ }
1757
+ }
1758
+ function loadMobileFlowState() {
1759
+ try {
1760
+ const raw = sessionStorage.getItem(MOBILE_FLOW_STORAGE_KEY);
1761
+ if (!raw) return null;
1762
+ return JSON.parse(raw);
1763
+ } catch {
1764
+ return null;
1765
+ }
1766
+ }
1767
+ function clearMobileFlowState() {
1768
+ try {
1769
+ sessionStorage.removeItem(MOBILE_FLOW_STORAGE_KEY);
1770
+ } catch {
1771
+ }
1772
+ }
1773
+ function computeSmartDefaults(accts, transferAmount) {
1774
+ if (accts.length === 0) return null;
1775
+ for (const acct of accts) {
1776
+ for (const wallet of acct.wallets) {
1777
+ if (wallet.status === "ACTIVE") {
1778
+ const bestSource = wallet.sources.find(
1779
+ (s) => s.balance.available.amount >= transferAmount
1780
+ );
1781
+ if (bestSource) {
1782
+ return { accountId: acct.id, walletId: wallet.id };
1783
+ }
1784
+ }
1785
+ }
1786
+ }
1787
+ let bestAccount = null;
1788
+ let bestWallet = null;
1789
+ let bestBalance = -1;
1790
+ let bestIsActive = false;
1791
+ for (const acct of accts) {
1792
+ for (const wallet of acct.wallets) {
1793
+ const walletBal = wallet.balance.available.amount;
1794
+ const isActive = wallet.status === "ACTIVE";
1795
+ if (walletBal > bestBalance || walletBal === bestBalance && isActive && !bestIsActive) {
1796
+ bestBalance = walletBal;
1797
+ bestAccount = acct;
1798
+ bestWallet = wallet;
1799
+ bestIsActive = isActive;
1800
+ }
1801
+ }
1802
+ }
1803
+ if (bestAccount) {
1804
+ return {
1805
+ accountId: bestAccount.id,
1806
+ walletId: bestWallet?.id ?? null
1807
+ };
1808
+ }
1809
+ return { accountId: accts[0].id, walletId: null };
1810
+ }
1811
+ function parseRawBalance(rawBalance, decimals) {
1812
+ const parsed = Number(rawBalance);
1813
+ if (!Number.isFinite(parsed)) return 0;
1814
+ return parsed / 10 ** decimals;
1815
+ }
1816
+ function buildSelectSourceChoices(options) {
1817
+ const chainChoices = [];
1818
+ const chainIndexByName = /* @__PURE__ */ new Map();
1819
+ for (const option of options) {
1820
+ const { chainName, tokenSymbol } = option;
1821
+ const balance = parseRawBalance(option.rawBalance, option.decimals);
1822
+ let chainChoice;
1823
+ const existingIdx = chainIndexByName.get(chainName);
1824
+ if (existingIdx === void 0) {
1825
+ chainChoice = { chainName, balance: 0, tokens: [] };
1826
+ chainIndexByName.set(chainName, chainChoices.length);
1827
+ chainChoices.push(chainChoice);
1828
+ } else {
1829
+ chainChoice = chainChoices[existingIdx];
1830
+ }
1831
+ chainChoice.balance += balance;
1832
+ const existing = chainChoice.tokens.find((t) => t.tokenSymbol === tokenSymbol);
1833
+ if (existing) {
1834
+ existing.balance += balance;
1835
+ } else {
1836
+ chainChoice.tokens.push({ tokenSymbol, balance });
1837
+ }
1838
+ }
1839
+ return chainChoices;
1840
+ }
1841
+
1842
+ // src/paymentReducer.ts
1843
+ function deriveSourceTypeAndId(state) {
1844
+ if (state.connectingNewAccount) {
1845
+ return { sourceType: "providerId", sourceId: state.selectedProviderId ?? "" };
1846
+ }
1847
+ if (state.selectedWalletId) {
1848
+ return { sourceType: "walletId", sourceId: state.selectedWalletId };
1849
+ }
1850
+ if (state.selectedAccountId) {
1851
+ return { sourceType: "accountId", sourceId: state.selectedAccountId };
1852
+ }
1853
+ return { sourceType: "providerId", sourceId: state.selectedProviderId ?? "" };
1854
+ }
1855
+ function createInitialState(config) {
1856
+ return {
1857
+ step: "login",
1858
+ error: null,
1859
+ providers: [],
1860
+ accounts: [],
1861
+ chains: [],
1862
+ loadingData: false,
1863
+ selectedAccountId: null,
1864
+ selectedWalletId: null,
1865
+ selectedProviderId: null,
1866
+ connectingNewAccount: false,
1867
+ amount: config.depositAmount != null ? config.depositAmount.toString() : "",
1868
+ transfer: null,
1869
+ creatingTransfer: false,
1870
+ registeringPasskey: false,
1871
+ verifyingPasskeyPopup: false,
1872
+ passkeyPopupNeeded: config.passkeyPopupNeeded,
1873
+ activeCredentialId: config.activeCredentialId,
1874
+ knownCredentialIds: [],
1875
+ verificationTarget: null,
1876
+ oneTapLimit: 100,
1877
+ mobileFlow: false,
1878
+ deeplinkUri: null,
1879
+ increasingLimit: false
1880
+ };
1881
+ }
1882
+ function paymentReducer(state, action) {
1883
+ switch (action.type) {
1884
+ // ── Auth ──────────────────────────────────────────────────────
1885
+ case "CODE_SENT":
1886
+ return {
1887
+ ...state,
1888
+ verificationTarget: action.target,
1889
+ error: null,
1890
+ step: "otp-verify"
1891
+ };
1892
+ case "BACK_TO_LOGIN":
1893
+ return {
1894
+ ...state,
1895
+ verificationTarget: null,
1896
+ error: null,
1897
+ step: "login"
1898
+ };
1899
+ // ── Passkey ──────────────────────────────────────────────────
1900
+ case "PASSKEY_CONFIG_LOADED":
1901
+ return {
1902
+ ...state,
1903
+ knownCredentialIds: action.knownIds,
1904
+ oneTapLimit: action.oneTapLimit ?? state.oneTapLimit
1905
+ };
1906
+ case "PASSKEY_ACTIVATED":
1907
+ return {
1908
+ ...state,
1909
+ activeCredentialId: action.credentialId,
1910
+ passkeyPopupNeeded: false
1911
+ };
1912
+ case "SET_PASSKEY_POPUP_NEEDED":
1913
+ return { ...state, passkeyPopupNeeded: action.needed };
1914
+ case "SET_REGISTERING_PASSKEY":
1915
+ return { ...state, registeringPasskey: action.value };
1916
+ case "SET_VERIFYING_PASSKEY":
1917
+ return { ...state, verifyingPasskeyPopup: action.value };
1918
+ // ── Data loading ─────────────────────────────────────────────
1919
+ case "DATA_LOAD_START":
1920
+ return { ...state, loadingData: true, error: null };
1921
+ case "DATA_LOADED": {
1922
+ const next = {
1923
+ ...state,
1924
+ providers: action.providers,
1925
+ accounts: action.accounts,
1926
+ chains: action.chains
1927
+ };
1928
+ if (action.defaults) {
1929
+ next.selectedAccountId = action.defaults.accountId;
1930
+ next.selectedWalletId = action.defaults.walletId;
1931
+ } else if (action.fallbackProviderId && !state.connectingNewAccount) {
1932
+ next.selectedProviderId = action.fallbackProviderId;
1933
+ }
1934
+ if (action.clearMobileState) {
1935
+ next.mobileFlow = false;
1936
+ next.deeplinkUri = null;
1937
+ }
1938
+ if (action.resolvedStep !== void 0) {
1939
+ next.step = action.resolvedStep;
1940
+ }
1941
+ return next;
1942
+ }
1943
+ case "DATA_LOAD_END":
1944
+ return { ...state, loadingData: false };
1945
+ case "ACCOUNTS_RELOADED": {
1946
+ const next = {
1947
+ ...state,
1948
+ accounts: action.accounts,
1949
+ providers: action.providers
1950
+ };
1951
+ if (action.defaults) {
1952
+ next.selectedAccountId = action.defaults.accountId;
1953
+ next.selectedWalletId = action.defaults.walletId;
1954
+ next.connectingNewAccount = false;
1955
+ }
1956
+ return next;
1957
+ }
1958
+ // ── Source selection ──────────────────────────────────────────
1959
+ case "SELECT_PROVIDER":
1960
+ return {
1961
+ ...state,
1962
+ selectedProviderId: action.providerId,
1963
+ selectedAccountId: null,
1964
+ connectingNewAccount: true
1965
+ };
1966
+ case "SELECT_ACCOUNT":
1967
+ return {
1968
+ ...state,
1969
+ selectedAccountId: action.accountId,
1970
+ selectedWalletId: action.walletId,
1971
+ connectingNewAccount: false,
1972
+ step: "deposit"
1973
+ };
1974
+ // ── Transfer lifecycle ───────────────────────────────────────
1975
+ case "PAY_STARTED":
1976
+ return {
1977
+ ...state,
1978
+ step: action.isSetupRedirect ? "open-wallet" : "processing",
1979
+ error: null,
1980
+ creatingTransfer: true,
1981
+ deeplinkUri: null,
1982
+ mobileFlow: false
1983
+ };
1984
+ case "PAY_ENDED":
1985
+ return { ...state, creatingTransfer: false };
1986
+ case "PAY_ERROR":
1987
+ return {
1988
+ ...state,
1989
+ error: action.error,
1990
+ step: action.fallbackStep
1991
+ };
1992
+ case "TRANSFER_CREATED":
1993
+ return { ...state, transfer: action.transfer };
1994
+ case "TRANSFER_SIGNED":
1995
+ return { ...state, transfer: action.transfer };
1996
+ case "TRANSFER_COMPLETED":
1997
+ return {
1998
+ ...state,
1999
+ transfer: action.transfer,
2000
+ step: "success",
2001
+ mobileFlow: false,
2002
+ deeplinkUri: null
2003
+ };
2004
+ case "TRANSFER_FAILED":
2005
+ return {
2006
+ ...state,
2007
+ transfer: action.transfer,
2008
+ error: action.error,
2009
+ step: "success",
2010
+ mobileFlow: false,
2011
+ deeplinkUri: null
2012
+ };
2013
+ case "PROCESSING_TIMEOUT":
2014
+ return { ...state, error: action.error, step: "deposit" };
2015
+ case "CONFIRM_SIGN_SUCCESS":
2016
+ return {
2017
+ ...state,
2018
+ transfer: action.transfer,
2019
+ step: "processing",
2020
+ mobileFlow: false,
2021
+ deeplinkUri: null
2022
+ };
2023
+ // ── Mobile flow ──────────────────────────────────────────────
2024
+ case "MOBILE_DEEPLINK_READY":
2025
+ return {
2026
+ ...state,
2027
+ mobileFlow: true,
2028
+ deeplinkUri: action.deeplinkUri,
2029
+ step: "open-wallet"
2030
+ };
2031
+ case "MOBILE_SETUP_COMPLETE":
2032
+ return {
2033
+ ...state,
2034
+ transfer: action.transfer,
2035
+ error: null,
2036
+ mobileFlow: false,
2037
+ deeplinkUri: null,
2038
+ step: "deposit"
2039
+ };
2040
+ case "MOBILE_SIGN_READY":
2041
+ return {
2042
+ ...state,
2043
+ transfer: action.transfer,
2044
+ error: null,
2045
+ mobileFlow: false,
2046
+ deeplinkUri: null,
2047
+ step: "confirm-sign"
2048
+ };
2049
+ case "CLEAR_MOBILE_STATE":
2050
+ return { ...state, mobileFlow: false, deeplinkUri: null };
2051
+ case "ENTER_MOBILE_FLOW":
2052
+ return {
2053
+ ...state,
2054
+ mobileFlow: true,
2055
+ deeplinkUri: action.deeplinkUri,
2056
+ selectedProviderId: action.providerId,
2057
+ error: action.error ?? null,
2058
+ step: "open-wallet"
2059
+ };
2060
+ case "MOBILE_RESUME_SUCCESS":
2061
+ return {
2062
+ ...state,
2063
+ transfer: action.transfer,
2064
+ error: null,
2065
+ mobileFlow: false,
2066
+ deeplinkUri: null,
2067
+ step: "success"
2068
+ };
2069
+ case "MOBILE_RESUME_FAILED":
2070
+ return {
2071
+ ...state,
2072
+ transfer: action.transfer,
2073
+ error: "Transfer failed.",
2074
+ mobileFlow: false,
2075
+ deeplinkUri: null,
2076
+ step: "success"
2077
+ };
2078
+ case "MOBILE_RESUME_PROCESSING":
2079
+ return {
2080
+ ...state,
2081
+ transfer: action.transfer,
2082
+ error: null,
2083
+ mobileFlow: false,
2084
+ deeplinkUri: null,
2085
+ step: "processing"
2086
+ };
2087
+ // ── Increase limit ───────────────────────────────────────────
2088
+ case "SET_INCREASING_LIMIT":
2089
+ return { ...state, increasingLimit: action.value };
2090
+ case "INCREASE_LIMIT_DEEPLINK":
2091
+ return {
2092
+ ...state,
2093
+ transfer: action.transfer,
2094
+ mobileFlow: true,
2095
+ deeplinkUri: action.deeplinkUri
2096
+ };
2097
+ // ── Navigation & error ───────────────────────────────────────
2098
+ case "NAVIGATE":
2099
+ return { ...state, step: action.step };
2100
+ case "SET_ERROR":
2101
+ return { ...state, error: action.error };
2102
+ // ── Lifecycle ────────────────────────────────────────────────
2103
+ case "NEW_PAYMENT":
2104
+ return {
2105
+ ...state,
2106
+ step: "deposit",
2107
+ transfer: null,
2108
+ error: null,
2109
+ amount: action.depositAmount != null ? action.depositAmount.toString() : "",
2110
+ mobileFlow: false,
2111
+ deeplinkUri: null,
2112
+ connectingNewAccount: false,
2113
+ selectedWalletId: null,
2114
+ selectedAccountId: action.firstAccountId
2115
+ };
2116
+ case "LOGOUT":
2117
+ return {
2118
+ ...createInitialState({
2119
+ depositAmount: action.depositAmount,
2120
+ passkeyPopupNeeded: state.passkeyPopupNeeded,
2121
+ activeCredentialId: null
2122
+ })
2123
+ };
2124
+ case "SYNC_AMOUNT":
2125
+ return { ...state, amount: action.amount };
2126
+ default:
2127
+ return state;
2128
+ }
2129
+ }
2130
+ function Spinner({ size = 40, label }) {
2131
+ const { tokens } = useSwypeConfig();
2132
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2133
+ "div",
2134
+ {
2135
+ style: {
2136
+ display: "flex",
2137
+ flexDirection: "column",
2138
+ alignItems: "center",
2139
+ gap: "12px"
2140
+ },
2141
+ children: [
2142
+ /* @__PURE__ */ jsxRuntime.jsx(
2143
+ "div",
2144
+ {
2145
+ style: {
2146
+ width: size,
2147
+ height: size,
2148
+ border: `4px solid ${tokens.bgInput}`,
2149
+ borderTopColor: tokens.accent,
2150
+ borderRightColor: tokens.accent + "66",
2151
+ borderRadius: "50%",
2152
+ boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.1)",
2153
+ animation: "swype-spin 0.9s linear infinite"
2154
+ }
2155
+ }
2156
+ ),
2157
+ label && /* @__PURE__ */ jsxRuntime.jsx("p", { style: { color: tokens.textSecondary, fontSize: "0.875rem", margin: 0 }, children: label }),
2158
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `
2159
+ @keyframes swype-spin {
2160
+ to { transform: rotate(360deg); }
2161
+ }
2162
+ ` })
2163
+ ]
2164
+ }
2165
+ );
2166
+ }
1849
2167
  var FOOTER_CSS = `
1850
2168
  .swype-screen-footer {
1851
2169
  padding-bottom: max(24px, env(safe-area-inset-bottom, 24px));
@@ -4562,11 +4880,248 @@ var errorStyle2 = (color) => ({
4562
4880
  color: "#ef4444",
4563
4881
  margin: "8px 0 0"
4564
4882
  });
4565
- var PaymentErrorBoundary = class extends react.Component {
4566
- constructor(props) {
4567
- super(props);
4568
- this.state = { hasError: false };
4569
- }
4883
+ function CenteredSpinner({ label }) {
4884
+ return /* @__PURE__ */ jsxRuntime.jsx(ScreenLayout, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "48px 0", flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { label }) }) });
4885
+ }
4886
+ function StepRenderer({
4887
+ state,
4888
+ ready,
4889
+ authenticated,
4890
+ activeOtpStatus,
4891
+ pollingTransfer,
4892
+ pollingError,
4893
+ authExecutorError,
4894
+ transferSigningSigning,
4895
+ transferSigningError,
4896
+ pendingConnections,
4897
+ sourceName,
4898
+ sourceAddress,
4899
+ sourceVerified,
4900
+ maxSourceBalance,
4901
+ tokenCount,
4902
+ selectedAccount,
4903
+ selectSourceChoices,
4904
+ selectSourceRecommended,
4905
+ authInput,
4906
+ otpCode,
4907
+ selectSourceChainName,
4908
+ selectSourceTokenSymbol,
4909
+ merchantName,
4910
+ onBack,
4911
+ onDismiss,
4912
+ autoCloseSeconds,
4913
+ depositAmount,
4914
+ handlers
4915
+ }) {
4916
+ const { step } = state;
4917
+ if (!ready) {
4918
+ return /* @__PURE__ */ jsxRuntime.jsx(CenteredSpinner, { label: "Initializing..." });
4919
+ }
4920
+ if (step === "login" && !authenticated) {
4921
+ return /* @__PURE__ */ jsxRuntime.jsx(
4922
+ LoginScreen,
4923
+ {
4924
+ authInput,
4925
+ onAuthInputChange: handlers.onSetAuthInput,
4926
+ onSubmit: handlers.onSendLoginCode,
4927
+ sending: activeOtpStatus === "sending-code",
4928
+ error: state.error,
4929
+ onBack,
4930
+ merchantInitials: merchantName ? merchantName.slice(0, 2).toUpperCase() : void 0
4931
+ }
4932
+ );
4933
+ }
4934
+ if (step === "otp-verify" && !authenticated) {
4935
+ return /* @__PURE__ */ jsxRuntime.jsx(
4936
+ OtpVerifyScreen,
4937
+ {
4938
+ maskedIdentifier: state.verificationTarget ? maskAuthIdentifier(state.verificationTarget) : "",
4939
+ otpCode,
4940
+ onOtpChange: (code) => {
4941
+ handlers.onSetOtpCode(code);
4942
+ },
4943
+ onVerify: handlers.onVerifyLoginCode,
4944
+ onResend: handlers.onResendLoginCode,
4945
+ onBack: handlers.onBackFromOtp,
4946
+ verifying: activeOtpStatus === "submitting-code",
4947
+ error: state.error
4948
+ }
4949
+ );
4950
+ }
4951
+ if ((step === "login" || step === "otp-verify") && authenticated) {
4952
+ return /* @__PURE__ */ jsxRuntime.jsx(CenteredSpinner, { label: "Verifying your passkey..." });
4953
+ }
4954
+ if (step === "verify-passkey") {
4955
+ return /* @__PURE__ */ jsxRuntime.jsx(
4956
+ VerifyPasskeyScreen,
4957
+ {
4958
+ onVerify: handlers.onVerifyPasskeyViaPopup,
4959
+ onBack: handlers.onLogout,
4960
+ verifying: state.verifyingPasskeyPopup,
4961
+ error: state.error
4962
+ }
4963
+ );
4964
+ }
4965
+ if (step === "create-passkey") {
4966
+ return /* @__PURE__ */ jsxRuntime.jsx(
4967
+ CreatePasskeyScreen,
4968
+ {
4969
+ onCreatePasskey: handlers.onRegisterPasskey,
4970
+ onBack: handlers.onLogout,
4971
+ creating: state.registeringPasskey,
4972
+ error: state.error,
4973
+ popupFallback: state.passkeyPopupNeeded,
4974
+ onCreatePasskeyViaPopup: handlers.onCreatePasskeyViaPopup
4975
+ }
4976
+ );
4977
+ }
4978
+ if (step === "wallet-picker") {
4979
+ return /* @__PURE__ */ jsxRuntime.jsx(
4980
+ WalletPickerScreen,
4981
+ {
4982
+ providers: state.providers,
4983
+ pendingConnections,
4984
+ loading: state.creatingTransfer,
4985
+ onSelectProvider: handlers.onSelectProvider,
4986
+ onContinueConnection: handlers.onContinueConnection,
4987
+ onBack: () => handlers.onNavigate(state.activeCredentialId ? "deposit" : "create-passkey")
4988
+ }
4989
+ );
4990
+ }
4991
+ if (step === "open-wallet") {
4992
+ const providerName = state.providers.find((p) => p.id === state.selectedProviderId)?.name ?? null;
4993
+ return /* @__PURE__ */ jsxRuntime.jsx(
4994
+ OpenWalletScreen,
4995
+ {
4996
+ walletName: providerName,
4997
+ deeplinkUri: state.deeplinkUri ?? "",
4998
+ loading: state.creatingTransfer || !state.deeplinkUri,
4999
+ error: state.error || pollingError,
5000
+ onRetryStatus: handlers.onRetryMobileStatus,
5001
+ onLogout: handlers.onLogout
5002
+ }
5003
+ );
5004
+ }
5005
+ if (step === "confirm-sign") {
5006
+ const providerName = state.providers.find((p) => p.id === state.selectedProviderId)?.name ?? null;
5007
+ return /* @__PURE__ */ jsxRuntime.jsx(
5008
+ ConfirmSignScreen,
5009
+ {
5010
+ walletName: providerName,
5011
+ signing: transferSigningSigning,
5012
+ error: state.error || transferSigningError,
5013
+ onSign: handlers.onConfirmSign,
5014
+ onLogout: handlers.onLogout
5015
+ }
5016
+ );
5017
+ }
5018
+ if (step === "deposit") {
5019
+ if (state.loadingData) {
5020
+ return /* @__PURE__ */ jsxRuntime.jsx(CenteredSpinner, { label: "Loading..." });
5021
+ }
5022
+ const parsedAmt = depositAmount != null ? depositAmount : 5;
5023
+ return /* @__PURE__ */ jsxRuntime.jsx(
5024
+ DepositScreen,
5025
+ {
5026
+ merchantName,
5027
+ sourceName,
5028
+ sourceAddress,
5029
+ sourceVerified,
5030
+ availableBalance: maxSourceBalance,
5031
+ remainingLimit: selectedAccount?.remainingAllowance ?? state.oneTapLimit,
5032
+ tokenCount,
5033
+ initialAmount: parsedAmt,
5034
+ processing: state.creatingTransfer,
5035
+ error: state.error,
5036
+ onDeposit: handlers.onPay,
5037
+ onChangeSource: () => handlers.onNavigate("wallet-picker"),
5038
+ onSwitchWallet: () => handlers.onNavigate("wallet-picker"),
5039
+ onBack: onBack ?? (() => handlers.onLogout()),
5040
+ onLogout: handlers.onLogout,
5041
+ onIncreaseLimit: handlers.onIncreaseLimit,
5042
+ increasingLimit: state.increasingLimit
5043
+ }
5044
+ );
5045
+ }
5046
+ if (step === "processing") {
5047
+ const polledStatus = pollingTransfer?.status;
5048
+ const transferPhase = state.creatingTransfer ? "creating" : polledStatus === "SENDING" || polledStatus === "SENT" ? "sent" : "verifying";
5049
+ return /* @__PURE__ */ jsxRuntime.jsx(
5050
+ TransferStatusScreen,
5051
+ {
5052
+ phase: transferPhase,
5053
+ error: state.error || authExecutorError || transferSigningError || pollingError,
5054
+ onLogout: handlers.onLogout
5055
+ }
5056
+ );
5057
+ }
5058
+ if (step === "select-source") {
5059
+ return /* @__PURE__ */ jsxRuntime.jsx(
5060
+ SelectSourceScreen,
5061
+ {
5062
+ choices: selectSourceChoices,
5063
+ selectedChainName: selectSourceChainName,
5064
+ selectedTokenSymbol: selectSourceTokenSymbol,
5065
+ recommended: selectSourceRecommended,
5066
+ onChainChange: handlers.onSelectSourceChainChange,
5067
+ onTokenChange: handlers.onSetSelectSourceTokenSymbol,
5068
+ onConfirm: handlers.onConfirmSelectSource,
5069
+ onLogout: handlers.onLogout
5070
+ }
5071
+ );
5072
+ }
5073
+ if (step === "success") {
5074
+ const succeeded = state.transfer?.status === "COMPLETED";
5075
+ const displayAmount = state.transfer?.amount?.amount ?? 0;
5076
+ const displayCurrency = state.transfer?.amount?.currency ?? "USD";
5077
+ return /* @__PURE__ */ jsxRuntime.jsx(
5078
+ SuccessScreen,
5079
+ {
5080
+ amount: displayAmount,
5081
+ currency: displayCurrency,
5082
+ succeeded,
5083
+ error: state.error,
5084
+ merchantName,
5085
+ sourceName,
5086
+ remainingLimit: succeeded ? (() => {
5087
+ const limit = selectedAccount?.remainingAllowance ?? state.oneTapLimit;
5088
+ return limit > displayAmount ? limit - displayAmount : 0;
5089
+ })() : void 0,
5090
+ onDone: onDismiss ?? handlers.onNewPayment,
5091
+ onLogout: handlers.onLogout,
5092
+ autoCloseSeconds
5093
+ }
5094
+ );
5095
+ }
5096
+ if (step === "low-balance") {
5097
+ return /* @__PURE__ */ jsxRuntime.jsx(
5098
+ DepositScreen,
5099
+ {
5100
+ merchantName,
5101
+ sourceName,
5102
+ sourceAddress,
5103
+ sourceVerified,
5104
+ availableBalance: 0,
5105
+ remainingLimit: selectedAccount?.remainingAllowance ?? state.oneTapLimit,
5106
+ tokenCount,
5107
+ initialAmount: depositAmount ?? 5,
5108
+ processing: false,
5109
+ error: state.error,
5110
+ onDeposit: handlers.onPay,
5111
+ onChangeSource: () => handlers.onNavigate("wallet-picker"),
5112
+ onSwitchWallet: () => handlers.onNavigate("wallet-picker"),
5113
+ onBack: onBack ?? (() => handlers.onLogout()),
5114
+ onLogout: handlers.onLogout
5115
+ }
5116
+ );
5117
+ }
5118
+ return null;
5119
+ }
5120
+ var PaymentErrorBoundary = class extends react.Component {
5121
+ constructor(props) {
5122
+ super(props);
5123
+ this.state = { hasError: false };
5124
+ }
4570
5125
  static getDerivedStateFromError() {
4571
5126
  return { hasError: true };
4572
5127
  }
@@ -4630,98 +5185,6 @@ var buttonStyle3 = {
4630
5185
  fontFamily: "inherit",
4631
5186
  cursor: "pointer"
4632
5187
  };
4633
- var ACTIVE_CREDENTIAL_STORAGE_KEY = "swype_active_credential_id";
4634
- var MOBILE_FLOW_STORAGE_KEY = "swype_mobile_flow";
4635
- var MIN_SEND_AMOUNT_USD = 0.25;
4636
- function persistMobileFlowState(data) {
4637
- try {
4638
- sessionStorage.setItem(MOBILE_FLOW_STORAGE_KEY, JSON.stringify(data));
4639
- } catch {
4640
- }
4641
- }
4642
- function loadMobileFlowState() {
4643
- try {
4644
- const raw = sessionStorage.getItem(MOBILE_FLOW_STORAGE_KEY);
4645
- if (!raw) return null;
4646
- return JSON.parse(raw);
4647
- } catch {
4648
- return null;
4649
- }
4650
- }
4651
- function clearMobileFlowState() {
4652
- try {
4653
- sessionStorage.removeItem(MOBILE_FLOW_STORAGE_KEY);
4654
- } catch {
4655
- }
4656
- }
4657
- function computeSmartDefaults(accts, transferAmount) {
4658
- if (accts.length === 0) return null;
4659
- for (const acct of accts) {
4660
- for (const wallet of acct.wallets) {
4661
- if (wallet.status === "ACTIVE") {
4662
- const bestSource = wallet.sources.find(
4663
- (s) => s.balance.available.amount >= transferAmount
4664
- );
4665
- if (bestSource) {
4666
- return { accountId: acct.id, walletId: wallet.id };
4667
- }
4668
- }
4669
- }
4670
- }
4671
- let bestAccount = null;
4672
- let bestWallet = null;
4673
- let bestBalance = -1;
4674
- let bestIsActive = false;
4675
- for (const acct of accts) {
4676
- for (const wallet of acct.wallets) {
4677
- const walletBal = wallet.balance.available.amount;
4678
- const isActive = wallet.status === "ACTIVE";
4679
- if (walletBal > bestBalance || walletBal === bestBalance && isActive && !bestIsActive) {
4680
- bestBalance = walletBal;
4681
- bestAccount = acct;
4682
- bestWallet = wallet;
4683
- bestIsActive = isActive;
4684
- }
4685
- }
4686
- }
4687
- if (bestAccount) {
4688
- return {
4689
- accountId: bestAccount.id,
4690
- walletId: bestWallet?.id ?? null
4691
- };
4692
- }
4693
- return { accountId: accts[0].id, walletId: null };
4694
- }
4695
- function parseRawBalance(rawBalance, decimals) {
4696
- const parsed = Number(rawBalance);
4697
- if (!Number.isFinite(parsed)) return 0;
4698
- return parsed / 10 ** decimals;
4699
- }
4700
- function buildSelectSourceChoices(options) {
4701
- const chainChoices = [];
4702
- const chainIndexByName = /* @__PURE__ */ new Map();
4703
- for (const option of options) {
4704
- const { chainName, tokenSymbol } = option;
4705
- const balance = parseRawBalance(option.rawBalance, option.decimals);
4706
- let chainChoice;
4707
- const existingIdx = chainIndexByName.get(chainName);
4708
- if (existingIdx === void 0) {
4709
- chainChoice = { chainName, balance: 0, tokens: [] };
4710
- chainIndexByName.set(chainName, chainChoices.length);
4711
- chainChoices.push(chainChoice);
4712
- } else {
4713
- chainChoice = chainChoices[existingIdx];
4714
- }
4715
- chainChoice.balance += balance;
4716
- const existing = chainChoice.tokens.find((t) => t.tokenSymbol === tokenSymbol);
4717
- if (existing) {
4718
- existing.balance += balance;
4719
- } else {
4720
- chainChoice.tokens.push({ tokenSymbol, balance });
4721
- }
4722
- }
4723
- return chainChoices;
4724
- }
4725
5188
  function SwypePayment(props) {
4726
5189
  const resetKey = react.useRef(0);
4727
5190
  const handleBoundaryReset = react.useCallback(() => {
@@ -4733,7 +5196,7 @@ function SwypePaymentInner({
4733
5196
  destination,
4734
5197
  onComplete,
4735
5198
  onError,
4736
- useWalletConnector,
5199
+ useWalletConnector: useWalletConnectorProp,
4737
5200
  idempotencyKey,
4738
5201
  merchantAuthorization,
4739
5202
  merchantName,
@@ -4741,7 +5204,7 @@ function SwypePaymentInner({
4741
5204
  onDismiss,
4742
5205
  autoCloseSeconds
4743
5206
  }) {
4744
- const { apiBaseUrl, tokens, depositAmount } = useSwypeConfig();
5207
+ const { apiBaseUrl, depositAmount } = useSwypeConfig();
4745
5208
  const { ready, authenticated, user, logout, getAccessToken } = reactAuth.usePrivy();
4746
5209
  const {
4747
5210
  sendCode: sendEmailCode,
@@ -4753,161 +5216,74 @@ function SwypePaymentInner({
4753
5216
  loginWithCode: loginWithSmsCode,
4754
5217
  state: smsLoginState
4755
5218
  } = reactAuth.useLoginWithSms();
4756
- const { initOAuth } = reactAuth.useLoginWithOAuth();
4757
- const [step, setStep] = react.useState("login");
4758
- const [error, setError] = react.useState(null);
4759
- const [providers, setProviders] = react.useState([]);
4760
- const [accounts, setAccounts] = react.useState([]);
4761
- const [chains, setChains] = react.useState([]);
4762
- const [loadingData, setLoadingData] = react.useState(false);
4763
- const [selectedAccountId, setSelectedAccountId] = react.useState(null);
4764
- const [selectedWalletId, setSelectedWalletId] = react.useState(null);
4765
- const [selectedProviderId, setSelectedProviderId] = react.useState(null);
4766
- const [connectingNewAccount, setConnectingNewAccount] = react.useState(false);
4767
- const [amount, setAmount] = react.useState(
4768
- depositAmount != null ? depositAmount.toString() : ""
4769
- );
4770
- const [transfer, setTransfer] = react.useState(null);
4771
- const [creatingTransfer, setCreatingTransfer] = react.useState(false);
4772
- const [registeringPasskey, setRegisteringPasskey] = react.useState(false);
4773
- const [verifyingPasskeyPopup, setVerifyingPasskeyPopup] = react.useState(false);
4774
- const [passkeyPopupNeeded, setPasskeyPopupNeeded] = react.useState(
4775
- () => isSafari() && isInCrossOriginIframe()
5219
+ reactAuth.useLoginWithOAuth();
5220
+ const [state, dispatch] = react.useReducer(
5221
+ paymentReducer,
5222
+ {
5223
+ depositAmount,
5224
+ passkeyPopupNeeded: isSafari() && isInCrossOriginIframe(),
5225
+ activeCredentialId: typeof window === "undefined" ? null : window.localStorage.getItem(ACTIVE_CREDENTIAL_STORAGE_KEY)
5226
+ },
5227
+ createInitialState
4776
5228
  );
4777
- const [activeCredentialId, setActiveCredentialId] = react.useState(() => {
4778
- if (typeof window === "undefined") return null;
4779
- return window.localStorage.getItem(ACTIVE_CREDENTIAL_STORAGE_KEY);
4780
- });
4781
- const [knownCredentialIds, setKnownCredentialIds] = react.useState([]);
4782
- const [authInput, setAuthInput] = react.useState("");
4783
- const [verificationTarget, setVerificationTarget] = react.useState(null);
4784
- const [otpCode, setOtpCode] = react.useState("");
4785
- const [oneTapLimit, setOneTapLimit] = react.useState(100);
4786
- const [mobileFlow, setMobileFlow] = react.useState(false);
4787
- const [deeplinkUri, setDeeplinkUri] = react.useState(null);
4788
5229
  const loadingDataRef = react.useRef(false);
4789
5230
  const pollingTransferIdRef = react.useRef(null);
4790
- const mobileSigningTransferIdRef = react.useRef(null);
4791
5231
  const mobileSetupFlowRef = react.useRef(false);
4792
5232
  const handlingMobileReturnRef = react.useRef(false);
4793
5233
  const processingStartedAtRef = react.useRef(null);
4794
- const [selectSourceChainName, setSelectSourceChainName] = react.useState("");
4795
- const [selectSourceTokenSymbol, setSelectSourceTokenSymbol] = react.useState("");
4796
5234
  const initializedSelectSourceActionRef = react.useRef(null);
4797
5235
  const preSelectSourceStepRef = react.useRef(null);
4798
- const authExecutor = useAuthorizationExecutor();
4799
- const polling = useTransferPolling();
4800
- const transferSigning = useTransferSigning();
4801
- const sourceType = connectingNewAccount ? "providerId" : selectedWalletId ? "walletId" : selectedAccountId ? "accountId" : "providerId";
4802
- const sourceId = connectingNewAccount ? selectedProviderId ?? "" : selectedWalletId ? selectedWalletId : selectedAccountId ? selectedAccountId : selectedProviderId ?? "";
4803
- const reloadAccounts = react.useCallback(async () => {
4804
- const token = await getAccessToken();
4805
- if (!token || !activeCredentialId) return;
4806
- const [accts, prov] = await Promise.all([
4807
- fetchAccounts(apiBaseUrl, token, activeCredentialId),
4808
- fetchProviders(apiBaseUrl, token)
4809
- ]);
4810
- setAccounts(accts);
4811
- setProviders(prov);
4812
- const parsedAmt = depositAmount != null ? depositAmount : 0;
4813
- const defaults = computeSmartDefaults(accts, parsedAmt);
4814
- if (defaults) {
4815
- setSelectedAccountId(defaults.accountId);
4816
- setSelectedWalletId(defaults.walletId);
4817
- setConnectingNewAccount(false);
4818
- }
4819
- }, [getAccessToken, activeCredentialId, apiBaseUrl, depositAmount]);
4820
- const resetDataLoadingState = react.useCallback(() => {
4821
- loadingDataRef.current = false;
4822
- setLoadingData(false);
4823
- }, []);
4824
- const enterPersistedMobileFlow = react.useCallback((persisted, errorMessage) => {
4825
- setMobileFlow(true);
4826
- setDeeplinkUri(persisted.deeplinkUri);
4827
- setSelectedProviderId(persisted.providerId);
4828
- pollingTransferIdRef.current = persisted.transferId;
4829
- mobileSetupFlowRef.current = persisted.isSetup;
4830
- setError(errorMessage ?? null);
4831
- setStep("open-wallet");
4832
- polling.startPolling(persisted.transferId);
4833
- }, [polling]);
4834
- const handleAuthorizedMobileReturn = react.useCallback(async (authorizedTransfer, isSetup) => {
4835
- if (handlingMobileReturnRef.current) return;
4836
- handlingMobileReturnRef.current = true;
4837
- polling.stopPolling();
4838
- if (isSetup) {
4839
- mobileSetupFlowRef.current = false;
4840
- clearMobileFlowState();
4841
- try {
4842
- await reloadAccounts();
4843
- resetDataLoadingState();
4844
- setTransfer(authorizedTransfer);
4845
- setError(null);
4846
- setDeeplinkUri(null);
4847
- setMobileFlow(false);
4848
- setStep("deposit");
4849
- } catch (err) {
4850
- handlingMobileReturnRef.current = false;
4851
- setError(
4852
- err instanceof Error ? err.message : "Wallet authorized, but we could not refresh your account yet."
4853
- );
4854
- setStep("open-wallet");
5236
+ const [authInput, setAuthInput] = react.useState("");
5237
+ const [otpCode, setOtpCode] = react.useState("");
5238
+ const [selectSourceChainName, setSelectSourceChainName] = react.useState("");
5239
+ const [selectSourceTokenSymbol, setSelectSourceTokenSymbol] = react.useState("");
5240
+ const authExecutor = useAuthorizationExecutor();
5241
+ const polling = useTransferPolling();
5242
+ const transferSigning = useTransferSigning();
5243
+ const { sourceType, sourceId } = deriveSourceTypeAndId(state);
5244
+ const selectedAccount = state.accounts.find((a) => a.id === state.selectedAccountId);
5245
+ const selectedWallet = selectedAccount?.wallets.find(
5246
+ (w) => w.id === state.selectedWalletId
5247
+ );
5248
+ const sourceName = selectedAccount?.name ?? selectedWallet?.chain.name ?? "Wallet";
5249
+ const sourceAddress = selectedWallet ? `${selectedWallet.name.slice(0, 6)}...${selectedWallet.name.slice(-4)}` : void 0;
5250
+ const sourceVerified = selectedWallet?.status === "ACTIVE";
5251
+ const pendingConnections = react.useMemo(
5252
+ () => state.accounts.filter(
5253
+ (a) => a.wallets.length > 0 && !a.wallets.some((w) => w.status === "ACTIVE")
5254
+ ),
5255
+ [state.accounts]
5256
+ );
5257
+ const maxSourceBalance = react.useMemo(() => {
5258
+ let max = 0;
5259
+ for (const acct of state.accounts) {
5260
+ for (const wallet of acct.wallets) {
5261
+ for (const source of wallet.sources) {
5262
+ if (source.balance.available.amount > max) {
5263
+ max = source.balance.available.amount;
5264
+ }
5265
+ }
4855
5266
  }
4856
- return;
4857
- }
4858
- setTransfer(authorizedTransfer);
4859
- mobileSetupFlowRef.current = false;
4860
- clearMobileFlowState();
4861
- setError(null);
4862
- setDeeplinkUri(null);
4863
- setMobileFlow(false);
4864
- setStep("confirm-sign");
4865
- }, [polling.stopPolling, reloadAccounts, resetDataLoadingState]);
4866
- const handleRetryMobileStatus = react.useCallback(() => {
4867
- setError(null);
4868
- handlingMobileReturnRef.current = false;
4869
- const currentTransfer = polling.transfer ?? transfer;
4870
- if (currentTransfer?.status === "AUTHORIZED") {
4871
- void handleAuthorizedMobileReturn(currentTransfer, mobileSetupFlowRef.current);
4872
- return;
4873
- }
4874
- const transferIdToResume = pollingTransferIdRef.current ?? currentTransfer?.id;
4875
- if (transferIdToResume) {
4876
- polling.startPolling(transferIdToResume);
4877
- }
4878
- }, [handleAuthorizedMobileReturn, polling, transfer]);
4879
- react.useEffect(() => {
4880
- if (depositAmount != null) {
4881
- setAmount(depositAmount.toString());
4882
5267
  }
4883
- }, [depositAmount]);
4884
- const resetHeadlessLogin = react.useCallback(() => {
4885
- setAuthInput("");
4886
- setVerificationTarget(null);
4887
- setOtpCode("");
4888
- }, []);
4889
- react.useCallback(async (provider) => {
4890
- setError(null);
4891
- try {
4892
- await initOAuth({ provider });
4893
- } catch (err) {
4894
- captureException(err);
4895
- setError(err instanceof Error ? err.message : "Social login failed");
5268
+ return max;
5269
+ }, [state.accounts]);
5270
+ const tokenCount = react.useMemo(() => {
5271
+ let count = 0;
5272
+ for (const acct of state.accounts) {
5273
+ for (const wallet of acct.wallets) {
5274
+ count += wallet.sources.length;
5275
+ }
4896
5276
  }
4897
- }, [initOAuth]);
4898
- const activeOtpStatus = verificationTarget?.kind === "email" ? emailLoginState.status : verificationTarget?.kind === "phone" ? smsLoginState.status : "initial";
4899
- const activeOtpErrorMessage = verificationTarget?.kind === "email" && emailLoginState.status === "error" ? emailLoginState.error?.message ?? "Failed to continue with email." : verificationTarget?.kind === "phone" && smsLoginState.status === "error" ? smsLoginState.error?.message ?? "Failed to continue with phone number." : null;
4900
- react.useEffect(() => {
4901
- if (authenticated) return;
4902
- if (activeOtpErrorMessage) setError(activeOtpErrorMessage);
4903
- }, [activeOtpErrorMessage, authenticated]);
5277
+ return count;
5278
+ }, [state.accounts]);
5279
+ const activeOtpStatus = state.verificationTarget?.kind === "email" ? emailLoginState.status : state.verificationTarget?.kind === "phone" ? smsLoginState.status : "initial";
5280
+ const activeOtpErrorMessage = state.verificationTarget?.kind === "email" && emailLoginState.status === "error" ? emailLoginState.error?.message ?? "Failed to continue with email." : state.verificationTarget?.kind === "phone" && smsLoginState.status === "error" ? smsLoginState.error?.message ?? "Failed to continue with phone number." : null;
4904
5281
  const handleSendLoginCode = react.useCallback(async () => {
4905
5282
  const normalizedIdentifier = normalizeAuthIdentifier(authInput);
4906
5283
  if (!normalizedIdentifier) {
4907
- setError("Enter a valid email address or phone number.");
5284
+ dispatch({ type: "SET_ERROR", error: "Enter a valid email address or phone number." });
4908
5285
  return;
4909
5286
  }
4910
- setError(null);
4911
5287
  setOtpCode("");
4912
5288
  try {
4913
5289
  if (normalizedIdentifier.kind === "email") {
@@ -4915,468 +5291,214 @@ function SwypePaymentInner({
4915
5291
  } else {
4916
5292
  await sendSmsCode({ phoneNumber: normalizedIdentifier.value });
4917
5293
  }
4918
- setVerificationTarget(normalizedIdentifier);
4919
- setStep("otp-verify");
5294
+ dispatch({ type: "CODE_SENT", target: normalizedIdentifier });
4920
5295
  } catch (err) {
4921
5296
  captureException(err);
4922
- setError(err instanceof Error ? err.message : "Failed to send verification code");
5297
+ dispatch({
5298
+ type: "SET_ERROR",
5299
+ error: err instanceof Error ? err.message : "Failed to send verification code"
5300
+ });
4923
5301
  }
4924
5302
  }, [authInput, sendEmailCode, sendSmsCode]);
4925
5303
  const handleVerifyLoginCode = react.useCallback(async () => {
4926
- if (!verificationTarget) return;
5304
+ if (!state.verificationTarget) return;
4927
5305
  const trimmedCode = otpCode.trim();
4928
5306
  if (!/^\d{6}$/.test(trimmedCode)) {
4929
- setError("Enter the 6-digit verification code.");
5307
+ dispatch({ type: "SET_ERROR", error: "Enter the 6-digit verification code." });
4930
5308
  return;
4931
5309
  }
4932
- setError(null);
5310
+ dispatch({ type: "SET_ERROR", error: null });
4933
5311
  try {
4934
- if (verificationTarget.kind === "email") {
5312
+ if (state.verificationTarget.kind === "email") {
4935
5313
  await loginWithEmailCode({ code: trimmedCode });
4936
5314
  } else {
4937
5315
  await loginWithSmsCode({ code: trimmedCode });
4938
5316
  }
4939
5317
  } catch (err) {
4940
5318
  captureException(err);
4941
- setError(err instanceof Error ? err.message : "Failed to verify code");
4942
- }
4943
- }, [verificationTarget, otpCode, loginWithEmailCode, loginWithSmsCode]);
4944
- react.useEffect(() => {
4945
- if (step === "otp-verify" && /^\d{6}$/.test(otpCode.trim()) && activeOtpStatus !== "submitting-code") {
4946
- handleVerifyLoginCode();
5319
+ dispatch({
5320
+ type: "SET_ERROR",
5321
+ error: err instanceof Error ? err.message : "Failed to verify code"
5322
+ });
4947
5323
  }
4948
- }, [otpCode, step, activeOtpStatus, handleVerifyLoginCode]);
5324
+ }, [state.verificationTarget, otpCode, loginWithEmailCode, loginWithSmsCode]);
4949
5325
  const handleResendLoginCode = react.useCallback(async () => {
4950
- if (!verificationTarget) return;
4951
- setError(null);
5326
+ if (!state.verificationTarget) return;
5327
+ dispatch({ type: "SET_ERROR", error: null });
4952
5328
  try {
4953
- if (verificationTarget.kind === "email") {
4954
- await sendEmailCode({ email: verificationTarget.value });
5329
+ if (state.verificationTarget.kind === "email") {
5330
+ await sendEmailCode({ email: state.verificationTarget.value });
4955
5331
  } else {
4956
- await sendSmsCode({ phoneNumber: verificationTarget.value });
5332
+ await sendSmsCode({ phoneNumber: state.verificationTarget.value });
4957
5333
  }
4958
5334
  } catch (err) {
4959
5335
  captureException(err);
4960
- setError(err instanceof Error ? err.message : "Failed to resend code");
5336
+ dispatch({
5337
+ type: "SET_ERROR",
5338
+ error: err instanceof Error ? err.message : "Failed to resend code"
5339
+ });
4961
5340
  }
4962
- }, [verificationTarget, sendEmailCode, sendSmsCode]);
4963
- react.useEffect(() => {
4964
- if (!ready || !authenticated) return;
4965
- if (step !== "login" && step !== "otp-verify") return;
4966
- let cancelled = false;
4967
- setError(null);
4968
- resetHeadlessLogin();
4969
- const restoreOrDeposit = async (credId, token) => {
4970
- const persisted = loadMobileFlowState();
4971
- let accts = [];
4972
- try {
4973
- accts = await fetchAccounts(apiBaseUrl, token, credId);
4974
- if (cancelled) return;
4975
- } catch {
5341
+ }, [state.verificationTarget, sendEmailCode, sendSmsCode]);
5342
+ const completePasskeyRegistration = react.useCallback(async (credentialId, publicKey) => {
5343
+ const token = await getAccessToken();
5344
+ if (!token) throw new Error("Not authenticated");
5345
+ await registerPasskey(apiBaseUrl, token, credentialId, publicKey);
5346
+ dispatch({ type: "PASSKEY_ACTIVATED", credentialId });
5347
+ window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
5348
+ const resolved = resolvePostAuthStep({
5349
+ hasPasskey: true,
5350
+ accounts: state.accounts,
5351
+ persistedMobileFlow: loadMobileFlowState(),
5352
+ mobileSetupInProgress: mobileSetupFlowRef.current,
5353
+ connectingNewAccount: state.connectingNewAccount
5354
+ });
5355
+ if (resolved.clearPersistedFlow) clearMobileFlowState();
5356
+ dispatch({ type: "NAVIGATE", step: resolved.step });
5357
+ }, [getAccessToken, apiBaseUrl, state.accounts, state.connectingNewAccount]);
5358
+ const handleRegisterPasskey = react.useCallback(async () => {
5359
+ dispatch({ type: "SET_REGISTERING_PASSKEY", value: true });
5360
+ dispatch({ type: "SET_ERROR", error: null });
5361
+ try {
5362
+ const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
5363
+ const { credentialId, publicKey } = await createPasskeyCredential({
5364
+ userId: user?.id ?? "unknown",
5365
+ displayName: passkeyDisplayName
5366
+ });
5367
+ await completePasskeyRegistration(credentialId, publicKey);
5368
+ } catch (err) {
5369
+ if (err instanceof PasskeyIframeBlockedError) {
5370
+ dispatch({ type: "SET_PASSKEY_POPUP_NEEDED", needed: true });
5371
+ } else {
5372
+ captureException(err);
5373
+ dispatch({
5374
+ type: "SET_ERROR",
5375
+ error: err instanceof Error ? err.message : "Failed to register passkey"
5376
+ });
4976
5377
  }
5378
+ } finally {
5379
+ dispatch({ type: "SET_REGISTERING_PASSKEY", value: false });
5380
+ }
5381
+ }, [user, completePasskeyRegistration]);
5382
+ const handleCreatePasskeyViaPopup = react.useCallback(async () => {
5383
+ dispatch({ type: "SET_REGISTERING_PASSKEY", value: true });
5384
+ dispatch({ type: "SET_ERROR", error: null });
5385
+ try {
5386
+ const token = await getAccessToken();
5387
+ const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
5388
+ const popupOptions = buildPasskeyPopupOptions({
5389
+ userId: user?.id ?? "unknown",
5390
+ displayName: passkeyDisplayName,
5391
+ authToken: token ?? void 0,
5392
+ apiBaseUrl
5393
+ });
5394
+ const { credentialId } = await createPasskeyViaPopup(popupOptions);
5395
+ dispatch({ type: "PASSKEY_ACTIVATED", credentialId });
5396
+ localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
4977
5397
  const resolved = resolvePostAuthStep({
4978
5398
  hasPasskey: true,
4979
- accounts: accts,
4980
- persistedMobileFlow: persisted,
4981
- mobileSetupInProgress: false,
4982
- connectingNewAccount: false
5399
+ accounts: state.accounts,
5400
+ persistedMobileFlow: loadMobileFlowState(),
5401
+ mobileSetupInProgress: mobileSetupFlowRef.current,
5402
+ connectingNewAccount: state.connectingNewAccount
4983
5403
  });
4984
- if (resolved.clearPersistedFlow) {
4985
- clearMobileFlowState();
4986
- }
4987
- if (resolved.step === "deposit" && persisted && persisted.isSetup) {
4988
- try {
4989
- const existingTransfer = await fetchTransfer(apiBaseUrl, token, persisted.transferId);
4990
- if (cancelled) return;
4991
- if (existingTransfer.status === "AUTHORIZED") {
4992
- await handleAuthorizedMobileReturn(existingTransfer, true);
4993
- return;
4994
- }
4995
- } catch {
4996
- }
4997
- }
4998
- if (resolved.step === "open-wallet" && persisted) {
4999
- try {
5000
- const existingTransfer = await fetchTransfer(apiBaseUrl, token, persisted.transferId);
5001
- if (cancelled) return;
5002
- const mobileResolution = resolveRestoredMobileFlow(
5003
- existingTransfer.status,
5004
- persisted.isSetup
5005
- );
5006
- if (mobileResolution.kind === "resume-setup-deposit") {
5007
- await handleAuthorizedMobileReturn(existingTransfer, true);
5008
- return;
5009
- }
5010
- if (mobileResolution.kind === "resume-confirm-sign") {
5011
- await handleAuthorizedMobileReturn(existingTransfer, false);
5012
- return;
5013
- }
5014
- if (mobileResolution.kind === "resume-success") {
5015
- clearMobileFlowState();
5016
- setMobileFlow(false);
5017
- setDeeplinkUri(null);
5018
- setTransfer(existingTransfer);
5019
- setError(null);
5020
- setStep("success");
5021
- onComplete?.(existingTransfer);
5022
- return;
5023
- }
5024
- if (mobileResolution.kind === "resume-failed") {
5025
- clearMobileFlowState();
5026
- setMobileFlow(false);
5027
- setDeeplinkUri(null);
5028
- setTransfer(existingTransfer);
5029
- setError("Transfer failed.");
5030
- setStep("success");
5031
- return;
5032
- }
5033
- if (mobileResolution.kind === "resume-processing") {
5034
- clearMobileFlowState();
5035
- setMobileFlow(false);
5036
- setDeeplinkUri(null);
5037
- setTransfer(existingTransfer);
5038
- setError(null);
5039
- setStep("processing");
5040
- polling.startPolling(existingTransfer.id);
5041
- return;
5042
- }
5043
- if (mobileResolution.kind === "resume-stale-setup") {
5044
- clearMobileFlowState();
5045
- if (!cancelled) setStep("wallet-picker");
5046
- return;
5047
- }
5048
- } catch (err) {
5049
- if (cancelled) return;
5050
- enterPersistedMobileFlow(
5051
- persisted,
5052
- err instanceof Error ? err.message : "Unable to refresh wallet authorization status."
5053
- );
5054
- return;
5055
- }
5056
- enterPersistedMobileFlow(persisted);
5057
- return;
5058
- }
5059
- setStep(resolved.step);
5060
- };
5061
- const checkPasskey = async () => {
5062
- try {
5063
- const token = await getAccessToken();
5064
- if (!token || cancelled) return;
5065
- const { config } = await fetchUserConfig(apiBaseUrl, token);
5066
- if (cancelled) return;
5067
- if (config.defaultAllowance != null) {
5068
- setOneTapLimit(config.defaultAllowance);
5069
- }
5070
- const allPasskeys = config.passkeys ?? (config.passkey ? [config.passkey] : []);
5071
- setKnownCredentialIds(allPasskeys.map((p) => p.credentialId));
5072
- if (allPasskeys.length === 0) {
5073
- setStep("create-passkey");
5074
- return;
5075
- }
5076
- if (activeCredentialId && allPasskeys.some((p) => p.credentialId === activeCredentialId)) {
5077
- await restoreOrDeposit(activeCredentialId, token);
5078
- return;
5079
- }
5080
- if (cancelled) return;
5081
- const credentialIds = allPasskeys.map((p) => p.credentialId);
5082
- if (isSafari() && isInCrossOriginIframe()) {
5083
- setStep("verify-passkey");
5084
- return;
5085
- }
5086
- const matched = await findDevicePasskey(credentialIds);
5087
- if (cancelled) return;
5088
- if (matched) {
5089
- setActiveCredentialId(matched);
5090
- window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
5404
+ if (resolved.clearPersistedFlow) clearMobileFlowState();
5405
+ dispatch({ type: "NAVIGATE", step: resolved.step });
5406
+ } catch (err) {
5407
+ captureException(err);
5408
+ dispatch({
5409
+ type: "SET_ERROR",
5410
+ error: err instanceof Error ? err.message : "Failed to register passkey"
5411
+ });
5412
+ } finally {
5413
+ dispatch({ type: "SET_REGISTERING_PASSKEY", value: false });
5414
+ }
5415
+ }, [user, getAccessToken, apiBaseUrl, state.accounts, state.connectingNewAccount]);
5416
+ const handleVerifyPasskeyViaPopup = react.useCallback(async () => {
5417
+ dispatch({ type: "SET_VERIFYING_PASSKEY", value: true });
5418
+ dispatch({ type: "SET_ERROR", error: null });
5419
+ try {
5420
+ const token = await getAccessToken();
5421
+ const matched = await findDevicePasskeyViaPopup({
5422
+ credentialIds: state.knownCredentialIds,
5423
+ rpId: resolvePasskeyRpId(),
5424
+ authToken: token ?? void 0,
5425
+ apiBaseUrl
5426
+ });
5427
+ if (matched) {
5428
+ dispatch({ type: "PASSKEY_ACTIVATED", credentialId: matched });
5429
+ window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
5430
+ if (token) {
5091
5431
  reportPasskeyActivity(apiBaseUrl, token, matched).catch(() => {
5092
5432
  });
5093
- await restoreOrDeposit(matched, token);
5094
- return;
5095
5433
  }
5096
- setStep("create-passkey");
5097
- } catch {
5098
- if (!cancelled) setStep("create-passkey");
5434
+ dispatch({ type: "NAVIGATE", step: "login" });
5435
+ } else {
5436
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5099
5437
  }
5100
- };
5101
- checkPasskey();
5102
- return () => {
5103
- cancelled = true;
5104
- };
5105
- }, [
5106
- ready,
5107
- authenticated,
5108
- step,
5109
- apiBaseUrl,
5110
- getAccessToken,
5111
- activeCredentialId,
5112
- resetHeadlessLogin,
5113
- enterPersistedMobileFlow,
5114
- handleAuthorizedMobileReturn,
5115
- onComplete
5116
- ]);
5117
- react.useEffect(() => {
5118
- const loadAction = resolveDataLoadAction({
5119
- authenticated,
5120
- step,
5121
- accountsCount: accounts.length,
5122
- hasActiveCredential: !!activeCredentialId,
5123
- loading: loadingDataRef.current
5124
- });
5125
- if (loadAction === "reset") {
5126
- resetDataLoadingState();
5127
- return;
5128
- }
5129
- if (loadAction === "wait") {
5130
- return;
5131
- }
5132
- const credentialId = activeCredentialId;
5133
- if (!credentialId) {
5134
- resetDataLoadingState();
5135
- return;
5438
+ } catch {
5439
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5440
+ } finally {
5441
+ dispatch({ type: "SET_VERIFYING_PASSKEY", value: false });
5136
5442
  }
5137
- let cancelled = false;
5138
- loadingDataRef.current = true;
5139
- const load = async () => {
5140
- setLoadingData(true);
5141
- setError(null);
5443
+ }, [state.knownCredentialIds, getAccessToken, apiBaseUrl]);
5444
+ const reloadAccounts = react.useCallback(async () => {
5445
+ const token = await getAccessToken();
5446
+ if (!token || !state.activeCredentialId) return;
5447
+ const [accts, prov] = await Promise.all([
5448
+ fetchAccounts(apiBaseUrl, token, state.activeCredentialId),
5449
+ fetchProviders(apiBaseUrl, token)
5450
+ ]);
5451
+ const parsedAmt = depositAmount != null ? depositAmount : 0;
5452
+ const defaults = computeSmartDefaults(accts, parsedAmt);
5453
+ dispatch({ type: "ACCOUNTS_RELOADED", accounts: accts, providers: prov, defaults });
5454
+ }, [getAccessToken, state.activeCredentialId, apiBaseUrl, depositAmount]);
5455
+ const handleAuthorizedMobileReturn = react.useCallback(async (authorizedTransfer, isSetup) => {
5456
+ if (handlingMobileReturnRef.current) return;
5457
+ handlingMobileReturnRef.current = true;
5458
+ polling.stopPolling();
5459
+ if (isSetup) {
5460
+ mobileSetupFlowRef.current = false;
5461
+ clearMobileFlowState();
5142
5462
  try {
5143
- const token = await getAccessToken();
5144
- if (!token) throw new Error("Not authenticated");
5145
- const [prov, accts, chn] = await Promise.all([
5146
- fetchProviders(apiBaseUrl, token),
5147
- fetchAccounts(apiBaseUrl, token, credentialId),
5148
- fetchChains(apiBaseUrl, token)
5149
- ]);
5150
- if (cancelled) return;
5151
- setProviders(prov);
5152
- setAccounts(accts);
5153
- setChains(chn);
5154
- const parsedAmt = depositAmount != null ? depositAmount : 0;
5155
- const defaults = computeSmartDefaults(accts, parsedAmt);
5156
- if (defaults) {
5157
- setSelectedAccountId(defaults.accountId);
5158
- setSelectedWalletId(defaults.walletId);
5159
- } else if (prov.length > 0 && !connectingNewAccount) {
5160
- setSelectedProviderId(prov[0].id);
5161
- }
5162
- const persisted = loadMobileFlowState();
5163
- const resolved = resolvePostAuthStep({
5164
- hasPasskey: !!activeCredentialId,
5165
- accounts: accts,
5166
- persistedMobileFlow: persisted,
5167
- mobileSetupInProgress: mobileSetupFlowRef.current,
5168
- connectingNewAccount
5169
- });
5170
- if (resolved.clearPersistedFlow) {
5171
- clearMobileFlowState();
5172
- setMobileFlow(false);
5173
- setDeeplinkUri(null);
5174
- }
5175
- const correctableSteps = ["deposit", "wallet-picker", "open-wallet"];
5176
- if (correctableSteps.includes(step)) {
5177
- setStep(resolved.step);
5178
- }
5463
+ await reloadAccounts();
5464
+ loadingDataRef.current = false;
5465
+ dispatch({ type: "MOBILE_SETUP_COMPLETE", transfer: authorizedTransfer });
5179
5466
  } catch (err) {
5180
- if (!cancelled) {
5181
- captureException(err);
5182
- setError(err instanceof Error ? err.message : "Failed to load data");
5183
- }
5184
- } finally {
5185
- if (!cancelled) {
5186
- resetDataLoadingState();
5187
- }
5188
- }
5189
- };
5190
- load();
5191
- return () => {
5192
- cancelled = true;
5193
- loadingDataRef.current = false;
5194
- };
5195
- }, [authenticated, step, accounts.length, apiBaseUrl, getAccessToken, activeCredentialId, depositAmount, connectingNewAccount, resetDataLoadingState]);
5196
- react.useEffect(() => {
5197
- if (!polling.transfer) return;
5198
- if (polling.transfer.status === "COMPLETED") {
5199
- clearMobileFlowState();
5200
- setStep("success");
5201
- setTransfer(polling.transfer);
5202
- onComplete?.(polling.transfer);
5203
- } else if (polling.transfer.status === "FAILED") {
5204
- clearMobileFlowState();
5205
- setStep("success");
5206
- setTransfer(polling.transfer);
5207
- setError("Transfer failed.");
5208
- }
5209
- }, [polling.transfer, onComplete]);
5210
- react.useEffect(() => {
5211
- if (step !== "processing") {
5212
- processingStartedAtRef.current = null;
5213
- return;
5214
- }
5215
- if (!processingStartedAtRef.current) {
5216
- processingStartedAtRef.current = Date.now();
5217
- }
5218
- const elapsedMs = Date.now() - processingStartedAtRef.current;
5219
- const remainingMs = PROCESSING_TIMEOUT_MS - elapsedMs;
5220
- const handleTimeout = () => {
5221
- if (!hasProcessingTimedOut(processingStartedAtRef.current, Date.now())) return;
5222
- const status = getTransferStatus(polling.transfer, transfer);
5223
- const msg = buildProcessingTimeoutMessage(status);
5224
- captureException(new Error(msg));
5225
- polling.stopPolling();
5226
- setStep("deposit");
5227
- setError(msg);
5228
- onError?.(msg);
5229
- };
5230
- if (remainingMs <= 0) {
5231
- handleTimeout();
5232
- return;
5233
- }
5234
- const timeoutId = window.setTimeout(handleTimeout, remainingMs);
5235
- return () => window.clearTimeout(timeoutId);
5236
- }, [step, polling.transfer, transfer, polling.stopPolling, onError]);
5237
- react.useEffect(() => {
5238
- if (!mobileFlow) {
5239
- handlingMobileReturnRef.current = false;
5240
- return;
5241
- }
5242
- if (handlingMobileReturnRef.current) return;
5243
- const polledTransfer = polling.transfer;
5244
- if (!polledTransfer || polledTransfer.status !== "AUTHORIZED") return;
5245
- void handleAuthorizedMobileReturn(polledTransfer, mobileSetupFlowRef.current);
5246
- }, [mobileFlow, polling.transfer, handleAuthorizedMobileReturn]);
5247
- react.useEffect(() => {
5248
- if (!mobileFlow) return;
5249
- if (handlingMobileReturnRef.current) return;
5250
- const transferIdToResume = pollingTransferIdRef.current ?? transfer?.id;
5251
- if (!transferIdToResume) return;
5252
- if (!polling.isPolling) polling.startPolling(transferIdToResume);
5253
- const handleVisibility = () => {
5254
- if (document.visibilityState === "visible" && !handlingMobileReturnRef.current) {
5255
- polling.startPolling(transferIdToResume);
5467
+ handlingMobileReturnRef.current = false;
5468
+ dispatch({
5469
+ type: "SET_ERROR",
5470
+ error: err instanceof Error ? err.message : "Wallet authorized, but we could not refresh your account yet."
5471
+ });
5472
+ dispatch({ type: "NAVIGATE", step: "open-wallet" });
5256
5473
  }
5257
- };
5258
- document.addEventListener("visibilitychange", handleVisibility);
5259
- return () => document.removeEventListener("visibilitychange", handleVisibility);
5260
- }, [mobileFlow, transfer?.id, polling.isPolling, polling.startPolling]);
5261
- const pendingSelectSourceAction = authExecutor.pendingSelectSource;
5262
- const selectSourceChoices = react.useMemo(() => {
5263
- if (!pendingSelectSourceAction) return [];
5264
- const options = pendingSelectSourceAction.metadata?.options ?? [];
5265
- return buildSelectSourceChoices(options);
5266
- }, [pendingSelectSourceAction]);
5267
- const selectSourceRecommended = react.useMemo(() => {
5268
- if (!pendingSelectSourceAction) return null;
5269
- return pendingSelectSourceAction.metadata?.recommended ?? null;
5270
- }, [pendingSelectSourceAction]);
5271
- react.useEffect(() => {
5272
- if (!pendingSelectSourceAction) {
5273
- initializedSelectSourceActionRef.current = null;
5274
- setSelectSourceChainName("");
5275
- setSelectSourceTokenSymbol("");
5276
5474
  return;
5277
5475
  }
5278
- if (initializedSelectSourceActionRef.current === pendingSelectSourceAction.id) return;
5279
- const hasRecommended = !!selectSourceRecommended && selectSourceChoices.some(
5280
- (chain) => chain.chainName === selectSourceRecommended.chainName && chain.tokens.some((t) => t.tokenSymbol === selectSourceRecommended.tokenSymbol)
5281
- );
5282
- if (hasRecommended && selectSourceRecommended) {
5283
- setSelectSourceChainName(selectSourceRecommended.chainName);
5284
- setSelectSourceTokenSymbol(selectSourceRecommended.tokenSymbol);
5285
- } else if (selectSourceChoices.length > 0 && selectSourceChoices[0].tokens.length > 0) {
5286
- setSelectSourceChainName(selectSourceChoices[0].chainName);
5287
- setSelectSourceTokenSymbol(selectSourceChoices[0].tokens[0].tokenSymbol);
5288
- } else {
5289
- setSelectSourceChainName("Base");
5290
- setSelectSourceTokenSymbol("USDC");
5291
- }
5292
- initializedSelectSourceActionRef.current = pendingSelectSourceAction.id;
5293
- }, [pendingSelectSourceAction, selectSourceChoices, selectSourceRecommended]);
5294
- react.useEffect(() => {
5295
- if (pendingSelectSourceAction && step === "processing") {
5296
- preSelectSourceStepRef.current = step;
5297
- setStep("select-source");
5298
- } else if (!pendingSelectSourceAction && step === "select-source") {
5299
- setStep(preSelectSourceStepRef.current ?? "processing");
5300
- preSelectSourceStepRef.current = null;
5301
- }
5302
- }, [pendingSelectSourceAction, step]);
5303
- const handleSelectSourceChainChange = react.useCallback(
5304
- (chainName) => {
5305
- setSelectSourceChainName(chainName);
5306
- const chain = selectSourceChoices.find((c) => c.chainName === chainName);
5307
- if (!chain || chain.tokens.length === 0) return;
5308
- const recommendedToken = selectSourceRecommended?.chainName === chainName ? selectSourceRecommended.tokenSymbol : null;
5309
- const hasRecommended = !!recommendedToken && chain.tokens.some((t) => t.tokenSymbol === recommendedToken);
5310
- setSelectSourceTokenSymbol(
5311
- hasRecommended ? recommendedToken : chain.tokens[0].tokenSymbol
5312
- );
5313
- },
5314
- [selectSourceChoices, selectSourceRecommended]
5315
- );
5316
- const pendingConnections = react.useMemo(
5317
- () => accounts.filter(
5318
- (a) => a.wallets.length > 0 && !a.wallets.some((w) => w.status === "ACTIVE")
5319
- ),
5320
- [accounts]
5321
- );
5322
- const selectedAccount = accounts.find((a) => a.id === selectedAccountId);
5323
- const selectedWallet = selectedAccount?.wallets.find((w) => w.id === selectedWalletId);
5324
- const sourceName = selectedAccount?.name ?? selectedWallet?.chain.name ?? "Wallet";
5325
- const sourceAddress = selectedWallet ? `${selectedWallet.name.slice(0, 6)}...${selectedWallet.name.slice(-4)}` : void 0;
5326
- const sourceVerified = selectedWallet?.status === "ACTIVE";
5327
- const maxSourceBalance = react.useMemo(() => {
5328
- let max = 0;
5329
- for (const acct of accounts) {
5330
- for (const wallet of acct.wallets) {
5331
- for (const source of wallet.sources) {
5332
- if (source.balance.available.amount > max) {
5333
- max = source.balance.available.amount;
5334
- }
5335
- }
5336
- }
5337
- }
5338
- return max;
5339
- }, [accounts]);
5340
- const tokenCount = react.useMemo(() => {
5341
- let count = 0;
5342
- for (const acct of accounts) {
5343
- for (const wallet of acct.wallets) {
5344
- count += wallet.sources.length;
5345
- }
5346
- }
5347
- return count;
5348
- }, [accounts]);
5349
- const handlePay = react.useCallback(async (depositAmount2, sourceOverrides) => {
5350
- const parsedAmount = depositAmount2;
5351
- if (isNaN(parsedAmount) || parsedAmount < MIN_SEND_AMOUNT_USD) {
5352
- setError(`Minimum amount is $${MIN_SEND_AMOUNT_USD.toFixed(2)}.`);
5476
+ mobileSetupFlowRef.current = false;
5477
+ clearMobileFlowState();
5478
+ dispatch({ type: "MOBILE_SIGN_READY", transfer: authorizedTransfer });
5479
+ }, [polling.stopPolling, reloadAccounts]);
5480
+ const handlePay = react.useCallback(async (payAmount, sourceOverrides) => {
5481
+ if (isNaN(payAmount) || payAmount < MIN_SEND_AMOUNT_USD) {
5482
+ dispatch({ type: "SET_ERROR", error: `Minimum amount is $${MIN_SEND_AMOUNT_USD.toFixed(2)}.` });
5353
5483
  return;
5354
5484
  }
5355
5485
  if (!sourceOverrides?.sourceId && !sourceId) {
5356
- setError("No account or provider selected.");
5486
+ dispatch({ type: "SET_ERROR", error: "No account or provider selected." });
5357
5487
  return;
5358
5488
  }
5359
- if (!activeCredentialId) {
5360
- setError("Create a passkey on this device before continuing.");
5361
- setStep("create-passkey");
5489
+ if (!state.activeCredentialId) {
5490
+ dispatch({ type: "SET_ERROR", error: "Create a passkey on this device before continuing." });
5491
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5362
5492
  return;
5363
5493
  }
5364
5494
  const isSetupRedirect = mobileSetupFlowRef.current;
5365
- if (isSetupRedirect) {
5366
- setStep("open-wallet");
5367
- } else {
5368
- setStep("processing");
5369
- }
5495
+ dispatch({ type: "PAY_STARTED", isSetupRedirect });
5370
5496
  processingStartedAtRef.current = Date.now();
5371
- setError(null);
5372
- setCreatingTransfer(true);
5373
- setDeeplinkUri(null);
5374
- setMobileFlow(false);
5375
5497
  try {
5376
- if (transfer?.status === "AUTHORIZED") {
5377
- const signedTransfer2 = await transferSigning.signTransfer(transfer.id);
5378
- setTransfer(signedTransfer2);
5379
- polling.startPolling(transfer.id);
5498
+ if (state.transfer?.status === "AUTHORIZED") {
5499
+ const signedTransfer2 = await transferSigning.signTransfer(state.transfer.id);
5500
+ dispatch({ type: "TRANSFER_SIGNED", transfer: signedTransfer2 });
5501
+ polling.startPolling(state.transfer.id);
5380
5502
  return;
5381
5503
  }
5382
5504
  const token = await getAccessToken();
@@ -5384,21 +5506,21 @@ function SwypePaymentInner({
5384
5506
  let effectiveSourceType = sourceOverrides?.sourceType ?? sourceType;
5385
5507
  let effectiveSourceId = sourceOverrides?.sourceId ?? sourceId;
5386
5508
  if (effectiveSourceType === "accountId") {
5387
- const acct = accounts.find((a) => a.id === effectiveSourceId);
5509
+ const acct = state.accounts.find((a) => a.id === effectiveSourceId);
5388
5510
  const activeWallet = acct?.wallets.find((w) => w.status === "ACTIVE");
5389
5511
  if (activeWallet) {
5390
5512
  effectiveSourceType = "walletId";
5391
5513
  effectiveSourceId = activeWallet.id;
5392
5514
  }
5393
5515
  }
5394
- const isActiveWallet = effectiveSourceType === "walletId" && accounts.some(
5516
+ const isActiveWallet = effectiveSourceType === "walletId" && state.accounts.some(
5395
5517
  (a) => a.wallets.some((w) => w.id === effectiveSourceId && w.status === "ACTIVE")
5396
5518
  );
5397
5519
  if (!isActiveWallet && !isSetupRedirect) {
5398
5520
  let found = false;
5399
- for (const acct of accounts) {
5521
+ for (const acct of state.accounts) {
5400
5522
  for (const wallet of acct.wallets) {
5401
- if (wallet.status === "ACTIVE" && wallet.sources.some((s) => s.balance.available.amount >= parsedAmount)) {
5523
+ if (wallet.status === "ACTIVE" && wallet.sources.some((s) => s.balance.available.amount >= payAmount)) {
5402
5524
  effectiveSourceType = "walletId";
5403
5525
  effectiveSourceId = wallet.id;
5404
5526
  found = true;
@@ -5410,30 +5532,28 @@ function SwypePaymentInner({
5410
5532
  }
5411
5533
  const t = await createTransfer(apiBaseUrl, token, {
5412
5534
  id: idempotencyKey,
5413
- credentialId: activeCredentialId,
5535
+ credentialId: state.activeCredentialId,
5414
5536
  merchantAuthorization,
5415
5537
  sourceType: effectiveSourceType,
5416
5538
  sourceId: effectiveSourceId,
5417
5539
  destination,
5418
- amount: parsedAmount
5540
+ amount: payAmount
5419
5541
  });
5420
- setTransfer(t);
5542
+ dispatch({ type: "TRANSFER_CREATED", transfer: t });
5421
5543
  if (t.authorizationSessions && t.authorizationSessions.length > 0) {
5422
- const shouldUseConnector = shouldUseWalletConnector({
5423
- useWalletConnector,
5544
+ const useConnector = shouldUseWalletConnector({
5545
+ useWalletConnector: useWalletConnectorProp,
5424
5546
  userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent
5425
5547
  });
5426
- if (!shouldUseConnector) {
5548
+ if (!useConnector) {
5427
5549
  const uri = t.authorizationSessions[0].uri;
5428
- setMobileFlow(true);
5429
5550
  pollingTransferIdRef.current = t.id;
5430
5551
  polling.startPolling(t.id);
5431
- setDeeplinkUri(uri);
5432
- setStep("open-wallet");
5552
+ dispatch({ type: "MOBILE_DEEPLINK_READY", deeplinkUri: uri });
5433
5553
  persistMobileFlowState({
5434
5554
  transferId: t.id,
5435
5555
  deeplinkUri: uri,
5436
- providerId: sourceOverrides?.sourceType === "providerId" ? sourceOverrides.sourceId : selectedProviderId,
5556
+ providerId: sourceOverrides?.sourceType === "providerId" ? sourceOverrides.sourceId : state.selectedProviderId,
5437
5557
  isSetup: mobileSetupFlowRef.current
5438
5558
  });
5439
5559
  triggerDeeplink(uri);
@@ -5443,55 +5563,58 @@ function SwypePaymentInner({
5443
5563
  }
5444
5564
  }
5445
5565
  const signedTransfer = await transferSigning.signTransfer(t.id);
5446
- setTransfer(signedTransfer);
5566
+ dispatch({ type: "TRANSFER_SIGNED", transfer: signedTransfer });
5447
5567
  polling.startPolling(t.id);
5448
5568
  } catch (err) {
5449
5569
  captureException(err);
5450
5570
  const msg = err instanceof Error ? err.message : "Transfer failed";
5451
- setError(msg);
5571
+ dispatch({
5572
+ type: "PAY_ERROR",
5573
+ error: msg,
5574
+ fallbackStep: isSetupRedirect ? "wallet-picker" : "deposit"
5575
+ });
5452
5576
  onError?.(msg);
5453
- setStep(isSetupRedirect ? "wallet-picker" : "deposit");
5454
5577
  } finally {
5455
- setCreatingTransfer(false);
5578
+ dispatch({ type: "PAY_ENDED" });
5456
5579
  }
5457
5580
  }, [
5458
5581
  sourceId,
5459
5582
  sourceType,
5460
- activeCredentialId,
5583
+ state.activeCredentialId,
5584
+ state.transfer,
5585
+ state.accounts,
5586
+ state.selectedProviderId,
5461
5587
  destination,
5462
5588
  apiBaseUrl,
5463
5589
  getAccessToken,
5464
- accounts,
5465
5590
  authExecutor,
5466
5591
  transferSigning,
5467
5592
  polling,
5468
5593
  onError,
5469
- useWalletConnector,
5594
+ useWalletConnectorProp,
5470
5595
  idempotencyKey,
5471
- merchantAuthorization,
5472
- transfer
5596
+ merchantAuthorization
5473
5597
  ]);
5474
- const [increasingLimit, setIncreasingLimit] = react.useState(false);
5475
5598
  const handleIncreaseLimit = react.useCallback(async () => {
5476
5599
  const parsedAmount = depositAmount ?? 5;
5477
5600
  if (!sourceId) {
5478
- setError("No account or provider selected.");
5601
+ dispatch({ type: "SET_ERROR", error: "No account or provider selected." });
5479
5602
  return;
5480
5603
  }
5481
- if (!activeCredentialId) {
5482
- setError("Create a passkey on this device before continuing.");
5483
- setStep("create-passkey");
5604
+ if (!state.activeCredentialId) {
5605
+ dispatch({ type: "SET_ERROR", error: "Create a passkey on this device before continuing." });
5606
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5484
5607
  return;
5485
5608
  }
5486
- setError(null);
5487
- setIncreasingLimit(true);
5609
+ dispatch({ type: "SET_ERROR", error: null });
5610
+ dispatch({ type: "SET_INCREASING_LIMIT", value: true });
5488
5611
  try {
5489
5612
  const token = await getAccessToken();
5490
5613
  if (!token) throw new Error("Not authenticated");
5491
5614
  let effectiveSourceType = sourceType;
5492
5615
  let effectiveSourceId = sourceId;
5493
5616
  if (effectiveSourceType === "accountId") {
5494
- const acct = accounts.find((a) => a.id === effectiveSourceId);
5617
+ const acct = state.accounts.find((a) => a.id === effectiveSourceId);
5495
5618
  const activeWallet = acct?.wallets.find((w) => w.status === "ACTIVE");
5496
5619
  if (activeWallet) {
5497
5620
  effectiveSourceType = "walletId";
@@ -5500,188 +5623,118 @@ function SwypePaymentInner({
5500
5623
  }
5501
5624
  const t = await createTransfer(apiBaseUrl, token, {
5502
5625
  id: idempotencyKey,
5503
- credentialId: activeCredentialId,
5626
+ credentialId: state.activeCredentialId,
5504
5627
  merchantAuthorization,
5505
5628
  sourceType: effectiveSourceType,
5506
5629
  sourceId: effectiveSourceId,
5507
5630
  destination,
5508
5631
  amount: parsedAmount
5509
5632
  });
5510
- setTransfer(t);
5511
5633
  if (t.authorizationSessions && t.authorizationSessions.length > 0) {
5512
5634
  const uri = t.authorizationSessions[0].uri;
5513
- setMobileFlow(true);
5514
5635
  pollingTransferIdRef.current = t.id;
5515
5636
  mobileSetupFlowRef.current = true;
5516
5637
  handlingMobileReturnRef.current = false;
5517
5638
  polling.startPolling(t.id);
5518
- setDeeplinkUri(uri);
5639
+ dispatch({ type: "INCREASE_LIMIT_DEEPLINK", transfer: t, deeplinkUri: uri });
5519
5640
  persistMobileFlowState({
5520
5641
  transferId: t.id,
5521
5642
  deeplinkUri: uri,
5522
- providerId: selectedProviderId,
5643
+ providerId: state.selectedProviderId,
5523
5644
  isSetup: true
5524
5645
  });
5525
5646
  triggerDeeplink(uri);
5647
+ } else {
5648
+ dispatch({ type: "TRANSFER_CREATED", transfer: t });
5526
5649
  }
5527
5650
  } catch (err) {
5528
5651
  captureException(err);
5529
5652
  const msg = err instanceof Error ? err.message : "Failed to increase limit";
5530
- setError(msg);
5653
+ dispatch({ type: "SET_ERROR", error: msg });
5531
5654
  onError?.(msg);
5532
5655
  } finally {
5533
- setIncreasingLimit(false);
5656
+ dispatch({ type: "SET_INCREASING_LIMIT", value: false });
5534
5657
  }
5535
5658
  }, [
5536
5659
  depositAmount,
5537
5660
  sourceId,
5538
5661
  sourceType,
5539
- activeCredentialId,
5662
+ state.activeCredentialId,
5663
+ state.accounts,
5664
+ state.selectedProviderId,
5540
5665
  apiBaseUrl,
5541
5666
  getAccessToken,
5542
- accounts,
5543
5667
  polling,
5544
5668
  onError,
5545
5669
  idempotencyKey,
5546
5670
  merchantAuthorization,
5547
- destination,
5548
- selectedProviderId
5671
+ destination
5549
5672
  ]);
5550
- const completePasskeyRegistration = react.useCallback(async (credentialId, publicKey) => {
5551
- const token = await getAccessToken();
5552
- if (!token) throw new Error("Not authenticated");
5553
- await registerPasskey(apiBaseUrl, token, credentialId, publicKey);
5554
- setActiveCredentialId(credentialId);
5555
- window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, credentialId);
5556
- setPasskeyPopupNeeded(false);
5557
- const resolved = resolvePostAuthStep({
5558
- hasPasskey: true,
5559
- accounts,
5560
- persistedMobileFlow: loadMobileFlowState(),
5561
- mobileSetupInProgress: mobileSetupFlowRef.current,
5562
- connectingNewAccount
5563
- });
5564
- if (resolved.clearPersistedFlow) {
5565
- clearMobileFlowState();
5566
- }
5567
- setStep(resolved.step);
5568
- }, [getAccessToken, apiBaseUrl, accounts, connectingNewAccount]);
5569
- const handleRegisterPasskey = react.useCallback(async () => {
5570
- setRegisteringPasskey(true);
5571
- setError(null);
5572
- try {
5573
- const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
5574
- const { credentialId, publicKey } = await createPasskeyCredential({
5575
- userId: user?.id ?? "unknown",
5576
- displayName: passkeyDisplayName
5577
- });
5578
- await completePasskeyRegistration(credentialId, publicKey);
5579
- } catch (err) {
5580
- if (err instanceof PasskeyIframeBlockedError) {
5581
- setPasskeyPopupNeeded(true);
5582
- } else {
5583
- captureException(err);
5584
- setError(err instanceof Error ? err.message : "Failed to register passkey");
5585
- }
5586
- } finally {
5587
- setRegisteringPasskey(false);
5588
- }
5589
- }, [user, completePasskeyRegistration]);
5590
- const handleCreatePasskeyViaPopup = react.useCallback(async () => {
5591
- setRegisteringPasskey(true);
5592
- setError(null);
5673
+ const handleConfirmSign = react.useCallback(async () => {
5674
+ const t = state.transfer ?? polling.transfer;
5675
+ if (!t) return;
5593
5676
  try {
5594
- const token = await getAccessToken();
5595
- const passkeyDisplayName = user?.email?.address ?? user?.google?.name ?? user?.id ?? "Swype User";
5596
- const popupOptions = buildPasskeyPopupOptions({
5597
- userId: user?.id ?? "unknown",
5598
- displayName: passkeyDisplayName,
5599
- authToken: token ?? void 0,
5600
- apiBaseUrl
5601
- });
5602
- const { credentialId, publicKey } = await createPasskeyViaPopup(
5603
- popupOptions,
5604
- knownCredentialIds
5605
- );
5606
- await completePasskeyRegistration(credentialId, publicKey);
5677
+ const signedTransfer = await transferSigning.signTransfer(t.id);
5678
+ clearMobileFlowState();
5679
+ dispatch({ type: "CONFIRM_SIGN_SUCCESS", transfer: signedTransfer });
5680
+ polling.startPolling(t.id);
5607
5681
  } catch (err) {
5608
5682
  captureException(err);
5609
- setError(err instanceof Error ? err.message : "Failed to register passkey");
5610
- } finally {
5611
- setRegisteringPasskey(false);
5683
+ const msg = err instanceof Error ? err.message : "Failed to sign transfer";
5684
+ dispatch({ type: "SET_ERROR", error: msg });
5685
+ onError?.(msg);
5612
5686
  }
5613
- }, [user, completePasskeyRegistration, getAccessToken, apiBaseUrl, knownCredentialIds]);
5614
- const handleVerifyPasskeyViaPopup = react.useCallback(async () => {
5615
- setVerifyingPasskeyPopup(true);
5616
- setError(null);
5617
- try {
5618
- const token = await getAccessToken();
5619
- const matched = await findDevicePasskeyViaPopup({
5620
- credentialIds: knownCredentialIds,
5621
- rpId: resolvePasskeyRpId(),
5622
- authToken: token ?? void 0,
5623
- apiBaseUrl
5624
- });
5625
- if (matched) {
5626
- setActiveCredentialId(matched);
5627
- window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
5628
- if (token) {
5629
- reportPasskeyActivity(apiBaseUrl, token, matched).catch(() => {
5630
- });
5631
- }
5632
- setStep("login");
5633
- } else {
5634
- setStep("create-passkey");
5635
- }
5636
- } catch {
5637
- setStep("create-passkey");
5638
- } finally {
5639
- setVerifyingPasskeyPopup(false);
5687
+ }, [state.transfer, polling.transfer, polling.startPolling, transferSigning, onError]);
5688
+ const handleRetryMobileStatus = react.useCallback(() => {
5689
+ dispatch({ type: "SET_ERROR", error: null });
5690
+ handlingMobileReturnRef.current = false;
5691
+ const currentTransfer = polling.transfer ?? state.transfer;
5692
+ if (currentTransfer?.status === "AUTHORIZED") {
5693
+ void handleAuthorizedMobileReturn(currentTransfer, mobileSetupFlowRef.current);
5694
+ return;
5695
+ }
5696
+ const transferIdToResume = pollingTransferIdRef.current ?? currentTransfer?.id;
5697
+ if (transferIdToResume) {
5698
+ polling.startPolling(transferIdToResume);
5640
5699
  }
5641
- }, [knownCredentialIds, getAccessToken, apiBaseUrl]);
5700
+ }, [handleAuthorizedMobileReturn, polling, state.transfer]);
5642
5701
  const handleSelectProvider = react.useCallback((providerId) => {
5643
- setSelectedProviderId(providerId);
5644
- setSelectedAccountId(null);
5645
- setConnectingNewAccount(true);
5702
+ dispatch({ type: "SELECT_PROVIDER", providerId });
5646
5703
  const isMobile = !shouldUseWalletConnector({
5647
- useWalletConnector,
5704
+ useWalletConnector: useWalletConnectorProp,
5648
5705
  userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent
5649
5706
  });
5650
5707
  if (isMobile) {
5651
5708
  handlingMobileReturnRef.current = false;
5652
5709
  mobileSetupFlowRef.current = true;
5653
- const amount2 = depositAmount ?? 5;
5654
- handlePay(amount2, { sourceType: "providerId", sourceId: providerId });
5710
+ const amount = depositAmount ?? 5;
5711
+ handlePay(amount, { sourceType: "providerId", sourceId: providerId });
5655
5712
  } else {
5656
- setStep("deposit");
5713
+ dispatch({ type: "NAVIGATE", step: "deposit" });
5657
5714
  }
5658
- }, [useWalletConnector, depositAmount, handlePay]);
5715
+ }, [useWalletConnectorProp, depositAmount, handlePay]);
5659
5716
  const handleContinueConnection = react.useCallback(
5660
5717
  (accountId) => {
5661
- const acct = accounts.find((a) => a.id === accountId);
5662
- setSelectedAccountId(accountId);
5663
- setSelectedWalletId(acct?.wallets[0]?.id ?? null);
5664
- setConnectingNewAccount(false);
5665
- setStep("deposit");
5718
+ const acct = state.accounts.find((a) => a.id === accountId);
5719
+ dispatch({
5720
+ type: "SELECT_ACCOUNT",
5721
+ accountId,
5722
+ walletId: acct?.wallets[0]?.id ?? null
5723
+ });
5666
5724
  },
5667
- [accounts]
5725
+ [state.accounts]
5668
5726
  );
5669
5727
  const handleNewPayment = react.useCallback(() => {
5670
5728
  clearMobileFlowState();
5671
- setStep("deposit");
5672
- setTransfer(null);
5673
- setError(null);
5674
- setAmount(depositAmount != null ? depositAmount.toString() : "");
5675
- setMobileFlow(false);
5676
- setDeeplinkUri(null);
5677
5729
  processingStartedAtRef.current = null;
5678
5730
  pollingTransferIdRef.current = null;
5679
- mobileSigningTransferIdRef.current = null;
5680
5731
  preSelectSourceStepRef.current = null;
5681
- setConnectingNewAccount(false);
5682
- setSelectedWalletId(null);
5683
- if (accounts.length > 0) setSelectedAccountId(accounts[0].id);
5684
- }, [depositAmount, accounts]);
5732
+ dispatch({
5733
+ type: "NEW_PAYMENT",
5734
+ depositAmount,
5735
+ firstAccountId: state.accounts.length > 0 ? state.accounts[0].id : null
5736
+ });
5737
+ }, [depositAmount, state.accounts]);
5685
5738
  const handleLogout = react.useCallback(async () => {
5686
5739
  try {
5687
5740
  await logout();
@@ -5692,254 +5745,464 @@ function SwypePaymentInner({
5692
5745
  window.localStorage.removeItem(ACTIVE_CREDENTIAL_STORAGE_KEY);
5693
5746
  }
5694
5747
  polling.stopPolling();
5695
- setActiveCredentialId(null);
5696
- setStep("login");
5697
- setError(null);
5698
- setTransfer(null);
5699
- setCreatingTransfer(false);
5700
- setRegisteringPasskey(false);
5701
- setProviders([]);
5702
- setAccounts([]);
5703
- setChains([]);
5704
- setSelectedAccountId(null);
5705
- setSelectedWalletId(null);
5706
- setSelectedProviderId(null);
5707
- setConnectingNewAccount(false);
5708
- setAmount(depositAmount != null ? depositAmount.toString() : "");
5709
- setMobileFlow(false);
5710
- setDeeplinkUri(null);
5711
5748
  preSelectSourceStepRef.current = null;
5712
- resetHeadlessLogin();
5713
- }, [logout, polling, depositAmount, resetHeadlessLogin]);
5714
- const handleConfirmSign = react.useCallback(async () => {
5715
- const t = transfer ?? polling.transfer;
5716
- if (!t) return;
5717
- try {
5718
- const signedTransfer = await transferSigning.signTransfer(t.id);
5719
- setTransfer(signedTransfer);
5720
- clearMobileFlowState();
5721
- setStep("processing");
5722
- polling.startPolling(t.id);
5723
- } catch (err) {
5724
- captureException(err);
5725
- const msg = err instanceof Error ? err.message : "Failed to sign transfer";
5726
- setError(msg);
5727
- onError?.(msg);
5749
+ setAuthInput("");
5750
+ setOtpCode("");
5751
+ dispatch({ type: "LOGOUT", depositAmount });
5752
+ }, [logout, polling, depositAmount]);
5753
+ const pendingSelectSourceAction = authExecutor.pendingSelectSource;
5754
+ const selectSourceChoices = react.useMemo(() => {
5755
+ if (!pendingSelectSourceAction) return [];
5756
+ const options = pendingSelectSourceAction.metadata?.options ?? [];
5757
+ return buildSelectSourceChoices(options);
5758
+ }, [pendingSelectSourceAction]);
5759
+ const selectSourceRecommended = react.useMemo(() => {
5760
+ if (!pendingSelectSourceAction) return null;
5761
+ return pendingSelectSourceAction.metadata?.recommended ?? null;
5762
+ }, [pendingSelectSourceAction]);
5763
+ const handleSelectSourceChainChange = react.useCallback(
5764
+ (chainName) => {
5765
+ setSelectSourceChainName(chainName);
5766
+ const chain = selectSourceChoices.find((c) => c.chainName === chainName);
5767
+ if (!chain || chain.tokens.length === 0) return;
5768
+ const recommendedToken = selectSourceRecommended?.chainName === chainName ? selectSourceRecommended.tokenSymbol : null;
5769
+ const hasRecommended = !!recommendedToken && chain.tokens.some((t) => t.tokenSymbol === recommendedToken);
5770
+ setSelectSourceTokenSymbol(
5771
+ hasRecommended ? recommendedToken : chain.tokens[0].tokenSymbol
5772
+ );
5773
+ },
5774
+ [selectSourceChoices, selectSourceRecommended]
5775
+ );
5776
+ const handleConfirmSelectSource = react.useCallback(() => {
5777
+ authExecutor.resolveSelectSource({
5778
+ chainName: selectSourceChainName,
5779
+ tokenSymbol: selectSourceTokenSymbol
5780
+ });
5781
+ }, [authExecutor, selectSourceChainName, selectSourceTokenSymbol]);
5782
+ react.useEffect(() => {
5783
+ if (depositAmount != null) {
5784
+ dispatch({ type: "SYNC_AMOUNT", amount: depositAmount.toString() });
5728
5785
  }
5729
- }, [transfer, polling.transfer, polling.startPolling, transferSigning, onError]);
5730
- if (!ready) {
5731
- return /* @__PURE__ */ jsxRuntime.jsx(ScreenLayout, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "48px 0", flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { label: "Initializing..." }) }) });
5732
- }
5733
- if (step === "login" && !authenticated) {
5734
- return /* @__PURE__ */ jsxRuntime.jsx(
5735
- LoginScreen,
5736
- {
5737
- authInput,
5738
- onAuthInputChange: setAuthInput,
5739
- onSubmit: handleSendLoginCode,
5740
- sending: activeOtpStatus === "sending-code",
5741
- error,
5742
- onBack,
5743
- merchantInitials: merchantName ? merchantName.slice(0, 2).toUpperCase() : void 0
5744
- }
5745
- );
5746
- }
5747
- if (step === "otp-verify" && !authenticated) {
5748
- return /* @__PURE__ */ jsxRuntime.jsx(
5749
- OtpVerifyScreen,
5750
- {
5751
- maskedIdentifier: verificationTarget ? maskAuthIdentifier(verificationTarget) : "",
5752
- otpCode,
5753
- onOtpChange: (code) => {
5754
- setOtpCode(code);
5755
- setError(null);
5756
- },
5757
- onVerify: handleVerifyLoginCode,
5758
- onResend: handleResendLoginCode,
5759
- onBack: () => {
5760
- setVerificationTarget(null);
5761
- setOtpCode("");
5762
- setError(null);
5763
- setStep("login");
5764
- },
5765
- verifying: activeOtpStatus === "submitting-code",
5766
- error
5767
- }
5768
- );
5769
- }
5770
- if ((step === "login" || step === "otp-verify") && authenticated) {
5771
- return /* @__PURE__ */ jsxRuntime.jsx(ScreenLayout, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "48px 0", flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { label: "Verifying your passkey..." }) }) });
5772
- }
5773
- if (step === "verify-passkey") {
5774
- return /* @__PURE__ */ jsxRuntime.jsx(
5775
- VerifyPasskeyScreen,
5776
- {
5777
- onVerify: handleVerifyPasskeyViaPopup,
5778
- onBack: handleLogout,
5779
- verifying: verifyingPasskeyPopup,
5780
- error
5781
- }
5782
- );
5783
- }
5784
- if (step === "create-passkey") {
5785
- return /* @__PURE__ */ jsxRuntime.jsx(
5786
- CreatePasskeyScreen,
5787
- {
5788
- onCreatePasskey: handleRegisterPasskey,
5789
- onBack: handleLogout,
5790
- creating: registeringPasskey,
5791
- error,
5792
- popupFallback: passkeyPopupNeeded,
5793
- onCreatePasskeyViaPopup: handleCreatePasskeyViaPopup
5786
+ }, [depositAmount]);
5787
+ react.useEffect(() => {
5788
+ if (authenticated) return;
5789
+ if (activeOtpErrorMessage) dispatch({ type: "SET_ERROR", error: activeOtpErrorMessage });
5790
+ }, [activeOtpErrorMessage, authenticated]);
5791
+ react.useEffect(() => {
5792
+ if (state.step === "otp-verify" && /^\d{6}$/.test(otpCode.trim()) && activeOtpStatus !== "submitting-code") {
5793
+ handleVerifyLoginCode();
5794
+ }
5795
+ }, [otpCode, state.step, activeOtpStatus, handleVerifyLoginCode]);
5796
+ react.useEffect(() => {
5797
+ if (!ready || !authenticated) return;
5798
+ if (state.step !== "login" && state.step !== "otp-verify") return;
5799
+ let cancelled = false;
5800
+ dispatch({ type: "SET_ERROR", error: null });
5801
+ setAuthInput("");
5802
+ setOtpCode("");
5803
+ const restoreOrDeposit = async (credId, token) => {
5804
+ const persisted = loadMobileFlowState();
5805
+ let accts = [];
5806
+ try {
5807
+ accts = await fetchAccounts(apiBaseUrl, token, credId);
5808
+ if (cancelled) return;
5809
+ } catch {
5794
5810
  }
5795
- );
5796
- }
5797
- if (step === "wallet-picker") {
5798
- return /* @__PURE__ */ jsxRuntime.jsx(
5799
- WalletPickerScreen,
5800
- {
5801
- providers,
5802
- pendingConnections,
5803
- loading: creatingTransfer,
5804
- onSelectProvider: handleSelectProvider,
5805
- onContinueConnection: handleContinueConnection,
5806
- onBack: () => setStep(activeCredentialId ? "deposit" : "create-passkey")
5811
+ const resolved = resolvePostAuthStep({
5812
+ hasPasskey: true,
5813
+ accounts: accts,
5814
+ persistedMobileFlow: persisted,
5815
+ mobileSetupInProgress: false,
5816
+ connectingNewAccount: false
5817
+ });
5818
+ if (resolved.clearPersistedFlow) clearMobileFlowState();
5819
+ if (resolved.step === "deposit" && persisted && persisted.isSetup) {
5820
+ try {
5821
+ const existingTransfer = await fetchTransfer(apiBaseUrl, token, persisted.transferId);
5822
+ if (cancelled) return;
5823
+ if (existingTransfer.status === "AUTHORIZED") {
5824
+ await handleAuthorizedMobileReturn(existingTransfer, true);
5825
+ return;
5826
+ }
5827
+ } catch {
5828
+ }
5807
5829
  }
5808
- );
5809
- }
5810
- if (step === "open-wallet") {
5811
- const providerName = providers.find((p) => p.id === selectedProviderId)?.name ?? null;
5812
- return /* @__PURE__ */ jsxRuntime.jsx(
5813
- OpenWalletScreen,
5814
- {
5815
- walletName: providerName,
5816
- deeplinkUri: deeplinkUri ?? "",
5817
- loading: creatingTransfer || !deeplinkUri,
5818
- error: error || polling.error,
5819
- onRetryStatus: handleRetryMobileStatus,
5820
- onLogout: handleLogout
5830
+ if (resolved.step === "open-wallet" && persisted) {
5831
+ try {
5832
+ const existingTransfer = await fetchTransfer(apiBaseUrl, token, persisted.transferId);
5833
+ if (cancelled) return;
5834
+ const mobileResolution = resolveRestoredMobileFlow(
5835
+ existingTransfer.status,
5836
+ persisted.isSetup
5837
+ );
5838
+ if (mobileResolution.kind === "resume-setup-deposit") {
5839
+ await handleAuthorizedMobileReturn(existingTransfer, true);
5840
+ return;
5841
+ }
5842
+ if (mobileResolution.kind === "resume-confirm-sign") {
5843
+ await handleAuthorizedMobileReturn(existingTransfer, false);
5844
+ return;
5845
+ }
5846
+ if (mobileResolution.kind === "resume-success") {
5847
+ clearMobileFlowState();
5848
+ dispatch({ type: "MOBILE_RESUME_SUCCESS", transfer: existingTransfer });
5849
+ onComplete?.(existingTransfer);
5850
+ return;
5851
+ }
5852
+ if (mobileResolution.kind === "resume-failed") {
5853
+ clearMobileFlowState();
5854
+ dispatch({ type: "MOBILE_RESUME_FAILED", transfer: existingTransfer });
5855
+ return;
5856
+ }
5857
+ if (mobileResolution.kind === "resume-processing") {
5858
+ clearMobileFlowState();
5859
+ dispatch({ type: "MOBILE_RESUME_PROCESSING", transfer: existingTransfer });
5860
+ polling.startPolling(existingTransfer.id);
5861
+ return;
5862
+ }
5863
+ if (mobileResolution.kind === "resume-stale-setup") {
5864
+ clearMobileFlowState();
5865
+ if (!cancelled) dispatch({ type: "NAVIGATE", step: "wallet-picker" });
5866
+ return;
5867
+ }
5868
+ } catch (err) {
5869
+ if (cancelled) return;
5870
+ dispatch({
5871
+ type: "ENTER_MOBILE_FLOW",
5872
+ deeplinkUri: persisted.deeplinkUri,
5873
+ providerId: persisted.providerId,
5874
+ error: err instanceof Error ? err.message : "Unable to refresh wallet authorization status."
5875
+ });
5876
+ pollingTransferIdRef.current = persisted.transferId;
5877
+ mobileSetupFlowRef.current = persisted.isSetup;
5878
+ polling.startPolling(persisted.transferId);
5879
+ return;
5880
+ }
5881
+ dispatch({
5882
+ type: "ENTER_MOBILE_FLOW",
5883
+ deeplinkUri: persisted.deeplinkUri,
5884
+ providerId: persisted.providerId
5885
+ });
5886
+ pollingTransferIdRef.current = persisted.transferId;
5887
+ mobileSetupFlowRef.current = persisted.isSetup;
5888
+ polling.startPolling(persisted.transferId);
5889
+ return;
5821
5890
  }
5822
- );
5823
- }
5824
- if (step === "confirm-sign") {
5825
- const providerName = providers.find((p) => p.id === selectedProviderId)?.name ?? null;
5826
- return /* @__PURE__ */ jsxRuntime.jsx(
5827
- ConfirmSignScreen,
5828
- {
5829
- walletName: providerName,
5830
- signing: transferSigning.signing,
5831
- error: error || transferSigning.error,
5832
- onSign: handleConfirmSign,
5833
- onLogout: handleLogout
5891
+ dispatch({ type: "NAVIGATE", step: resolved.step });
5892
+ };
5893
+ const checkPasskey = async () => {
5894
+ try {
5895
+ const token = await getAccessToken();
5896
+ if (!token || cancelled) return;
5897
+ const { config } = await fetchUserConfig(apiBaseUrl, token);
5898
+ if (cancelled) return;
5899
+ const allPasskeys = config.passkeys ?? (config.passkey ? [config.passkey] : []);
5900
+ dispatch({
5901
+ type: "PASSKEY_CONFIG_LOADED",
5902
+ knownIds: allPasskeys.map((p) => p.credentialId),
5903
+ oneTapLimit: config.defaultAllowance ?? void 0
5904
+ });
5905
+ if (allPasskeys.length === 0) {
5906
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5907
+ return;
5908
+ }
5909
+ if (state.activeCredentialId && allPasskeys.some((p) => p.credentialId === state.activeCredentialId)) {
5910
+ await restoreOrDeposit(state.activeCredentialId, token);
5911
+ return;
5912
+ }
5913
+ if (cancelled) return;
5914
+ if (isSafari() && isInCrossOriginIframe()) {
5915
+ dispatch({ type: "NAVIGATE", step: "verify-passkey" });
5916
+ return;
5917
+ }
5918
+ const credentialIds = allPasskeys.map((p) => p.credentialId);
5919
+ const matched = await findDevicePasskey(credentialIds);
5920
+ if (cancelled) return;
5921
+ if (matched) {
5922
+ dispatch({ type: "PASSKEY_ACTIVATED", credentialId: matched });
5923
+ window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
5924
+ reportPasskeyActivity(apiBaseUrl, token, matched).catch(() => {
5925
+ });
5926
+ await restoreOrDeposit(matched, token);
5927
+ return;
5928
+ }
5929
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5930
+ } catch {
5931
+ if (!cancelled) dispatch({ type: "NAVIGATE", step: "create-passkey" });
5834
5932
  }
5835
- );
5836
- }
5837
- if (step === "deposit") {
5838
- if (loadingData) {
5839
- return /* @__PURE__ */ jsxRuntime.jsx(ScreenLayout, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { textAlign: "center", padding: "48px 0", flex: 1, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsxRuntime.jsx(Spinner, { label: "Loading..." }) }) });
5933
+ };
5934
+ checkPasskey();
5935
+ return () => {
5936
+ cancelled = true;
5937
+ };
5938
+ }, [
5939
+ ready,
5940
+ authenticated,
5941
+ state.step,
5942
+ apiBaseUrl,
5943
+ getAccessToken,
5944
+ state.activeCredentialId,
5945
+ handleAuthorizedMobileReturn,
5946
+ onComplete,
5947
+ polling
5948
+ ]);
5949
+ react.useEffect(() => {
5950
+ const loadAction = resolveDataLoadAction({
5951
+ authenticated,
5952
+ step: state.step,
5953
+ accountsCount: state.accounts.length,
5954
+ hasActiveCredential: !!state.activeCredentialId,
5955
+ loading: loadingDataRef.current
5956
+ });
5957
+ if (loadAction === "reset") {
5958
+ loadingDataRef.current = false;
5959
+ dispatch({ type: "DATA_LOAD_END" });
5960
+ return;
5840
5961
  }
5841
- const parsedAmt = depositAmount != null ? depositAmount : 5;
5842
- return /* @__PURE__ */ jsxRuntime.jsx(
5843
- DepositScreen,
5844
- {
5845
- merchantName,
5846
- sourceName,
5847
- sourceAddress,
5848
- sourceVerified,
5849
- availableBalance: maxSourceBalance,
5850
- remainingLimit: selectedAccount?.remainingAllowance ?? oneTapLimit,
5851
- tokenCount,
5852
- initialAmount: parsedAmt,
5853
- processing: creatingTransfer,
5854
- error,
5855
- onDeposit: handlePay,
5856
- onChangeSource: () => setStep("wallet-picker"),
5857
- onSwitchWallet: () => setStep("wallet-picker"),
5858
- onBack: onBack ?? (() => handleLogout()),
5859
- onLogout: handleLogout,
5860
- onIncreaseLimit: handleIncreaseLimit,
5861
- increasingLimit
5862
- }
5863
- );
5864
- }
5865
- if (step === "processing") {
5866
- const polledStatus = polling.transfer?.status;
5867
- const transferPhase = creatingTransfer ? "creating" : polledStatus === "SENDING" || polledStatus === "SENT" ? "sent" : "verifying";
5868
- return /* @__PURE__ */ jsxRuntime.jsx(
5869
- TransferStatusScreen,
5870
- {
5871
- phase: transferPhase,
5872
- error: error || authExecutor.error || transferSigning.error || polling.error,
5873
- onLogout: handleLogout
5874
- }
5875
- );
5876
- }
5877
- if (step === "select-source") {
5878
- return /* @__PURE__ */ jsxRuntime.jsx(
5879
- SelectSourceScreen,
5880
- {
5881
- choices: selectSourceChoices,
5882
- selectedChainName: selectSourceChainName,
5883
- selectedTokenSymbol: selectSourceTokenSymbol,
5884
- recommended: selectSourceRecommended,
5885
- onChainChange: handleSelectSourceChainChange,
5886
- onTokenChange: setSelectSourceTokenSymbol,
5887
- onConfirm: () => {
5888
- authExecutor.resolveSelectSource({
5889
- chainName: selectSourceChainName,
5890
- tokenSymbol: selectSourceTokenSymbol
5962
+ if (loadAction === "wait") return;
5963
+ const credentialId = state.activeCredentialId;
5964
+ if (!credentialId) {
5965
+ loadingDataRef.current = false;
5966
+ dispatch({ type: "DATA_LOAD_END" });
5967
+ return;
5968
+ }
5969
+ let cancelled = false;
5970
+ loadingDataRef.current = true;
5971
+ const load = async () => {
5972
+ dispatch({ type: "DATA_LOAD_START" });
5973
+ try {
5974
+ const token = await getAccessToken();
5975
+ if (!token) throw new Error("Not authenticated");
5976
+ const [prov, accts, chn] = await Promise.all([
5977
+ fetchProviders(apiBaseUrl, token),
5978
+ fetchAccounts(apiBaseUrl, token, credentialId),
5979
+ fetchChains(apiBaseUrl, token)
5980
+ ]);
5981
+ if (cancelled) return;
5982
+ const parsedAmt = depositAmount != null ? depositAmount : 0;
5983
+ const defaults = computeSmartDefaults(accts, parsedAmt);
5984
+ const persisted = loadMobileFlowState();
5985
+ const resolved = resolvePostAuthStep({
5986
+ hasPasskey: !!state.activeCredentialId,
5987
+ accounts: accts,
5988
+ persistedMobileFlow: persisted,
5989
+ mobileSetupInProgress: mobileSetupFlowRef.current,
5990
+ connectingNewAccount: state.connectingNewAccount
5991
+ });
5992
+ const correctableSteps = ["deposit", "wallet-picker", "open-wallet"];
5993
+ dispatch({
5994
+ type: "DATA_LOADED",
5995
+ providers: prov,
5996
+ accounts: accts,
5997
+ chains: chn,
5998
+ defaults,
5999
+ fallbackProviderId: !defaults && prov.length > 0 ? prov[0].id : null,
6000
+ resolvedStep: correctableSteps.includes(state.step) ? resolved.step : void 0,
6001
+ clearMobileState: resolved.clearPersistedFlow
6002
+ });
6003
+ if (resolved.clearPersistedFlow) clearMobileFlowState();
6004
+ } catch (err) {
6005
+ if (!cancelled) {
6006
+ captureException(err);
6007
+ dispatch({
6008
+ type: "SET_ERROR",
6009
+ error: err instanceof Error ? err.message : "Failed to load data"
5891
6010
  });
5892
- },
5893
- onLogout: handleLogout
5894
- }
5895
- );
5896
- }
5897
- if (step === "success") {
5898
- const succeeded = transfer?.status === "COMPLETED";
5899
- const displayAmount = transfer?.amount?.amount ?? 0;
5900
- const displayCurrency = transfer?.amount?.currency ?? "USD";
5901
- return /* @__PURE__ */ jsxRuntime.jsx(
5902
- SuccessScreen,
5903
- {
5904
- amount: displayAmount,
5905
- currency: displayCurrency,
5906
- succeeded,
5907
- error,
5908
- merchantName,
5909
- sourceName,
5910
- remainingLimit: succeeded ? (() => {
5911
- const limit = selectedAccount?.remainingAllowance ?? oneTapLimit;
5912
- return limit > displayAmount ? limit - displayAmount : 0;
5913
- })() : void 0,
5914
- onDone: onDismiss ?? handleNewPayment,
5915
- onLogout: handleLogout,
5916
- autoCloseSeconds
6011
+ }
6012
+ } finally {
6013
+ if (!cancelled) {
6014
+ loadingDataRef.current = false;
6015
+ dispatch({ type: "DATA_LOAD_END" });
6016
+ }
5917
6017
  }
5918
- );
5919
- }
5920
- if (step === "low-balance") {
5921
- return /* @__PURE__ */ jsxRuntime.jsx(
5922
- DepositScreen,
5923
- {
5924
- merchantName,
5925
- sourceName,
5926
- sourceAddress,
5927
- sourceVerified,
5928
- availableBalance: 0,
5929
- remainingLimit: selectedAccount?.remainingAllowance ?? oneTapLimit,
5930
- tokenCount,
5931
- initialAmount: depositAmount ?? 5,
5932
- processing: false,
5933
- error,
5934
- onDeposit: handlePay,
5935
- onChangeSource: () => setStep("wallet-picker"),
5936
- onSwitchWallet: () => setStep("wallet-picker"),
5937
- onBack: onBack ?? (() => handleLogout()),
5938
- onLogout: handleLogout
6018
+ };
6019
+ load();
6020
+ return () => {
6021
+ cancelled = true;
6022
+ loadingDataRef.current = false;
6023
+ };
6024
+ }, [
6025
+ authenticated,
6026
+ state.step,
6027
+ state.accounts.length,
6028
+ apiBaseUrl,
6029
+ getAccessToken,
6030
+ state.activeCredentialId,
6031
+ depositAmount,
6032
+ state.connectingNewAccount
6033
+ ]);
6034
+ react.useEffect(() => {
6035
+ if (!polling.transfer) return;
6036
+ if (polling.transfer.status === "COMPLETED") {
6037
+ clearMobileFlowState();
6038
+ dispatch({ type: "TRANSFER_COMPLETED", transfer: polling.transfer });
6039
+ onComplete?.(polling.transfer);
6040
+ } else if (polling.transfer.status === "FAILED") {
6041
+ clearMobileFlowState();
6042
+ dispatch({ type: "TRANSFER_FAILED", transfer: polling.transfer, error: "Transfer failed." });
6043
+ }
6044
+ }, [polling.transfer, onComplete]);
6045
+ react.useEffect(() => {
6046
+ if (state.step !== "processing") {
6047
+ processingStartedAtRef.current = null;
6048
+ return;
6049
+ }
6050
+ if (!processingStartedAtRef.current) {
6051
+ processingStartedAtRef.current = Date.now();
6052
+ }
6053
+ const elapsedMs = Date.now() - processingStartedAtRef.current;
6054
+ const remainingMs = PROCESSING_TIMEOUT_MS - elapsedMs;
6055
+ const handleTimeout = () => {
6056
+ if (!hasProcessingTimedOut(processingStartedAtRef.current, Date.now())) return;
6057
+ const status = getTransferStatus(polling.transfer, state.transfer);
6058
+ const msg = buildProcessingTimeoutMessage(status);
6059
+ captureException(new Error(msg));
6060
+ polling.stopPolling();
6061
+ dispatch({ type: "PROCESSING_TIMEOUT", error: msg });
6062
+ onError?.(msg);
6063
+ };
6064
+ if (remainingMs <= 0) {
6065
+ handleTimeout();
6066
+ return;
6067
+ }
6068
+ const timeoutId = window.setTimeout(handleTimeout, remainingMs);
6069
+ return () => window.clearTimeout(timeoutId);
6070
+ }, [state.step, polling.transfer, state.transfer, polling.stopPolling, onError]);
6071
+ react.useEffect(() => {
6072
+ if (!state.mobileFlow) {
6073
+ handlingMobileReturnRef.current = false;
6074
+ return;
6075
+ }
6076
+ if (handlingMobileReturnRef.current) return;
6077
+ const polledTransfer = polling.transfer;
6078
+ if (!polledTransfer || polledTransfer.status !== "AUTHORIZED") return;
6079
+ void handleAuthorizedMobileReturn(polledTransfer, mobileSetupFlowRef.current);
6080
+ }, [state.mobileFlow, polling.transfer, handleAuthorizedMobileReturn]);
6081
+ react.useEffect(() => {
6082
+ if (!state.mobileFlow) return;
6083
+ if (handlingMobileReturnRef.current) return;
6084
+ const transferIdToResume = pollingTransferIdRef.current ?? state.transfer?.id;
6085
+ if (!transferIdToResume) return;
6086
+ if (!polling.isPolling) polling.startPolling(transferIdToResume);
6087
+ const handleVisibility = () => {
6088
+ if (document.visibilityState === "visible" && !handlingMobileReturnRef.current) {
6089
+ polling.startPolling(transferIdToResume);
5939
6090
  }
6091
+ };
6092
+ document.addEventListener("visibilitychange", handleVisibility);
6093
+ return () => document.removeEventListener("visibilitychange", handleVisibility);
6094
+ }, [state.mobileFlow, state.transfer?.id, polling.isPolling, polling.startPolling]);
6095
+ react.useEffect(() => {
6096
+ if (!pendingSelectSourceAction) {
6097
+ initializedSelectSourceActionRef.current = null;
6098
+ setSelectSourceChainName("");
6099
+ setSelectSourceTokenSymbol("");
6100
+ return;
6101
+ }
6102
+ if (initializedSelectSourceActionRef.current === pendingSelectSourceAction.id) return;
6103
+ const hasRecommended = !!selectSourceRecommended && selectSourceChoices.some(
6104
+ (chain) => chain.chainName === selectSourceRecommended.chainName && chain.tokens.some((t) => t.tokenSymbol === selectSourceRecommended.tokenSymbol)
5940
6105
  );
5941
- }
5942
- return null;
6106
+ if (hasRecommended && selectSourceRecommended) {
6107
+ setSelectSourceChainName(selectSourceRecommended.chainName);
6108
+ setSelectSourceTokenSymbol(selectSourceRecommended.tokenSymbol);
6109
+ } else if (selectSourceChoices.length > 0 && selectSourceChoices[0].tokens.length > 0) {
6110
+ setSelectSourceChainName(selectSourceChoices[0].chainName);
6111
+ setSelectSourceTokenSymbol(selectSourceChoices[0].tokens[0].tokenSymbol);
6112
+ } else {
6113
+ setSelectSourceChainName("Base");
6114
+ setSelectSourceTokenSymbol("USDC");
6115
+ }
6116
+ initializedSelectSourceActionRef.current = pendingSelectSourceAction.id;
6117
+ }, [pendingSelectSourceAction, selectSourceChoices, selectSourceRecommended]);
6118
+ react.useEffect(() => {
6119
+ if (pendingSelectSourceAction && state.step === "processing") {
6120
+ preSelectSourceStepRef.current = state.step;
6121
+ dispatch({ type: "NAVIGATE", step: "select-source" });
6122
+ } else if (!pendingSelectSourceAction && state.step === "select-source") {
6123
+ dispatch({ type: "NAVIGATE", step: preSelectSourceStepRef.current ?? "processing" });
6124
+ preSelectSourceStepRef.current = null;
6125
+ }
6126
+ }, [pendingSelectSourceAction, state.step]);
6127
+ const handlers = react.useMemo(() => ({
6128
+ onSendLoginCode: handleSendLoginCode,
6129
+ onVerifyLoginCode: handleVerifyLoginCode,
6130
+ onResendLoginCode: handleResendLoginCode,
6131
+ onBackFromOtp: () => {
6132
+ setOtpCode("");
6133
+ dispatch({ type: "BACK_TO_LOGIN" });
6134
+ },
6135
+ onRegisterPasskey: handleRegisterPasskey,
6136
+ onCreatePasskeyViaPopup: handleCreatePasskeyViaPopup,
6137
+ onVerifyPasskeyViaPopup: handleVerifyPasskeyViaPopup,
6138
+ onSelectProvider: handleSelectProvider,
6139
+ onContinueConnection: handleContinueConnection,
6140
+ onPay: handlePay,
6141
+ onIncreaseLimit: handleIncreaseLimit,
6142
+ onConfirmSign: handleConfirmSign,
6143
+ onRetryMobileStatus: handleRetryMobileStatus,
6144
+ onLogout: handleLogout,
6145
+ onNewPayment: handleNewPayment,
6146
+ onNavigate: (step) => dispatch({ type: "NAVIGATE", step }),
6147
+ onSetAuthInput: setAuthInput,
6148
+ onSetOtpCode: (code) => {
6149
+ setOtpCode(code);
6150
+ dispatch({ type: "SET_ERROR", error: null });
6151
+ },
6152
+ onSelectSourceChainChange: handleSelectSourceChainChange,
6153
+ onSetSelectSourceTokenSymbol: setSelectSourceTokenSymbol,
6154
+ onConfirmSelectSource: handleConfirmSelectSource
6155
+ }), [
6156
+ handleSendLoginCode,
6157
+ handleVerifyLoginCode,
6158
+ handleResendLoginCode,
6159
+ handleRegisterPasskey,
6160
+ handleCreatePasskeyViaPopup,
6161
+ handleVerifyPasskeyViaPopup,
6162
+ handleSelectProvider,
6163
+ handleContinueConnection,
6164
+ handlePay,
6165
+ handleIncreaseLimit,
6166
+ handleConfirmSign,
6167
+ handleRetryMobileStatus,
6168
+ handleLogout,
6169
+ handleNewPayment,
6170
+ handleSelectSourceChainChange,
6171
+ handleConfirmSelectSource
6172
+ ]);
6173
+ return /* @__PURE__ */ jsxRuntime.jsx(
6174
+ StepRenderer,
6175
+ {
6176
+ state,
6177
+ ready,
6178
+ authenticated,
6179
+ activeOtpStatus,
6180
+ pollingTransfer: polling.transfer,
6181
+ pollingError: polling.error,
6182
+ authExecutorError: authExecutor.error,
6183
+ transferSigningSigning: transferSigning.signing,
6184
+ transferSigningError: transferSigning.error,
6185
+ pendingConnections,
6186
+ sourceName,
6187
+ sourceAddress,
6188
+ sourceVerified,
6189
+ maxSourceBalance,
6190
+ tokenCount,
6191
+ selectedAccount,
6192
+ selectSourceChoices,
6193
+ selectSourceRecommended,
6194
+ authInput,
6195
+ otpCode,
6196
+ selectSourceChainName,
6197
+ selectSourceTokenSymbol,
6198
+ merchantName,
6199
+ onBack,
6200
+ onDismiss,
6201
+ autoCloseSeconds,
6202
+ depositAmount,
6203
+ handlers
6204
+ }
6205
+ );
5943
6206
  }
5944
6207
 
5945
6208
  exports.AdvancedSourceScreen = AdvancedSourceScreen;