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