@swype-org/react-sdk 0.1.88 → 0.1.89

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