From d57a76be334a01ee98b0e64b5637ed56b0ad7958 Mon Sep 17 00:00:00 2001 From: Pavlo Syrotyna Date: Thu, 23 Jan 2025 13:11:43 +0200 Subject: [PATCH] feat(suite): add empty state and refetch logic for Solana staking rewards --- packages/suite/src/support/messages.ts | 13 +++ .../SolStakingDashboard.tsx | 2 +- .../components/Rewards/RewardsEmpty.tsx | 18 ++++ .../components/{ => Rewards}/RewardsList.tsx | 85 ++++++++++++++----- .../wallet-core/src/stake/stakeReducer.ts | 6 +- .../wallet-core/src/stake/stakeThunks.ts | 8 +- .../wallet-core/src/stake/stakeTypes.ts | 4 + 7 files changed, 108 insertions(+), 28 deletions(-) create mode 100644 packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/Rewards/RewardsEmpty.tsx rename packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/{ => Rewards}/RewardsList.tsx (78%) diff --git a/packages/suite/src/support/messages.ts b/packages/suite/src/support/messages.ts index 7d6d1326709..66ecad0c9ca 100644 --- a/packages/suite/src/support/messages.ts +++ b/packages/suite/src/support/messages.ts @@ -8568,6 +8568,19 @@ export default defineMessages({ defaultMessage: 'An epoch in Solana is approximately {count, plural, one {# day} other {# days}} long.', }, + TR_STAKE_REFRESH_REWARDS_TOOLTIP: { + id: 'TR_STAKE_REFRESH_REWARDS_TOOLTIP', + defaultMessage: 'Refresh your rewards for this account.', + }, + TR_STAKE_REWARDS_ARE_EMPTY: { + id: 'TR_STAKE_REWARDS_ARE_EMPTY', + defaultMessage: 'No Rewards', + }, + TR_STAKE_WAIT_TO_CHECK_REWARDS: { + id: 'TR_STAKE_WAIT_TO_CHECK_REWARDS', + defaultMessage: + 'Wait up to {count, plural, one {# day} other {# days}} to check your rewards', + }, TR_STAKE_ETH_CARD_TITLE: { id: 'TR_STAKE_ETH_CARD_TITLE', defaultMessage: 'The easiest way to earn {symbol}', diff --git a/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx index aee2796fcef..927dab19cb1 100644 --- a/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx +++ b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/SolStakingDashboard.tsx @@ -16,7 +16,7 @@ import { ApyCard } from '../StakingDashboard/components/ApyCard'; import { PayoutCard } from '../StakingDashboard/components/PayoutCard'; import { ClaimCard } from '../StakingDashboard/components/ClaimCard'; import { StakingCard } from '../StakingDashboard/components/StakingCard'; -import { RewardsList } from './components/RewardsList'; +import { RewardsList } from './components/Rewards/RewardsList'; interface SolStakingDashboardProps { selectedAccount: SelectedAccountLoaded; diff --git a/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/Rewards/RewardsEmpty.tsx b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/Rewards/RewardsEmpty.tsx new file mode 100644 index 00000000000..3956508e584 --- /dev/null +++ b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/Rewards/RewardsEmpty.tsx @@ -0,0 +1,18 @@ +import { SOLANA_EPOCH_DAYS } from '@suite-common/wallet-constants'; + +import { Translation } from 'src/components/suite'; +import { AccountExceptionLayout } from 'src/components/wallet'; + +export const RewardsEmpty = () => ( + } + description={ + + } + iconName="arrowLineDown" + iconVariant="tertiary" + /> +); diff --git a/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/RewardsList.tsx b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/Rewards/RewardsList.tsx similarity index 78% rename from packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/RewardsList.tsx rename to packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/Rewards/RewardsList.tsx index 0112dae8154..5e1bc788e9b 100644 --- a/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/RewardsList.tsx +++ b/packages/suite/src/views/wallet/staking/components/SolStakingDashboard/components/Rewards/RewardsList.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { EverstakeRewardsEndpointType, @@ -7,13 +7,24 @@ import { StakeAccountRewards, } from '@suite-common/wallet-core'; import { formatNetworkAmount } from '@suite-common/wallet-utils'; -import { Badge, Card, Column, Icon, Row, SkeletonStack, Text, Tooltip } from '@trezor/components'; +import { + Badge, + Card, + Column, + Icon, + IconButton, + Row, + SkeletonStack, + Text, + Tooltip, +} from '@trezor/components'; import { spacings } from '@trezor/theme'; import { SOLANA_EPOCH_DAYS } from '@suite-common/wallet-constants'; +import { useDebounce } from '@trezor/react-utils'; import { - CoinBalance, FiatValue, + FormattedCryptoAmount, FormattedDate, HiddenPlaceholder, Translation, @@ -25,6 +36,8 @@ import { Pagination } from 'src/components/wallet'; import SkeletonTransactionItem from 'src/views/wallet/transactions/TransactionList/SkeletonTransactionItem'; import { ColDate } from 'src/views/wallet/transactions/TransactionList/TransactionsGroup/CommonComponents'; +import { RewardsEmpty } from './RewardsEmpty'; + const PAGE_SIZE_DEFAULT = 10; interface RewardsListProps { @@ -32,14 +45,16 @@ interface RewardsListProps { } export const RewardsList = ({ account }: RewardsListProps) => { - const anchor = useSelector(state => state.router.anchor); const { data, isLoading } = - useSelector(state => selectStakingRewards(state, account?.symbol)) || {}; + useSelector(state => selectStakingRewards(state, account.symbol)) || {}; const { rewards } = data ?? {}; + const selectedAccountRewards = rewards?.[account.descriptor]; + const sectionRef = useRef(null); const dispatch = useDispatch(); + const debounce = useDebounce(); const perPage = PAGE_SIZE_DEFAULT; const startPage = 1; @@ -50,32 +65,41 @@ export const RewardsList = ({ account }: RewardsListProps) => { const startIndex = (currentPage - 1) * perPage; const stopIndex = startIndex + perPage; + const isSolanaMainnet = account.symbol === 'sol'; + + const fetchRewards = useCallback( + async ({ symbol, descriptor }: Account) => { + await debounce(() => { + if (symbol !== 'sol') return; + dispatch( + fetchEverstakeRewards({ + symbol, + endpointType: EverstakeRewardsEndpointType.GetRewards, + address: descriptor, + }), + ); + }); + }, + [dispatch, debounce], + ); + useEffect(() => { - // Fetch rewards only for the Solana mainnet - if (account.symbol === 'sol') { - dispatch( - fetchEverstakeRewards({ - symbol: account.symbol, - endpointType: EverstakeRewardsEndpointType.GetRewards, - address: account.descriptor, - }), - ); - } - }, [anchor, account, dispatch]); + fetchRewards(account); + }, [account, fetchRewards]); useEffect(() => { - if (rewards) { - const slicedRewards = rewards?.slice(startIndex, stopIndex); + if (selectedAccountRewards) { + const slicedRewards = selectedAccountRewards?.slice(startIndex, stopIndex); setSlicedRewards(slicedRewards); } - }, [currentPage, rewards, startIndex, stopIndex]); + }, [currentPage, selectedAccountRewards, startIndex, stopIndex]); useEffect(() => { // reset page on account change setSelectedPage(startPage); }, [account.descriptor, account.symbol, startPage]); - const totalItems = rewards?.length ?? 0; + const totalItems = selectedAccountRewards?.length ?? 0; const showPagination = totalItems > perPage; const isLastPage = stopIndex >= totalItems; @@ -86,10 +110,29 @@ export const RewardsList = ({ account }: RewardsListProps) => { } }; + if (!isSolanaMainnet || !totalItems) { + return ; + } + return ( } + actions={ + + } + > + fetchRewards(account)} + /> + + + } data-testid="@wallet/accounts/rewards-list" > {isLoading ? ( @@ -147,7 +190,7 @@ export const RewardsList = ({ account }: RewardsListProps) => { {reward?.amount && ( - { const { symbol } = action.meta.arg; - if (!state.data[symbol]) { + if (!state.data[symbol]?.stakingRewards) { state.data[symbol] = { stakingRewards: { error: false, diff --git a/suite-common/wallet-core/src/stake/stakeThunks.ts b/suite-common/wallet-core/src/stake/stakeThunks.ts index 3c6b34d5045..6ff9a4778a9 100644 --- a/suite-common/wallet-core/src/stake/stakeThunks.ts +++ b/suite-common/wallet-core/src/stake/stakeThunks.ts @@ -19,7 +19,7 @@ import { EverstakeEndpointType, EverstakeRewardsEndpointType, ValidatorsQueue, - StakeAccountRewards, + StakeRewardsByAccount, } from './stakeTypes'; import { EVERSTAKE_ENDPOINT_PREFIX, EVERSTAKE_REWARDS_SOLANA_ENPOINT } from './stakeConstants'; import { selectAllNetworkSymbolsOfVisibleAccounts } from '../accounts/accountsReducer'; @@ -108,7 +108,7 @@ export const fetchEverstakeAssetData = createThunk< ); export const fetchEverstakeRewards = createThunk< - { rewards: StakeAccountRewards[] }, + { rewards: StakeRewardsByAccount }, { symbol: SupportedSolanaNetworkSymbols; endpointType: EverstakeRewardsEndpointType; @@ -132,7 +132,9 @@ export const fetchEverstakeRewards = createThunk< const data = await response.json(); return fulfillWithValue({ - rewards: data, + rewards: { + [address]: data, + }, }); } catch (error) { return rejectWithValue(error.toString()); diff --git a/suite-common/wallet-core/src/stake/stakeTypes.ts b/suite-common/wallet-core/src/stake/stakeTypes.ts index 275f78da9c5..7099869d0df 100644 --- a/suite-common/wallet-core/src/stake/stakeTypes.ts +++ b/suite-common/wallet-core/src/stake/stakeTypes.ts @@ -105,3 +105,7 @@ export type StakeAccountRewards = { currency: string; time: string; }; + +export type StakeRewardsByAccount = { + [address: string]: StakeAccountRewards[]; +};