@swype-org/react-sdk 0.1.88 → 0.1.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,229 @@ 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
+ const resolved = resolvePostAuthStep({
5432
+ hasPasskey: true,
5433
+ accounts: state.accounts,
5434
+ persistedMobileFlow: loadMobileFlowState(),
5435
+ mobileSetupInProgress: mobileSetupFlowRef.current,
5436
+ connectingNewAccount: state.connectingNewAccount
5437
+ });
5438
+ if (resolved.clearPersistedFlow) clearMobileFlowState();
5439
+ dispatch({ type: "NAVIGATE", step: resolved.step });
5440
+ } else {
5441
+ dispatch({
5442
+ type: "SET_ERROR",
5443
+ error: "Passkey verification was not completed. Please try again."
5444
+ });
5031
5445
  }
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;
5446
+ } catch (err) {
5447
+ captureException(err);
5448
+ dispatch({
5449
+ type: "SET_ERROR",
5450
+ error: err instanceof Error ? err.message : "Passkey verification failed. Please try again."
5451
+ });
5452
+ } finally {
5453
+ dispatch({ type: "SET_VERIFYING_PASSKEY", value: false });
5068
5454
  }
5069
- let cancelled = false;
5070
- loadingDataRef.current = true;
5071
- const load = async () => {
5072
- setLoadingData(true);
5073
- setError(null);
5455
+ }, [state.knownCredentialIds, getAccessToken, apiBaseUrl, state.accounts, state.connectingNewAccount]);
5456
+ const reloadAccounts = useCallback(async () => {
5457
+ const token = await getAccessToken();
5458
+ if (!token || !state.activeCredentialId) return;
5459
+ const [accts, prov] = await Promise.all([
5460
+ fetchAccounts(apiBaseUrl, token, state.activeCredentialId),
5461
+ fetchProviders(apiBaseUrl, token)
5462
+ ]);
5463
+ const parsedAmt = depositAmount != null ? depositAmount : 0;
5464
+ const defaults = computeSmartDefaults(accts, parsedAmt);
5465
+ dispatch({ type: "ACCOUNTS_RELOADED", accounts: accts, providers: prov, defaults });
5466
+ }, [getAccessToken, state.activeCredentialId, apiBaseUrl, depositAmount]);
5467
+ const handleAuthorizedMobileReturn = useCallback(async (authorizedTransfer, isSetup) => {
5468
+ if (handlingMobileReturnRef.current) return;
5469
+ handlingMobileReturnRef.current = true;
5470
+ polling.stopPolling();
5471
+ if (isSetup) {
5472
+ mobileSetupFlowRef.current = false;
5473
+ clearMobileFlowState();
5074
5474
  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
- }
5475
+ await reloadAccounts();
5476
+ loadingDataRef.current = false;
5477
+ dispatch({ type: "MOBILE_SETUP_COMPLETE", transfer: authorizedTransfer });
5111
5478
  } 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);
5479
+ handlingMobileReturnRef.current = false;
5480
+ dispatch({
5481
+ type: "SET_ERROR",
5482
+ error: err instanceof Error ? err.message : "Wallet authorized, but we could not refresh your account yet."
5483
+ });
5484
+ dispatch({ type: "NAVIGATE", step: "open-wallet" });
5188
5485
  }
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
5486
  return;
5209
5487
  }
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)}.`);
5488
+ mobileSetupFlowRef.current = false;
5489
+ clearMobileFlowState();
5490
+ dispatch({ type: "MOBILE_SIGN_READY", transfer: authorizedTransfer });
5491
+ }, [polling.stopPolling, reloadAccounts]);
5492
+ const handlePay = useCallback(async (payAmount, sourceOverrides) => {
5493
+ if (isNaN(payAmount) || payAmount < MIN_SEND_AMOUNT_USD) {
5494
+ dispatch({ type: "SET_ERROR", error: `Minimum amount is $${MIN_SEND_AMOUNT_USD.toFixed(2)}.` });
5285
5495
  return;
5286
5496
  }
5287
5497
  if (!sourceOverrides?.sourceId && !sourceId) {
5288
- setError("No account or provider selected.");
5498
+ dispatch({ type: "SET_ERROR", error: "No account or provider selected." });
5289
5499
  return;
5290
5500
  }
5291
- if (!activeCredentialId) {
5292
- setError("Create a passkey on this device before continuing.");
5293
- setStep("create-passkey");
5501
+ if (!state.activeCredentialId) {
5502
+ dispatch({ type: "SET_ERROR", error: "Create a passkey on this device before continuing." });
5503
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5294
5504
  return;
5295
5505
  }
5296
5506
  const isSetupRedirect = mobileSetupFlowRef.current;
5297
- if (isSetupRedirect) {
5298
- setStep("open-wallet");
5299
- } else {
5300
- setStep("processing");
5301
- }
5507
+ dispatch({ type: "PAY_STARTED", isSetupRedirect });
5302
5508
  processingStartedAtRef.current = Date.now();
5303
- setError(null);
5304
- setCreatingTransfer(true);
5305
- setDeeplinkUri(null);
5306
- setMobileFlow(false);
5307
5509
  try {
5308
- if (transfer?.status === "AUTHORIZED") {
5309
- const signedTransfer2 = await transferSigning.signTransfer(transfer.id);
5310
- setTransfer(signedTransfer2);
5311
- polling.startPolling(transfer.id);
5510
+ if (state.transfer?.status === "AUTHORIZED") {
5511
+ const signedTransfer2 = await transferSigning.signTransfer(state.transfer.id);
5512
+ dispatch({ type: "TRANSFER_SIGNED", transfer: signedTransfer2 });
5513
+ polling.startPolling(state.transfer.id);
5312
5514
  return;
5313
5515
  }
5314
5516
  const token = await getAccessToken();
@@ -5316,21 +5518,21 @@ function SwypePaymentInner({
5316
5518
  let effectiveSourceType = sourceOverrides?.sourceType ?? sourceType;
5317
5519
  let effectiveSourceId = sourceOverrides?.sourceId ?? sourceId;
5318
5520
  if (effectiveSourceType === "accountId") {
5319
- const acct = accounts.find((a) => a.id === effectiveSourceId);
5521
+ const acct = state.accounts.find((a) => a.id === effectiveSourceId);
5320
5522
  const activeWallet = acct?.wallets.find((w) => w.status === "ACTIVE");
5321
5523
  if (activeWallet) {
5322
5524
  effectiveSourceType = "walletId";
5323
5525
  effectiveSourceId = activeWallet.id;
5324
5526
  }
5325
5527
  }
5326
- const isActiveWallet = effectiveSourceType === "walletId" && accounts.some(
5528
+ const isActiveWallet = effectiveSourceType === "walletId" && state.accounts.some(
5327
5529
  (a) => a.wallets.some((w) => w.id === effectiveSourceId && w.status === "ACTIVE")
5328
5530
  );
5329
5531
  if (!isActiveWallet && !isSetupRedirect) {
5330
5532
  let found = false;
5331
- for (const acct of accounts) {
5533
+ for (const acct of state.accounts) {
5332
5534
  for (const wallet of acct.wallets) {
5333
- if (wallet.status === "ACTIVE" && wallet.sources.some((s) => s.balance.available.amount >= parsedAmount)) {
5535
+ if (wallet.status === "ACTIVE" && wallet.sources.some((s) => s.balance.available.amount >= payAmount)) {
5334
5536
  effectiveSourceType = "walletId";
5335
5537
  effectiveSourceId = wallet.id;
5336
5538
  found = true;
@@ -5342,30 +5544,28 @@ function SwypePaymentInner({
5342
5544
  }
5343
5545
  const t = await createTransfer(apiBaseUrl, token, {
5344
5546
  id: idempotencyKey,
5345
- credentialId: activeCredentialId,
5547
+ credentialId: state.activeCredentialId,
5346
5548
  merchantAuthorization,
5347
5549
  sourceType: effectiveSourceType,
5348
5550
  sourceId: effectiveSourceId,
5349
5551
  destination,
5350
- amount: parsedAmount
5552
+ amount: payAmount
5351
5553
  });
5352
- setTransfer(t);
5554
+ dispatch({ type: "TRANSFER_CREATED", transfer: t });
5353
5555
  if (t.authorizationSessions && t.authorizationSessions.length > 0) {
5354
- const shouldUseConnector = shouldUseWalletConnector({
5355
- useWalletConnector,
5556
+ const useConnector = shouldUseWalletConnector({
5557
+ useWalletConnector: useWalletConnectorProp,
5356
5558
  userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent
5357
5559
  });
5358
- if (!shouldUseConnector) {
5560
+ if (!useConnector) {
5359
5561
  const uri = t.authorizationSessions[0].uri;
5360
- setMobileFlow(true);
5361
5562
  pollingTransferIdRef.current = t.id;
5362
5563
  polling.startPolling(t.id);
5363
- setDeeplinkUri(uri);
5364
- setStep("open-wallet");
5564
+ dispatch({ type: "MOBILE_DEEPLINK_READY", deeplinkUri: uri });
5365
5565
  persistMobileFlowState({
5366
5566
  transferId: t.id,
5367
5567
  deeplinkUri: uri,
5368
- providerId: sourceOverrides?.sourceType === "providerId" ? sourceOverrides.sourceId : selectedProviderId,
5568
+ providerId: sourceOverrides?.sourceType === "providerId" ? sourceOverrides.sourceId : state.selectedProviderId,
5369
5569
  isSetup: mobileSetupFlowRef.current
5370
5570
  });
5371
5571
  triggerDeeplink(uri);
@@ -5375,55 +5575,58 @@ function SwypePaymentInner({
5375
5575
  }
5376
5576
  }
5377
5577
  const signedTransfer = await transferSigning.signTransfer(t.id);
5378
- setTransfer(signedTransfer);
5578
+ dispatch({ type: "TRANSFER_SIGNED", transfer: signedTransfer });
5379
5579
  polling.startPolling(t.id);
5380
5580
  } catch (err) {
5381
5581
  captureException(err);
5382
5582
  const msg = err instanceof Error ? err.message : "Transfer failed";
5383
- setError(msg);
5583
+ dispatch({
5584
+ type: "PAY_ERROR",
5585
+ error: msg,
5586
+ fallbackStep: isSetupRedirect ? "wallet-picker" : "deposit"
5587
+ });
5384
5588
  onError?.(msg);
5385
- setStep(isSetupRedirect ? "wallet-picker" : "deposit");
5386
5589
  } finally {
5387
- setCreatingTransfer(false);
5590
+ dispatch({ type: "PAY_ENDED" });
5388
5591
  }
5389
5592
  }, [
5390
5593
  sourceId,
5391
5594
  sourceType,
5392
- activeCredentialId,
5595
+ state.activeCredentialId,
5596
+ state.transfer,
5597
+ state.accounts,
5598
+ state.selectedProviderId,
5393
5599
  destination,
5394
5600
  apiBaseUrl,
5395
5601
  getAccessToken,
5396
- accounts,
5397
5602
  authExecutor,
5398
5603
  transferSigning,
5399
5604
  polling,
5400
5605
  onError,
5401
- useWalletConnector,
5606
+ useWalletConnectorProp,
5402
5607
  idempotencyKey,
5403
- merchantAuthorization,
5404
- transfer
5608
+ merchantAuthorization
5405
5609
  ]);
5406
- const [increasingLimit, setIncreasingLimit] = useState(false);
5407
5610
  const handleIncreaseLimit = useCallback(async () => {
5408
5611
  const parsedAmount = depositAmount ?? 5;
5409
5612
  if (!sourceId) {
5410
- setError("No account or provider selected.");
5613
+ dispatch({ type: "SET_ERROR", error: "No account or provider selected." });
5411
5614
  return;
5412
5615
  }
5413
- if (!activeCredentialId) {
5414
- setError("Create a passkey on this device before continuing.");
5415
- setStep("create-passkey");
5616
+ if (!state.activeCredentialId) {
5617
+ dispatch({ type: "SET_ERROR", error: "Create a passkey on this device before continuing." });
5618
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5416
5619
  return;
5417
5620
  }
5418
- setError(null);
5419
- setIncreasingLimit(true);
5621
+ dispatch({ type: "SET_ERROR", error: null });
5622
+ dispatch({ type: "SET_INCREASING_LIMIT", value: true });
5420
5623
  try {
5421
5624
  const token = await getAccessToken();
5422
5625
  if (!token) throw new Error("Not authenticated");
5423
5626
  let effectiveSourceType = sourceType;
5424
5627
  let effectiveSourceId = sourceId;
5425
5628
  if (effectiveSourceType === "accountId") {
5426
- const acct = accounts.find((a) => a.id === effectiveSourceId);
5629
+ const acct = state.accounts.find((a) => a.id === effectiveSourceId);
5427
5630
  const activeWallet = acct?.wallets.find((w) => w.status === "ACTIVE");
5428
5631
  if (activeWallet) {
5429
5632
  effectiveSourceType = "walletId";
@@ -5432,198 +5635,118 @@ function SwypePaymentInner({
5432
5635
  }
5433
5636
  const t = await createTransfer(apiBaseUrl, token, {
5434
5637
  id: idempotencyKey,
5435
- credentialId: activeCredentialId,
5638
+ credentialId: state.activeCredentialId,
5436
5639
  merchantAuthorization,
5437
5640
  sourceType: effectiveSourceType,
5438
5641
  sourceId: effectiveSourceId,
5439
5642
  destination,
5440
5643
  amount: parsedAmount
5441
5644
  });
5442
- setTransfer(t);
5443
5645
  if (t.authorizationSessions && t.authorizationSessions.length > 0) {
5444
5646
  const uri = t.authorizationSessions[0].uri;
5445
- setMobileFlow(true);
5446
5647
  pollingTransferIdRef.current = t.id;
5447
5648
  mobileSetupFlowRef.current = true;
5448
5649
  handlingMobileReturnRef.current = false;
5449
5650
  polling.startPolling(t.id);
5450
- setDeeplinkUri(uri);
5651
+ dispatch({ type: "INCREASE_LIMIT_DEEPLINK", transfer: t, deeplinkUri: uri });
5451
5652
  persistMobileFlowState({
5452
5653
  transferId: t.id,
5453
5654
  deeplinkUri: uri,
5454
- providerId: selectedProviderId,
5655
+ providerId: state.selectedProviderId,
5455
5656
  isSetup: true
5456
5657
  });
5457
5658
  triggerDeeplink(uri);
5659
+ } else {
5660
+ dispatch({ type: "TRANSFER_CREATED", transfer: t });
5458
5661
  }
5459
5662
  } catch (err) {
5460
5663
  captureException(err);
5461
5664
  const msg = err instanceof Error ? err.message : "Failed to increase limit";
5462
- setError(msg);
5665
+ dispatch({ type: "SET_ERROR", error: msg });
5463
5666
  onError?.(msg);
5464
5667
  } finally {
5465
- setIncreasingLimit(false);
5668
+ dispatch({ type: "SET_INCREASING_LIMIT", value: false });
5466
5669
  }
5467
5670
  }, [
5468
5671
  depositAmount,
5469
5672
  sourceId,
5470
5673
  sourceType,
5471
- activeCredentialId,
5674
+ state.activeCredentialId,
5675
+ state.accounts,
5676
+ state.selectedProviderId,
5472
5677
  apiBaseUrl,
5473
5678
  getAccessToken,
5474
- accounts,
5475
5679
  polling,
5476
5680
  onError,
5477
5681
  idempotencyKey,
5478
5682
  merchantAuthorization,
5479
- destination,
5480
- selectedProviderId
5683
+ destination
5481
5684
  ]);
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);
5685
+ const handleConfirmSign = useCallback(async () => {
5686
+ const t = state.transfer ?? polling.transfer;
5687
+ if (!t) return;
5525
5688
  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);
5689
+ const signedTransfer = await transferSigning.signTransfer(t.id);
5690
+ clearMobileFlowState();
5691
+ dispatch({ type: "CONFIRM_SIGN_SUCCESS", transfer: signedTransfer });
5692
+ polling.startPolling(t.id);
5549
5693
  } catch (err) {
5550
5694
  captureException(err);
5551
- setError(err instanceof Error ? err.message : "Failed to register passkey");
5552
- } finally {
5553
- setRegisteringPasskey(false);
5695
+ const msg = err instanceof Error ? err.message : "Failed to sign transfer";
5696
+ dispatch({ type: "SET_ERROR", error: msg });
5697
+ onError?.(msg);
5554
5698
  }
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);
5699
+ }, [state.transfer, polling.transfer, polling.startPolling, transferSigning, onError]);
5700
+ const handleRetryMobileStatus = useCallback(() => {
5701
+ dispatch({ type: "SET_ERROR", error: null });
5702
+ handlingMobileReturnRef.current = false;
5703
+ const currentTransfer = polling.transfer ?? state.transfer;
5704
+ if (currentTransfer?.status === "AUTHORIZED") {
5705
+ void handleAuthorizedMobileReturn(currentTransfer, mobileSetupFlowRef.current);
5706
+ return;
5582
5707
  }
5583
- }, [knownCredentialIds, getAccessToken, apiBaseUrl]);
5708
+ const transferIdToResume = pollingTransferIdRef.current ?? currentTransfer?.id;
5709
+ if (transferIdToResume) {
5710
+ polling.startPolling(transferIdToResume);
5711
+ }
5712
+ }, [handleAuthorizedMobileReturn, polling, state.transfer]);
5584
5713
  const handleSelectProvider = useCallback((providerId) => {
5585
- setSelectedProviderId(providerId);
5586
- setSelectedAccountId(null);
5587
- setConnectingNewAccount(true);
5714
+ dispatch({ type: "SELECT_PROVIDER", providerId });
5588
5715
  const isMobile = !shouldUseWalletConnector({
5589
- useWalletConnector,
5716
+ useWalletConnector: useWalletConnectorProp,
5590
5717
  userAgent: typeof navigator === "undefined" ? void 0 : navigator.userAgent
5591
5718
  });
5592
5719
  if (isMobile) {
5593
5720
  handlingMobileReturnRef.current = false;
5594
5721
  mobileSetupFlowRef.current = true;
5595
- const amount2 = depositAmount ?? 5;
5596
- handlePay(amount2, { sourceType: "providerId", sourceId: providerId });
5722
+ const amount = depositAmount ?? 5;
5723
+ handlePay(amount, { sourceType: "providerId", sourceId: providerId });
5597
5724
  } else {
5598
- setStep("deposit");
5725
+ dispatch({ type: "NAVIGATE", step: "deposit" });
5599
5726
  }
5600
- }, [useWalletConnector, depositAmount, handlePay]);
5727
+ }, [useWalletConnectorProp, depositAmount, handlePay]);
5601
5728
  const handleContinueConnection = useCallback(
5602
5729
  (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");
5730
+ const acct = state.accounts.find((a) => a.id === accountId);
5731
+ dispatch({
5732
+ type: "SELECT_ACCOUNT",
5733
+ accountId,
5734
+ walletId: acct?.wallets[0]?.id ?? null
5735
+ });
5608
5736
  },
5609
- [accounts]
5737
+ [state.accounts]
5610
5738
  );
5611
5739
  const handleNewPayment = useCallback(() => {
5612
5740
  clearMobileFlowState();
5613
- setStep("deposit");
5614
- setTransfer(null);
5615
- setError(null);
5616
- setAmount(depositAmount != null ? depositAmount.toString() : "");
5617
- setMobileFlow(false);
5618
- setDeeplinkUri(null);
5619
5741
  processingStartedAtRef.current = null;
5620
5742
  pollingTransferIdRef.current = null;
5621
- mobileSigningTransferIdRef.current = null;
5622
5743
  preSelectSourceStepRef.current = null;
5623
- setConnectingNewAccount(false);
5624
- setSelectedWalletId(null);
5625
- if (accounts.length > 0) setSelectedAccountId(accounts[0].id);
5626
- }, [depositAmount, accounts]);
5744
+ dispatch({
5745
+ type: "NEW_PAYMENT",
5746
+ depositAmount,
5747
+ firstAccountId: state.accounts.length > 0 ? state.accounts[0].id : null
5748
+ });
5749
+ }, [depositAmount, state.accounts]);
5627
5750
  const handleLogout = useCallback(async () => {
5628
5751
  try {
5629
5752
  await logout();
@@ -5634,254 +5757,464 @@ function SwypePaymentInner({
5634
5757
  window.localStorage.removeItem(ACTIVE_CREDENTIAL_STORAGE_KEY);
5635
5758
  }
5636
5759
  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
5760
  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);
5761
+ setAuthInput("");
5762
+ setOtpCode("");
5763
+ dispatch({ type: "LOGOUT", depositAmount });
5764
+ }, [logout, polling, depositAmount]);
5765
+ const pendingSelectSourceAction = authExecutor.pendingSelectSource;
5766
+ const selectSourceChoices = useMemo(() => {
5767
+ if (!pendingSelectSourceAction) return [];
5768
+ const options = pendingSelectSourceAction.metadata?.options ?? [];
5769
+ return buildSelectSourceChoices(options);
5770
+ }, [pendingSelectSourceAction]);
5771
+ const selectSourceRecommended = useMemo(() => {
5772
+ if (!pendingSelectSourceAction) return null;
5773
+ return pendingSelectSourceAction.metadata?.recommended ?? null;
5774
+ }, [pendingSelectSourceAction]);
5775
+ const handleSelectSourceChainChange = useCallback(
5776
+ (chainName) => {
5777
+ setSelectSourceChainName(chainName);
5778
+ const chain = selectSourceChoices.find((c) => c.chainName === chainName);
5779
+ if (!chain || chain.tokens.length === 0) return;
5780
+ const recommendedToken = selectSourceRecommended?.chainName === chainName ? selectSourceRecommended.tokenSymbol : null;
5781
+ const hasRecommended = !!recommendedToken && chain.tokens.some((t) => t.tokenSymbol === recommendedToken);
5782
+ setSelectSourceTokenSymbol(
5783
+ hasRecommended ? recommendedToken : chain.tokens[0].tokenSymbol
5784
+ );
5785
+ },
5786
+ [selectSourceChoices, selectSourceRecommended]
5787
+ );
5788
+ const handleConfirmSelectSource = useCallback(() => {
5789
+ authExecutor.resolveSelectSource({
5790
+ chainName: selectSourceChainName,
5791
+ tokenSymbol: selectSourceTokenSymbol
5792
+ });
5793
+ }, [authExecutor, selectSourceChainName, selectSourceTokenSymbol]);
5794
+ useEffect(() => {
5795
+ if (depositAmount != null) {
5796
+ dispatch({ type: "SYNC_AMOUNT", amount: depositAmount.toString() });
5670
5797
  }
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
5798
+ }, [depositAmount]);
5799
+ useEffect(() => {
5800
+ if (authenticated) return;
5801
+ if (activeOtpErrorMessage) dispatch({ type: "SET_ERROR", error: activeOtpErrorMessage });
5802
+ }, [activeOtpErrorMessage, authenticated]);
5803
+ useEffect(() => {
5804
+ if (state.step === "otp-verify" && /^\d{6}$/.test(otpCode.trim()) && activeOtpStatus !== "submitting-code") {
5805
+ handleVerifyLoginCode();
5806
+ }
5807
+ }, [otpCode, state.step, activeOtpStatus, handleVerifyLoginCode]);
5808
+ useEffect(() => {
5809
+ if (!ready || !authenticated) return;
5810
+ if (state.step !== "login" && state.step !== "otp-verify") return;
5811
+ let cancelled = false;
5812
+ dispatch({ type: "SET_ERROR", error: null });
5813
+ setAuthInput("");
5814
+ setOtpCode("");
5815
+ const restoreOrDeposit = async (credId, token) => {
5816
+ const persisted = loadMobileFlowState();
5817
+ let accts = [];
5818
+ try {
5819
+ accts = await fetchAccounts(apiBaseUrl, token, credId);
5820
+ if (cancelled) return;
5821
+ } catch {
5736
5822
  }
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")
5823
+ const resolved = resolvePostAuthStep({
5824
+ hasPasskey: true,
5825
+ accounts: accts,
5826
+ persistedMobileFlow: persisted,
5827
+ mobileSetupInProgress: false,
5828
+ connectingNewAccount: false
5829
+ });
5830
+ if (resolved.clearPersistedFlow) clearMobileFlowState();
5831
+ if (resolved.step === "deposit" && persisted && persisted.isSetup) {
5832
+ try {
5833
+ const existingTransfer = await fetchTransfer(apiBaseUrl, token, persisted.transferId);
5834
+ if (cancelled) return;
5835
+ if (existingTransfer.status === "AUTHORIZED") {
5836
+ await handleAuthorizedMobileReturn(existingTransfer, true);
5837
+ return;
5838
+ }
5839
+ } catch {
5840
+ }
5749
5841
  }
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
5842
+ if (resolved.step === "open-wallet" && persisted) {
5843
+ try {
5844
+ const existingTransfer = await fetchTransfer(apiBaseUrl, token, persisted.transferId);
5845
+ if (cancelled) return;
5846
+ const mobileResolution = resolveRestoredMobileFlow(
5847
+ existingTransfer.status,
5848
+ persisted.isSetup
5849
+ );
5850
+ if (mobileResolution.kind === "resume-setup-deposit") {
5851
+ await handleAuthorizedMobileReturn(existingTransfer, true);
5852
+ return;
5853
+ }
5854
+ if (mobileResolution.kind === "resume-confirm-sign") {
5855
+ await handleAuthorizedMobileReturn(existingTransfer, false);
5856
+ return;
5857
+ }
5858
+ if (mobileResolution.kind === "resume-success") {
5859
+ clearMobileFlowState();
5860
+ dispatch({ type: "MOBILE_RESUME_SUCCESS", transfer: existingTransfer });
5861
+ onComplete?.(existingTransfer);
5862
+ return;
5863
+ }
5864
+ if (mobileResolution.kind === "resume-failed") {
5865
+ clearMobileFlowState();
5866
+ dispatch({ type: "MOBILE_RESUME_FAILED", transfer: existingTransfer });
5867
+ return;
5868
+ }
5869
+ if (mobileResolution.kind === "resume-processing") {
5870
+ clearMobileFlowState();
5871
+ dispatch({ type: "MOBILE_RESUME_PROCESSING", transfer: existingTransfer });
5872
+ polling.startPolling(existingTransfer.id);
5873
+ return;
5874
+ }
5875
+ if (mobileResolution.kind === "resume-stale-setup") {
5876
+ clearMobileFlowState();
5877
+ if (!cancelled) dispatch({ type: "NAVIGATE", step: "wallet-picker" });
5878
+ return;
5879
+ }
5880
+ } catch (err) {
5881
+ if (cancelled) return;
5882
+ dispatch({
5883
+ type: "ENTER_MOBILE_FLOW",
5884
+ deeplinkUri: persisted.deeplinkUri,
5885
+ providerId: persisted.providerId,
5886
+ error: err instanceof Error ? err.message : "Unable to refresh wallet authorization status."
5887
+ });
5888
+ pollingTransferIdRef.current = persisted.transferId;
5889
+ mobileSetupFlowRef.current = persisted.isSetup;
5890
+ polling.startPolling(persisted.transferId);
5891
+ return;
5892
+ }
5893
+ dispatch({
5894
+ type: "ENTER_MOBILE_FLOW",
5895
+ deeplinkUri: persisted.deeplinkUri,
5896
+ providerId: persisted.providerId
5897
+ });
5898
+ pollingTransferIdRef.current = persisted.transferId;
5899
+ mobileSetupFlowRef.current = persisted.isSetup;
5900
+ polling.startPolling(persisted.transferId);
5901
+ return;
5763
5902
  }
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
5903
+ dispatch({ type: "NAVIGATE", step: resolved.step });
5904
+ };
5905
+ const checkPasskey = async () => {
5906
+ try {
5907
+ const token = await getAccessToken();
5908
+ if (!token || cancelled) return;
5909
+ const { config } = await fetchUserConfig(apiBaseUrl, token);
5910
+ if (cancelled) return;
5911
+ const allPasskeys = config.passkeys ?? (config.passkey ? [config.passkey] : []);
5912
+ dispatch({
5913
+ type: "PASSKEY_CONFIG_LOADED",
5914
+ knownIds: allPasskeys.map((p) => p.credentialId),
5915
+ oneTapLimit: config.defaultAllowance ?? void 0
5916
+ });
5917
+ if (allPasskeys.length === 0) {
5918
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5919
+ return;
5920
+ }
5921
+ if (state.activeCredentialId && allPasskeys.some((p) => p.credentialId === state.activeCredentialId)) {
5922
+ await restoreOrDeposit(state.activeCredentialId, token);
5923
+ return;
5924
+ }
5925
+ if (cancelled) return;
5926
+ if (isSafari() && isInCrossOriginIframe()) {
5927
+ dispatch({ type: "NAVIGATE", step: "verify-passkey" });
5928
+ return;
5929
+ }
5930
+ const credentialIds = allPasskeys.map((p) => p.credentialId);
5931
+ const matched = await findDevicePasskey(credentialIds);
5932
+ if (cancelled) return;
5933
+ if (matched) {
5934
+ dispatch({ type: "PASSKEY_ACTIVATED", credentialId: matched });
5935
+ window.localStorage.setItem(ACTIVE_CREDENTIAL_STORAGE_KEY, matched);
5936
+ reportPasskeyActivity(apiBaseUrl, token, matched).catch(() => {
5937
+ });
5938
+ await restoreOrDeposit(matched, token);
5939
+ return;
5940
+ }
5941
+ dispatch({ type: "NAVIGATE", step: "create-passkey" });
5942
+ } catch {
5943
+ if (!cancelled) dispatch({ type: "NAVIGATE", step: "create-passkey" });
5776
5944
  }
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..." }) }) });
5945
+ };
5946
+ checkPasskey();
5947
+ return () => {
5948
+ cancelled = true;
5949
+ };
5950
+ }, [
5951
+ ready,
5952
+ authenticated,
5953
+ state.step,
5954
+ apiBaseUrl,
5955
+ getAccessToken,
5956
+ state.activeCredentialId,
5957
+ handleAuthorizedMobileReturn,
5958
+ onComplete,
5959
+ polling
5960
+ ]);
5961
+ useEffect(() => {
5962
+ const loadAction = resolveDataLoadAction({
5963
+ authenticated,
5964
+ step: state.step,
5965
+ accountsCount: state.accounts.length,
5966
+ hasActiveCredential: !!state.activeCredentialId,
5967
+ loading: loadingDataRef.current
5968
+ });
5969
+ if (loadAction === "reset") {
5970
+ loadingDataRef.current = false;
5971
+ dispatch({ type: "DATA_LOAD_END" });
5972
+ return;
5782
5973
  }
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
5974
+ if (loadAction === "wait") return;
5975
+ const credentialId = state.activeCredentialId;
5976
+ if (!credentialId) {
5977
+ loadingDataRef.current = false;
5978
+ dispatch({ type: "DATA_LOAD_END" });
5979
+ return;
5980
+ }
5981
+ let cancelled = false;
5982
+ loadingDataRef.current = true;
5983
+ const load = async () => {
5984
+ dispatch({ type: "DATA_LOAD_START" });
5985
+ try {
5986
+ const token = await getAccessToken();
5987
+ if (!token) throw new Error("Not authenticated");
5988
+ const [prov, accts, chn] = await Promise.all([
5989
+ fetchProviders(apiBaseUrl, token),
5990
+ fetchAccounts(apiBaseUrl, token, credentialId),
5991
+ fetchChains(apiBaseUrl, token)
5992
+ ]);
5993
+ if (cancelled) return;
5994
+ const parsedAmt = depositAmount != null ? depositAmount : 0;
5995
+ const defaults = computeSmartDefaults(accts, parsedAmt);
5996
+ const persisted = loadMobileFlowState();
5997
+ const resolved = resolvePostAuthStep({
5998
+ hasPasskey: !!state.activeCredentialId,
5999
+ accounts: accts,
6000
+ persistedMobileFlow: persisted,
6001
+ mobileSetupInProgress: mobileSetupFlowRef.current,
6002
+ connectingNewAccount: state.connectingNewAccount
6003
+ });
6004
+ const correctableSteps = ["deposit", "wallet-picker", "open-wallet"];
6005
+ dispatch({
6006
+ type: "DATA_LOADED",
6007
+ providers: prov,
6008
+ accounts: accts,
6009
+ chains: chn,
6010
+ defaults,
6011
+ fallbackProviderId: !defaults && prov.length > 0 ? prov[0].id : null,
6012
+ resolvedStep: correctableSteps.includes(state.step) ? resolved.step : void 0,
6013
+ clearMobileState: resolved.clearPersistedFlow
6014
+ });
6015
+ if (resolved.clearPersistedFlow) clearMobileFlowState();
6016
+ } catch (err) {
6017
+ if (!cancelled) {
6018
+ captureException(err);
6019
+ dispatch({
6020
+ type: "SET_ERROR",
6021
+ error: err instanceof Error ? err.message : "Failed to load data"
5833
6022
  });
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
6023
+ }
6024
+ } finally {
6025
+ if (!cancelled) {
6026
+ loadingDataRef.current = false;
6027
+ dispatch({ type: "DATA_LOAD_END" });
6028
+ }
5859
6029
  }
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
6030
+ };
6031
+ load();
6032
+ return () => {
6033
+ cancelled = true;
6034
+ loadingDataRef.current = false;
6035
+ };
6036
+ }, [
6037
+ authenticated,
6038
+ state.step,
6039
+ state.accounts.length,
6040
+ apiBaseUrl,
6041
+ getAccessToken,
6042
+ state.activeCredentialId,
6043
+ depositAmount,
6044
+ state.connectingNewAccount
6045
+ ]);
6046
+ useEffect(() => {
6047
+ if (!polling.transfer) return;
6048
+ if (polling.transfer.status === "COMPLETED") {
6049
+ clearMobileFlowState();
6050
+ dispatch({ type: "TRANSFER_COMPLETED", transfer: polling.transfer });
6051
+ onComplete?.(polling.transfer);
6052
+ } else if (polling.transfer.status === "FAILED") {
6053
+ clearMobileFlowState();
6054
+ dispatch({ type: "TRANSFER_FAILED", transfer: polling.transfer, error: "Transfer failed." });
6055
+ }
6056
+ }, [polling.transfer, onComplete]);
6057
+ useEffect(() => {
6058
+ if (state.step !== "processing") {
6059
+ processingStartedAtRef.current = null;
6060
+ return;
6061
+ }
6062
+ if (!processingStartedAtRef.current) {
6063
+ processingStartedAtRef.current = Date.now();
6064
+ }
6065
+ const elapsedMs = Date.now() - processingStartedAtRef.current;
6066
+ const remainingMs = PROCESSING_TIMEOUT_MS - elapsedMs;
6067
+ const handleTimeout = () => {
6068
+ if (!hasProcessingTimedOut(processingStartedAtRef.current, Date.now())) return;
6069
+ const status = getTransferStatus(polling.transfer, state.transfer);
6070
+ const msg = buildProcessingTimeoutMessage(status);
6071
+ captureException(new Error(msg));
6072
+ polling.stopPolling();
6073
+ dispatch({ type: "PROCESSING_TIMEOUT", error: msg });
6074
+ onError?.(msg);
6075
+ };
6076
+ if (remainingMs <= 0) {
6077
+ handleTimeout();
6078
+ return;
6079
+ }
6080
+ const timeoutId = window.setTimeout(handleTimeout, remainingMs);
6081
+ return () => window.clearTimeout(timeoutId);
6082
+ }, [state.step, polling.transfer, state.transfer, polling.stopPolling, onError]);
6083
+ useEffect(() => {
6084
+ if (!state.mobileFlow) {
6085
+ handlingMobileReturnRef.current = false;
6086
+ return;
6087
+ }
6088
+ if (handlingMobileReturnRef.current) return;
6089
+ const polledTransfer = polling.transfer;
6090
+ if (!polledTransfer || polledTransfer.status !== "AUTHORIZED") return;
6091
+ void handleAuthorizedMobileReturn(polledTransfer, mobileSetupFlowRef.current);
6092
+ }, [state.mobileFlow, polling.transfer, handleAuthorizedMobileReturn]);
6093
+ useEffect(() => {
6094
+ if (!state.mobileFlow) return;
6095
+ if (handlingMobileReturnRef.current) return;
6096
+ const transferIdToResume = pollingTransferIdRef.current ?? state.transfer?.id;
6097
+ if (!transferIdToResume) return;
6098
+ if (!polling.isPolling) polling.startPolling(transferIdToResume);
6099
+ const handleVisibility = () => {
6100
+ if (document.visibilityState === "visible" && !handlingMobileReturnRef.current) {
6101
+ polling.startPolling(transferIdToResume);
5881
6102
  }
6103
+ };
6104
+ document.addEventListener("visibilitychange", handleVisibility);
6105
+ return () => document.removeEventListener("visibilitychange", handleVisibility);
6106
+ }, [state.mobileFlow, state.transfer?.id, polling.isPolling, polling.startPolling]);
6107
+ useEffect(() => {
6108
+ if (!pendingSelectSourceAction) {
6109
+ initializedSelectSourceActionRef.current = null;
6110
+ setSelectSourceChainName("");
6111
+ setSelectSourceTokenSymbol("");
6112
+ return;
6113
+ }
6114
+ if (initializedSelectSourceActionRef.current === pendingSelectSourceAction.id) return;
6115
+ const hasRecommended = !!selectSourceRecommended && selectSourceChoices.some(
6116
+ (chain) => chain.chainName === selectSourceRecommended.chainName && chain.tokens.some((t) => t.tokenSymbol === selectSourceRecommended.tokenSymbol)
5882
6117
  );
5883
- }
5884
- return null;
6118
+ if (hasRecommended && selectSourceRecommended) {
6119
+ setSelectSourceChainName(selectSourceRecommended.chainName);
6120
+ setSelectSourceTokenSymbol(selectSourceRecommended.tokenSymbol);
6121
+ } else if (selectSourceChoices.length > 0 && selectSourceChoices[0].tokens.length > 0) {
6122
+ setSelectSourceChainName(selectSourceChoices[0].chainName);
6123
+ setSelectSourceTokenSymbol(selectSourceChoices[0].tokens[0].tokenSymbol);
6124
+ } else {
6125
+ setSelectSourceChainName("Base");
6126
+ setSelectSourceTokenSymbol("USDC");
6127
+ }
6128
+ initializedSelectSourceActionRef.current = pendingSelectSourceAction.id;
6129
+ }, [pendingSelectSourceAction, selectSourceChoices, selectSourceRecommended]);
6130
+ useEffect(() => {
6131
+ if (pendingSelectSourceAction && state.step === "processing") {
6132
+ preSelectSourceStepRef.current = state.step;
6133
+ dispatch({ type: "NAVIGATE", step: "select-source" });
6134
+ } else if (!pendingSelectSourceAction && state.step === "select-source") {
6135
+ dispatch({ type: "NAVIGATE", step: preSelectSourceStepRef.current ?? "processing" });
6136
+ preSelectSourceStepRef.current = null;
6137
+ }
6138
+ }, [pendingSelectSourceAction, state.step]);
6139
+ const handlers = useMemo(() => ({
6140
+ onSendLoginCode: handleSendLoginCode,
6141
+ onVerifyLoginCode: handleVerifyLoginCode,
6142
+ onResendLoginCode: handleResendLoginCode,
6143
+ onBackFromOtp: () => {
6144
+ setOtpCode("");
6145
+ dispatch({ type: "BACK_TO_LOGIN" });
6146
+ },
6147
+ onRegisterPasskey: handleRegisterPasskey,
6148
+ onCreatePasskeyViaPopup: handleCreatePasskeyViaPopup,
6149
+ onVerifyPasskeyViaPopup: handleVerifyPasskeyViaPopup,
6150
+ onSelectProvider: handleSelectProvider,
6151
+ onContinueConnection: handleContinueConnection,
6152
+ onPay: handlePay,
6153
+ onIncreaseLimit: handleIncreaseLimit,
6154
+ onConfirmSign: handleConfirmSign,
6155
+ onRetryMobileStatus: handleRetryMobileStatus,
6156
+ onLogout: handleLogout,
6157
+ onNewPayment: handleNewPayment,
6158
+ onNavigate: (step) => dispatch({ type: "NAVIGATE", step }),
6159
+ onSetAuthInput: setAuthInput,
6160
+ onSetOtpCode: (code) => {
6161
+ setOtpCode(code);
6162
+ dispatch({ type: "SET_ERROR", error: null });
6163
+ },
6164
+ onSelectSourceChainChange: handleSelectSourceChainChange,
6165
+ onSetSelectSourceTokenSymbol: setSelectSourceTokenSymbol,
6166
+ onConfirmSelectSource: handleConfirmSelectSource
6167
+ }), [
6168
+ handleSendLoginCode,
6169
+ handleVerifyLoginCode,
6170
+ handleResendLoginCode,
6171
+ handleRegisterPasskey,
6172
+ handleCreatePasskeyViaPopup,
6173
+ handleVerifyPasskeyViaPopup,
6174
+ handleSelectProvider,
6175
+ handleContinueConnection,
6176
+ handlePay,
6177
+ handleIncreaseLimit,
6178
+ handleConfirmSign,
6179
+ handleRetryMobileStatus,
6180
+ handleLogout,
6181
+ handleNewPayment,
6182
+ handleSelectSourceChainChange,
6183
+ handleConfirmSelectSource
6184
+ ]);
6185
+ return /* @__PURE__ */ jsx(
6186
+ StepRenderer,
6187
+ {
6188
+ state,
6189
+ ready,
6190
+ authenticated,
6191
+ activeOtpStatus,
6192
+ pollingTransfer: polling.transfer,
6193
+ pollingError: polling.error,
6194
+ authExecutorError: authExecutor.error,
6195
+ transferSigningSigning: transferSigning.signing,
6196
+ transferSigningError: transferSigning.error,
6197
+ pendingConnections,
6198
+ sourceName,
6199
+ sourceAddress,
6200
+ sourceVerified,
6201
+ maxSourceBalance,
6202
+ tokenCount,
6203
+ selectedAccount,
6204
+ selectSourceChoices,
6205
+ selectSourceRecommended,
6206
+ authInput,
6207
+ otpCode,
6208
+ selectSourceChainName,
6209
+ selectSourceTokenSymbol,
6210
+ merchantName,
6211
+ onBack,
6212
+ onDismiss,
6213
+ autoCloseSeconds,
6214
+ depositAmount,
6215
+ handlers
6216
+ }
6217
+ );
5885
6218
  }
5886
6219
 
5887
6220
  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 };