Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/(protected)/(tabs)/savings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
return <SavingsScreenNew />;
}

function LegacySavings() {

Check warning on line 51 in app/(protected)/(tabs)/savings.tsx

View workflow job for this annotation

GitHub Actions / lint

'LegacySavings' is defined but never used. Allowed unused vars must match /^_/u
useRenderMonitor({ componentName: MONITORED_COMPONENTS.SAVINGS_SCREEN });

const { vault: vaultParam } = useLocalSearchParams<{ vault?: string }>();
Expand Down Expand Up @@ -171,11 +171,11 @@
/>
<DepositTrigger
buttonText="Deposit"
modal={DEPOSIT_MODAL.OPEN_SAVINGS_FUND}
modal={DEPOSIT_MODAL.OPEN_FORM}
preserveSelectedVault
source="savings_header"
onBeforeOpen={() => {
useDepositStore.getState().setDepositFromSolid(false);
useDepositStore.getState().setDepositFromSolid(true);
}}
/>
</View>
Expand Down
40 changes: 31 additions & 9 deletions components/DepositToVault/DepositToVaultForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -560,18 +575,25 @@ function DepositToVaultForm() {
return (
<Pressable onPress={Platform.OS === 'web' ? undefined : Keyboard.dismiss}>
<View className="gap-1">
<View className="gap-2">
<Text className="text-muted-foreground">
{useSolidForFuse || useSolidForEth || useSolidForUsdc ? '' : 'From wallet'}
</Text>
{!useSolidForFuse && !useSolidForEth && !useSolidForUsdc && <ConnectedWalletDropdown />}
</View>
{!isDepositFromSolid && (
<View className="gap-2">
<Text className="text-muted-foreground">From wallet</Text>
<ConnectedWalletDropdown />
</View>
)}
<View className="gap-1" style={{ zIndex: 50 }}>
<Text className="text-base text-muted-foreground">Destination</Text>
<VaultSelectorDropdown />
</View>
<View className="gap-2">
{!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.
<View className="mt-12 items-center">
<ActivityIndicator size="small" color="white" />
</View>
) : !hasDepositableBalance ? (
<EmptyDepositTokens vault={vault} />
) : (
<>
Expand Down
22 changes: 18 additions & 4 deletions components/DepositToVault/EmptyDepositTokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,26 @@ 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));
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 (
Expand Down Expand Up @@ -88,7 +102,7 @@ const EmptyDepositTokens = ({ vault }: EmptyDepositTokensProps) => {
</View>
<View className="mt-3 items-center gap-1 rounded-2xl px-12">
<Text className="text-center text-lg font-semibold text-white">
You don’t have {tokenList} balance
You don’t have {tokenPair} balance
</Text>
{tokenList && (
<Text className="text-center text-sm text-muted-foreground">
Expand All @@ -98,7 +112,7 @@ const EmptyDepositTokens = ({ vault }: EmptyDepositTokensProps) => {
)}
</View>
<Button variant="brand" className="mt-8 h-12 w-full rounded-xl" onPress={handleDepositPress}>
<Text className="text-base font-bold">Add Funds</Text>
<Text className="text-base font-bold">Add funds</Text>
</Button>
</View>
);
Expand Down
4 changes: 2 additions & 2 deletions components/Home/NewHome/OtherBalancesDropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,12 +288,12 @@ export const SavingsBalanceRow = ({
}) => (
<BalanceRow color={SAVINGS_COLOR} label="Savings" value={savingsBalance} isLoading={isLoading}>
<DepositTrigger
modal={DEPOSIT_MODAL.OPEN_SAVINGS_FUND}
modal={DEPOSIT_MODAL.OPEN_FORM}
preserveSelectedVault
source="home_other_balances"
onBeforeOpen={() => {
onDismiss?.();
useDepositStore.getState().setDepositFromSolid(false);
useDepositStore.getState().setDepositFromSolid(true);
}}
trigger={<AddButton />}
/>
Expand Down
4 changes: 2 additions & 2 deletions components/Savings/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ export default function SavingsEmptyState() {
<DashboardHeaderButtons hideSend hideDeposit />
<DepositTrigger
buttonText="Deposit"
modal={DEPOSIT_MODAL.OPEN_SAVINGS_FUND}
modal={DEPOSIT_MODAL.OPEN_FORM}
preserveSelectedVault
source="savings_empty_state_header"
onBeforeOpen={() => {
useDepositStore.getState().setDepositFromSolid(false);
useDepositStore.getState().setDepositFromSolid(true);
}}
/>
</View>
Expand Down
4 changes: 2 additions & 2 deletions components/Savings/NewSavings/SavingsFundedActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@ const SavingsFundedActions = ({ vaultType }: SavingsFundedActionsProps) => {
return (
<View className="flex-row items-center gap-3 px-4">
<DepositTrigger
modal={DEPOSIT_MODAL.OPEN_SAVINGS_FUND}
modal={DEPOSIT_MODAL.OPEN_FORM}
preserveSelectedVault
source="savings_add_funds"
onBeforeOpen={() => {
const index = VAULTS.findIndex(vault => vault.type === vaultType);
if (index >= 0) {
useSavingStore.getState().selectVaultForDeposit(index);
}
useDepositStore.getState().setDepositFromSolid(false);
useDepositStore.getState().setDepositFromSolid(true);
}}
trigger={<DepositTriggerButton />}
/>
Expand Down
4 changes: 2 additions & 2 deletions components/Savings/NewSavings/StartEarningButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ const StartEarningButton = ({
return (
<View className={cn('px-4', className)}>
<DepositTrigger
modal={DEPOSIT_MODAL.OPEN_SAVINGS_FUND}
modal={DEPOSIT_MODAL.OPEN_FORM}
preserveSelectedVault
source={source}
onBeforeOpen={() => {
const index = VAULTS.findIndex(vault => vault.type === vaultType);
if (index >= 0) {
useSavingStore.getState().selectVaultForDeposit(index);
}
useDepositStore.getState().setDepositFromSolid(false);
useDepositStore.getState().setDepositFromSolid(true);
}}
trigger={<DepositTriggerButton label={label} />}
/>
Expand Down
4 changes: 2 additions & 2 deletions components/Savings/SavingDepositButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ const SavingDepositButton = () => {

return (
<DepositTrigger
modal={DEPOSIT_MODAL.OPEN_SAVINGS_FUND}
modal={DEPOSIT_MODAL.OPEN_FORM}
preserveSelectedVault
source="savings_empty_state"
onBeforeOpen={() => {
useDepositStore.getState().setDepositFromSolid(false);
useDepositStore.getState().setDepositFromSolid(true);
}}
trigger={getTrigger()}
/>
Expand Down
4 changes: 2 additions & 2 deletions components/Savings/SavingsHeaderButtonsMobile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ const SavingsHeaderButtonsMobile = ({
return (
<View className="flex-row items-center justify-center gap-8">
<DepositTrigger
modal={DEPOSIT_MODAL.OPEN_SAVINGS_FUND}
modal={DEPOSIT_MODAL.OPEN_FORM}
preserveSelectedVault
source="savings_mobile"
onBeforeOpen={() => {
useDepositStore.getState().setDepositFromSolid(false);
useDepositStore.getState().setDepositFromSolid(true);
}}
trigger={
<CircleButton
Expand Down
6 changes: 3 additions & 3 deletions constants/vaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const VAULTS: Vault[] = [
supportedChains: BRIDGE_CHAIN_IDS,
supportedTokens: ['USDC', 'USDT'],
},
vaultName: 'USDC Savings',
vaultName: 'USD Yield',
},
{
name: 'FUSE',
Expand All @@ -59,7 +59,7 @@ export const VAULTS: Vault[] = [
supportedChains: [fuse.id],
supportedTokens: ['WFUSE', 'FUSE'],
},
vaultName: 'FUSE Savings',
vaultName: 'FUSE Yield',
},
{
name: 'ETH',
Expand All @@ -83,6 +83,6 @@ export const VAULTS: Vault[] = [
supportedChains: [mainnet.id],
supportedTokens: ['WETH', 'ETH'],
},
vaultName: 'ETH Savings',
vaultName: 'ETH Yield',
},
];
18 changes: 13 additions & 5 deletions hooks/useDepositOption.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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';

Expand Down Expand Up @@ -669,8 +671,10 @@ const useDepositOption = ({
if (user && !user.email) {
setModal(DEPOSIT_MODAL.OPEN_EMAIL_GATE);
} else if (modal.name === DEPOSIT_MODAL.OPEN_SAVINGS_FUND.name) {
// Explicit savings entry point: show the token list, never a form that a
// The card's minimum-deposit step, which still funds savings by sending
// new money in: hold it on the token list rather than the form a
// connected wallet or a stale chain selection would otherwise jump to.
// The savings screens themselves now open OPEN_FORM directly.
setModal(DEPOSIT_MODAL.OPEN_SAVINGS_FUND);
} else if (depositFromSolid && user?.safeAddress) {
// Savings deposit: open form directly — token selector is inline
Expand Down Expand Up @@ -707,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) {
Expand Down Expand Up @@ -875,7 +883,7 @@ const useDepositOption = ({
const shouldOpen = !isClose;

const showBackButton =
(isFormAndAddress && !depositFromSolid) ||
isFormAndAddress ||
isBuyCrypto ||
isNetworks ||
isOptions ||
Expand Down
Loading