@swype-org/react-sdk 0.1.88 → 0.1.90

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