From 1da5fafceeb1f92788dd881788d6d0debb08ba66 Mon Sep 17 00:00:00 2001 From: mul53 Date: Wed, 16 Sep 2026 22:19:39 +0200 Subject: [PATCH 1/3] Open savings deposits on the amount form, not the token list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every savings entry point — the header and mobile Deposit buttons, the empty state, the funded action row, Start earning and the home balances dropdown — opened "Deposit to savings" first: a token list whose only route to the amount form was the "Move from wallet" row buried under it. Point them straight at the form with depositFromSolid set, so the first screen is the one that takes an amount. The form already knows how to say there is nothing to deposit, but it read an unloaded balance as an empty one. As a step behind the token list that flashed by; as the entry point it would greet most users with the empty state before their balance arrived. Hold the decision until balances have loaded. The card's minimum-deposit step still opens the token list: it funds savings by sending new money in, which the form does not do. Co-Authored-By: Claude Opus 5 --- app/(protected)/(tabs)/savings.tsx | 4 +- .../DepositToVault/DepositToVaultForm.tsx | 40 ++++++++++++++----- .../DepositToVault/EmptyDepositTokens.tsx | 7 +++- .../NewHome/OtherBalancesDropdown/index.tsx | 4 +- components/Savings/EmptyState.tsx | 4 +- .../NewSavings/SavingsFundedActions.tsx | 4 +- .../Savings/NewSavings/StartEarningButton.tsx | 4 +- components/Savings/SavingDepositButton.tsx | 4 +- .../Savings/SavingsHeaderButtonsMobile.tsx | 4 +- hooks/useDepositOption.tsx | 4 +- 10 files changed, 53 insertions(+), 26 deletions(-) diff --git a/app/(protected)/(tabs)/savings.tsx b/app/(protected)/(tabs)/savings.tsx index 3b8cadae4..7ff1b30e5 100644 --- a/app/(protected)/(tabs)/savings.tsx +++ b/app/(protected)/(tabs)/savings.tsx @@ -171,11 +171,11 @@ function LegacySavings() { /> { - useDepositStore.getState().setDepositFromSolid(false); + useDepositStore.getState().setDepositFromSolid(true); }} /> diff --git a/components/DepositToVault/DepositToVaultForm.tsx b/components/DepositToVault/DepositToVaultForm.tsx index 72f5ae08f..3f74bfa8b 100644 --- a/components/DepositToVault/DepositToVaultForm.tsx +++ b/components/DepositToVault/DepositToVaultForm.tsx @@ -85,8 +85,15 @@ function DepositToVaultForm() { const { isScreenMedium } = useDimension(); const { vault, depositConfig } = useVaultDepositConfig(); const { data: vaultExchangeRate } = useVaultExchangeRate(vault.name); - const { ethereumTokens, fuseTokens, polygonTokens, baseTokens, arbitrumTokens, bscTokens } = - useWalletTokens(); + const { + ethereumTokens, + fuseTokens, + polygonTokens, + baseTokens, + arbitrumTokens, + bscTokens, + isLoading: isLoadingWalletTokens, + } = useWalletTokens(); // Sum across all (chain, token) pairs the selected vault accepts. If nothing // depositable is held in the user's wallet, show the empty state. @@ -117,6 +124,11 @@ function DepositToVaultForm() { }); }, [ethereumTokens, fuseTokens, polygonTokens, baseTokens, arbitrumTokens, bscTokens, vault]); + // Balances start empty, so "no depositable balance" only means something once + // they have loaded. A balance already in hand is enough to show the form even + // while a refresh is in flight. + const isBalanceUnknown = isLoadingWalletTokens && !hasDepositableBalance; + const vaultToken = vault.vaultToken ?? 'soUSD'; const vaultTokenIcon = vault.name === 'USDC' ? getAsset('images/sousd-4x.png') : getAsset(vault.icon); @@ -243,6 +255,9 @@ function DepositToVaultForm() { const useSolidForFuse = isFuseVault && depositFromSolid; const useSolidForEth = isEthVault && depositFromSolid; const useSolidForUsdc = !isFuseVault && !isEthVault && depositFromSolid; + // Exactly one of the three holds whenever the deposit comes from the Solid + // balance, which is also when the form has no external wallet to name. + const isDepositFromSolid = useSolidForFuse || useSolidForEth || useSolidForUsdc; // Synthesize a TokenBalance for the WalletTokenButton when depositFromSolid const selectedWalletToken: TokenBalance | null = useMemo(() => { @@ -560,18 +575,25 @@ function DepositToVaultForm() { return ( - - - {useSolidForFuse || useSolidForEth || useSolidForUsdc ? '' : 'From wallet'} - - {!useSolidForFuse && !useSolidForEth && !useSolidForUsdc && } - + {!isDepositFromSolid && ( + + From wallet + + + )} Destination - {!hasDepositableBalance ? ( + {isBalanceUnknown ? ( + // This form is the savings deposit entry point, so balances are + // usually still in flight on first open. Waiting here keeps the user + // off an empty state that a loaded balance is about to contradict. + + + + ) : !hasDepositableBalance ? ( ) : ( <> diff --git a/components/DepositToVault/EmptyDepositTokens.tsx b/components/DepositToVault/EmptyDepositTokens.tsx index 51ea57091..6d6fe8d5e 100644 --- a/components/DepositToVault/EmptyDepositTokens.tsx +++ b/components/DepositToVault/EmptyDepositTokens.tsx @@ -55,6 +55,9 @@ type EmptyDepositTokensProps = { const EmptyDepositTokens = ({ vault }: EmptyDepositTokensProps) => { const supportedTokens = useMemo(() => getSupportedTokens(vault), [vault]); + // The headline stacks the symbols ("USDC/USDT"); the sentence below reads them + // out ("USDC or USDT"). + const tokenPair = supportedTokens.map(t => t.symbol).join('/'); const tokenList = supportedTokens.map(t => t.symbol).join(' or '); const setModal = useDepositStore(useShallow(state => state.setModal)); @@ -88,7 +91,7 @@ const EmptyDepositTokens = ({ vault }: EmptyDepositTokensProps) => { - You don’t have {tokenList} balance + You don’t have {tokenPair} balance {tokenList && ( @@ -98,7 +101,7 @@ const EmptyDepositTokens = ({ vault }: EmptyDepositTokensProps) => { )} ); diff --git a/components/Home/NewHome/OtherBalancesDropdown/index.tsx b/components/Home/NewHome/OtherBalancesDropdown/index.tsx index 5f8890de8..b500ef57c 100644 --- a/components/Home/NewHome/OtherBalancesDropdown/index.tsx +++ b/components/Home/NewHome/OtherBalancesDropdown/index.tsx @@ -288,12 +288,12 @@ export const SavingsBalanceRow = ({ }) => ( { onDismiss?.(); - useDepositStore.getState().setDepositFromSolid(false); + useDepositStore.getState().setDepositFromSolid(true); }} trigger={} /> diff --git a/components/Savings/EmptyState.tsx b/components/Savings/EmptyState.tsx index cdf1b96b7..63436f414 100644 --- a/components/Savings/EmptyState.tsx +++ b/components/Savings/EmptyState.tsx @@ -35,11 +35,11 @@ export default function SavingsEmptyState() { { - useDepositStore.getState().setDepositFromSolid(false); + useDepositStore.getState().setDepositFromSolid(true); }} /> diff --git a/components/Savings/NewSavings/SavingsFundedActions.tsx b/components/Savings/NewSavings/SavingsFundedActions.tsx index 88b93680e..e386c6306 100644 --- a/components/Savings/NewSavings/SavingsFundedActions.tsx +++ b/components/Savings/NewSavings/SavingsFundedActions.tsx @@ -42,7 +42,7 @@ const SavingsFundedActions = ({ vaultType }: SavingsFundedActionsProps) => { return ( { @@ -50,7 +50,7 @@ const SavingsFundedActions = ({ vaultType }: SavingsFundedActionsProps) => { if (index >= 0) { useSavingStore.getState().selectVaultForDeposit(index); } - useDepositStore.getState().setDepositFromSolid(false); + useDepositStore.getState().setDepositFromSolid(true); }} trigger={} /> diff --git a/components/Savings/NewSavings/StartEarningButton.tsx b/components/Savings/NewSavings/StartEarningButton.tsx index 7df07b67d..65ae2bb6a 100644 --- a/components/Savings/NewSavings/StartEarningButton.tsx +++ b/components/Savings/NewSavings/StartEarningButton.tsx @@ -47,7 +47,7 @@ const StartEarningButton = ({ return ( { @@ -55,7 +55,7 @@ const StartEarningButton = ({ if (index >= 0) { useSavingStore.getState().selectVaultForDeposit(index); } - useDepositStore.getState().setDepositFromSolid(false); + useDepositStore.getState().setDepositFromSolid(true); }} trigger={} /> diff --git a/components/Savings/SavingDepositButton.tsx b/components/Savings/SavingDepositButton.tsx index 357e7cf9d..5743b7aab 100644 --- a/components/Savings/SavingDepositButton.tsx +++ b/components/Savings/SavingDepositButton.tsx @@ -21,11 +21,11 @@ const SavingDepositButton = () => { return ( { - useDepositStore.getState().setDepositFromSolid(false); + useDepositStore.getState().setDepositFromSolid(true); }} trigger={getTrigger()} /> diff --git a/components/Savings/SavingsHeaderButtonsMobile.tsx b/components/Savings/SavingsHeaderButtonsMobile.tsx index c14be307c..a46129dca 100644 --- a/components/Savings/SavingsHeaderButtonsMobile.tsx +++ b/components/Savings/SavingsHeaderButtonsMobile.tsx @@ -50,11 +50,11 @@ const SavingsHeaderButtonsMobile = ({ return ( { - useDepositStore.getState().setDepositFromSolid(false); + useDepositStore.getState().setDepositFromSolid(true); }} trigger={ Date: Wed, 16 Sep 2026 22:25:09 +0200 Subject: [PATCH 2/3] Name the vaults by yield, and give the deposit form a back arrow The Destination dropdown is the only thing that reads `vaultName`, so "USDC / FUSE / ETH Savings" become "USD / FUSE / ETH Yield" to match the design. The deposit form had no back arrow because it had nowhere to go: as the savings entry point, back and close were the same action. Point it at the landing screen for the vault being deposited into instead, so it lands somewhere whether the flow was opened from that vault's own screen or from the home balances dropdown. Co-Authored-By: Claude Opus 5 --- constants/vaults.ts | 6 +++--- hooks/useDepositOption.tsx | 14 ++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/constants/vaults.ts b/constants/vaults.ts index 16190ebc7..a9cba71d2 100644 --- a/constants/vaults.ts +++ b/constants/vaults.ts @@ -39,7 +39,7 @@ export const VAULTS: Vault[] = [ supportedChains: BRIDGE_CHAIN_IDS, supportedTokens: ['USDC', 'USDT'], }, - vaultName: 'USDC Savings', + vaultName: 'USD Yield', }, { name: 'FUSE', @@ -59,7 +59,7 @@ export const VAULTS: Vault[] = [ supportedChains: [fuse.id], supportedTokens: ['WFUSE', 'FUSE'], }, - vaultName: 'FUSE Savings', + vaultName: 'FUSE Yield', }, { name: 'ETH', @@ -83,6 +83,6 @@ export const VAULTS: Vault[] = [ supportedChains: [mainnet.id], supportedTokens: ['WETH', 'ETH'], }, - vaultName: 'ETH Savings', + vaultName: 'ETH Yield', }, ]; diff --git a/hooks/useDepositOption.tsx b/hooks/useDepositOption.tsx index f281df720..f4ab3a21a 100644 --- a/hooks/useDepositOption.tsx +++ b/hooks/useDepositOption.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { ActivityIndicator, Platform, Pressable, PressableProps, View } from 'react-native'; -import { useRouter } from 'expo-router'; +import { Href, useRouter } from 'expo-router'; import { Plus } from 'lucide-react-native'; import { useShallow } from 'zustand/react/shallow'; @@ -42,18 +42,20 @@ import { WirexBankAccountPane } from '@/components/WirexBankAccount/WirexBankAcc import { DEPOSIT_MODAL } from '@/constants/modals'; import { path } from '@/constants/path'; import { TRACKING_EVENTS } from '@/constants/tracking-events'; +import { VAULTS } from '@/constants/vaults'; import { useDirectDepositSession } from '@/hooks/useDirectDepositSession'; import useUser from '@/hooks/useUser'; import { useVirtualAccountProvider } from '@/hooks/useVirtualAccountProvider'; import { track } from '@/lib/analytics'; import getTokenIcon from '@/lib/getTokenIcon'; -import { DepositModal } from '@/lib/types'; +import { DepositModal, VaultType } from '@/lib/types'; import { getAllowedTokensForChain, getDefaultDepositSelection, getVaultDepositConfig, } from '@/lib/vaults'; import { useDepositStore } from '@/store/useDepositStore'; +import { useSavingStore } from '@/store/useSavingStore'; import useResponsiveModal from './useResponsiveModal'; @@ -709,10 +711,14 @@ const useDepositOption = ({ const handleBackPress = () => { if (isFormAndAddress && depositFromSolid) { - // Savings deposit form is the entry point — close the modal + // The savings deposit form is the entry point, so there is no earlier step + // to return to. Back leads to the vault being deposited into instead — + // reached from its own screen or from Home, it lands in the same place. + const vaultType = VAULTS[useSavingStore.getState().selectedVault]?.type ?? VaultType.USDC; setModal(DEPOSIT_MODAL.CLOSE); resetDepositFlow(); clearSessionStartTime(); + router.navigate({ pathname: '/savings', params: { vault: vaultType } } as Href); } else if (isFormAndAddress) { setModal(DEPOSIT_MODAL.OPEN_NETWORKS); } else if (isBankTransferKycFrame) { @@ -877,7 +883,7 @@ const useDepositOption = ({ const shouldOpen = !isClose; const showBackButton = - (isFormAndAddress && !depositFromSolid) || + isFormAndAddress || isBuyCrypto || isNetworks || isOptions || From be5e29131811ad8a5df73ab244e534fd0c53ce20 Mon Sep 17 00:00:00 2001 From: mul53 Date: Wed, 16 Sep 2026 23:01:48 +0200 Subject: [PATCH 3/3] Send the empty deposit state to "Fund your wallet" "Add funds" opened the deposit-methods list; the screen it belongs on says the wallet holds nothing to deposit, so the wallet-funding entry is the step that follows. Clear depositFromSolid on the way out. Left set, the steps behind that screen still resolve to the vault deposit form rather than the add-funds-to-wallet one, and skip the wallet provider an external deposit needs. Co-Authored-By: Claude Opus 5 --- components/DepositToVault/EmptyDepositTokens.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/components/DepositToVault/EmptyDepositTokens.tsx b/components/DepositToVault/EmptyDepositTokens.tsx index 6d6fe8d5e..2f0afd884 100644 --- a/components/DepositToVault/EmptyDepositTokens.tsx +++ b/components/DepositToVault/EmptyDepositTokens.tsx @@ -60,10 +60,21 @@ const EmptyDepositTokens = ({ vault }: EmptyDepositTokensProps) => { const tokenPair = supportedTokens.map(t => t.symbol).join('/'); const tokenList = supportedTokens.map(t => t.symbol).join(' or '); - const setModal = useDepositStore(useShallow(state => state.setModal)); + const { setModal, setDepositFromSolid } = useDepositStore( + useShallow(state => ({ + setModal: state.setModal, + setDepositFromSolid: state.setDepositFromSolid, + })), + ); const handleDepositPress = () => { - setModal(DEPOSIT_MODAL.OPEN_OPTIONS); + // There is nothing in the wallet to move into the vault, so this leaves the + // deposit-from-Solid flow for the one that gets money into the wallet. The + // flag has to go with it: left set, the steps behind "Fund your wallet" + // would still resolve to the vault deposit form, without the wallet + // provider an external deposit needs. + setDepositFromSolid(false); + setModal(DEPOSIT_MODAL.OPEN_DEPOSIT_TYPE); }; return (