mirror of
https://github.com/trezor/trezor-suite.git
synced 2026-03-02 21:45:14 +01:00
feat(suite): display an info alert if SOL staking rewards not available yet
This commit is contained in:
@@ -82,5 +82,7 @@ export const useSolanaRewards = (account: Account) => {
|
||||
showPagination,
|
||||
isLastPage,
|
||||
selectedAccountRewards,
|
||||
};
|
||||
} as const;
|
||||
};
|
||||
|
||||
export type SolanaRewards = ReturnType<typeof useSolanaRewards>;
|
||||
|
||||
@@ -7907,6 +7907,11 @@ export default defineMessages({
|
||||
id: 'TR_DISCOVERY_WARNING_DESCRIPTION',
|
||||
defaultMessage: 'Just a moment.',
|
||||
},
|
||||
TR_SOL_STAKING_REWARD_WARNING: {
|
||||
id: 'TR_SOL_STAKING_REWARD_WARNING',
|
||||
defaultMessage:
|
||||
'Your recent rewards are securely on the blockchain and may take more time to appear in Trezor Suite.',
|
||||
},
|
||||
TR_STAKING_REWARDS_TITLE: {
|
||||
id: 'TR_STAKING_REWARDS_TITLE',
|
||||
defaultMessage: 'Cardano staking is active',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { SOLANA_EPOCH_DAYS } from '@suite-common/wallet-constants';
|
||||
import { selectAccountIsStakingActive } from '@suite-common/wallet-core';
|
||||
import { formatNetworkAmount, isTestnet } from '@suite-common/wallet-utils';
|
||||
import { Badge, Card, Column, Icon, Row, SkeletonStack, Text, Tooltip } from '@trezor/components';
|
||||
import { spacings } from '@trezor/theme';
|
||||
@@ -15,8 +14,7 @@ import {
|
||||
} from 'src/components/suite';
|
||||
import { Translation } from 'src/components/suite/Translation';
|
||||
import { Pagination } from 'src/components/wallet';
|
||||
import { useSelector } from 'src/hooks/suite';
|
||||
import { useSolanaRewards } from 'src/hooks/wallet/useSolanaRewards';
|
||||
import { type SolanaRewards } from 'src/hooks/wallet/useSolanaRewards';
|
||||
import { Account } from 'src/types/wallet';
|
||||
import SkeletonTransactionItem from 'src/views/wallet/transactions/TransactionList/SkeletonTransactionItem';
|
||||
import { ColDate } from 'src/views/wallet/transactions/TransactionList/TransactionsGroup/CommonComponents';
|
||||
@@ -25,37 +23,22 @@ import { RewardsEmpty } from './RewardsEmpty';
|
||||
|
||||
interface RewardsListProps {
|
||||
account: Account;
|
||||
rewards: SolanaRewards;
|
||||
}
|
||||
|
||||
export const RewardsList = ({ account }: RewardsListProps) => {
|
||||
export const RewardsList = ({ account, rewards }: RewardsListProps) => {
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const {
|
||||
slicedRewards,
|
||||
isLoading,
|
||||
currentPage,
|
||||
setSelectedPage,
|
||||
totalItems,
|
||||
itemsPerPage,
|
||||
showPagination,
|
||||
isLastPage,
|
||||
selectedAccountRewards,
|
||||
} = useSolanaRewards(account);
|
||||
|
||||
const isSolanaMainnet = !isTestnet(account.symbol);
|
||||
|
||||
const onPageSelected = (page: number) => {
|
||||
setSelectedPage(page);
|
||||
rewards.setSelectedPage(page);
|
||||
if (sectionRef.current) {
|
||||
sectionRef.current.scrollIntoView();
|
||||
}
|
||||
};
|
||||
|
||||
const isStakingActive = useSelector(state => selectAccountIsStakingActive(state, account.key));
|
||||
if (!isStakingActive) return null;
|
||||
|
||||
const noRewards = !isSolanaMainnet || selectedAccountRewards?.length === 0;
|
||||
if (noRewards && !isLoading) {
|
||||
const noRewards = !isSolanaMainnet || rewards.selectedAccountRewards?.length === 0;
|
||||
if (noRewards && !rewards.isLoading) {
|
||||
return <RewardsEmpty />;
|
||||
}
|
||||
|
||||
@@ -65,7 +48,7 @@ export const RewardsList = ({ account }: RewardsListProps) => {
|
||||
heading={<Translation id="TR_REWARDS" />}
|
||||
data-testid="@wallet/accounts/rewards-list"
|
||||
>
|
||||
{isLoading || selectedAccountRewards === undefined ? (
|
||||
{rewards.isLoading || rewards.selectedAccountRewards === undefined ? (
|
||||
<SkeletonStack $col $childMargin="0px 0px 16px 0px">
|
||||
<SkeletonTransactionItem />
|
||||
<SkeletonTransactionItem />
|
||||
@@ -73,7 +56,7 @@ export const RewardsList = ({ account }: RewardsListProps) => {
|
||||
</SkeletonStack>
|
||||
) : (
|
||||
<>
|
||||
{slicedRewards?.map(reward => (
|
||||
{rewards.slicedRewards?.map(reward => (
|
||||
<React.Fragment key={reward.epoch}>
|
||||
<Row>
|
||||
<ColDate>
|
||||
@@ -148,13 +131,13 @@ export const RewardsList = ({ account }: RewardsListProps) => {
|
||||
</>
|
||||
)}
|
||||
|
||||
{showPagination && !isLoading && slicedRewards?.length && (
|
||||
{rewards.showPagination && !rewards.isLoading && rewards.slicedRewards?.length && (
|
||||
<Pagination
|
||||
hasPages={true}
|
||||
currentPage={currentPage}
|
||||
isLastPage={isLastPage}
|
||||
perPage={itemsPerPage}
|
||||
totalItems={totalItems}
|
||||
currentPage={rewards.currentPage}
|
||||
isLastPage={rewards.isLastPage}
|
||||
perPage={rewards.itemsPerPage}
|
||||
totalItems={rewards.totalItems}
|
||||
onPageSelected={onPageSelected}
|
||||
explicitNavigation
|
||||
/>
|
||||
@@ -14,16 +14,19 @@ import { spacings } from '@trezor/theme';
|
||||
import { DashboardSection } from 'src/components/dashboard';
|
||||
import { Translation } from 'src/components/suite/Translation';
|
||||
import { useDevice, useLayoutSize, useSelector } from 'src/hooks/suite';
|
||||
import { useSolanaRewards } from 'src/hooks/wallet/useSolanaRewards';
|
||||
import { ConnectDeviceGenericPromo } from 'src/views/wallet/receive/components/ConnectDevicePromo';
|
||||
|
||||
import { StakingDashboard } from '../StakingDashboard/StakingDashboard';
|
||||
import { RewardsList } from './Rewards/RewardsList';
|
||||
import { StakingRewardsWarning } from './StakingRewardsWarning';
|
||||
import { useRewardsNotAvailableYet } from './hooks/useRewardsNotAvailableYet';
|
||||
import { ApyCard } from '../StakingDashboard/components/ApyCard';
|
||||
import { ClaimCard } from '../StakingDashboard/components/ClaimCard';
|
||||
import { DiscoveryWarning } from '../StakingDashboard/components/DiscoveryWarning';
|
||||
import { EmptyStakingCard } from '../StakingDashboard/components/EmptyStakingCard';
|
||||
import { PayoutCard } from '../StakingDashboard/components/PayoutCard';
|
||||
import { StakingCard } from '../StakingDashboard/components/StakingCard';
|
||||
import { RewardsList } from './components/Rewards/RewardsList';
|
||||
|
||||
interface SolStakingDashboardProps {
|
||||
selectedAccount: SelectedAccountLoaded;
|
||||
@@ -45,49 +48,61 @@ export const SolStakingDashboard = ({ selectedAccount }: SolStakingDashboardProp
|
||||
|
||||
const isStakingActive = useSelector(state => selectAccountIsStakingActive(state, account.key));
|
||||
|
||||
const rewards = useSolanaRewards(account);
|
||||
const rewardsNotAvailableYet = useRewardsNotAvailableYet(
|
||||
account,
|
||||
rewards.selectedAccountRewards?.[0],
|
||||
);
|
||||
|
||||
return (
|
||||
<StakingDashboard
|
||||
selectedAccount={selectedAccount}
|
||||
dashboard={
|
||||
<Column alignItems="normal" gap={spacings.xxxxl}>
|
||||
{isStakingActive ? (
|
||||
<DashboardSection
|
||||
heading={
|
||||
<Translation
|
||||
id="TR_STAKE_STAKE_TOKEN"
|
||||
values={{ symbol: getNetworkDisplaySymbol(account.symbol) }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Column alignItems="normal" gap={spacings.sm}>
|
||||
{!isDeviceConnected && <ConnectDeviceGenericPromo />}
|
||||
{isDiscoveryRunning && <DiscoveryWarning />}
|
||||
<>
|
||||
<DashboardSection
|
||||
heading={
|
||||
<Translation
|
||||
id="TR_STAKE_STAKE_TOKEN"
|
||||
values={{ symbol: getNetworkDisplaySymbol(account.symbol) }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Column alignItems="normal" gap={spacings.sm}>
|
||||
{!isDeviceConnected && <ConnectDeviceGenericPromo />}
|
||||
{isDiscoveryRunning && <DiscoveryWarning />}
|
||||
{rewardsNotAvailableYet && <StakingRewardsWarning />}
|
||||
|
||||
<Grid
|
||||
columns={isBelowLaptop || !canClaim ? 1 : 2}
|
||||
gap={spacings.sm}
|
||||
>
|
||||
<ClaimCard />
|
||||
<Flex direction={canClaim ? 'column' : 'row'} gap={spacings.sm}>
|
||||
<ApyCard apy={apy} />
|
||||
<PayoutCard
|
||||
nextRewardPayout={SOLANA_EPOCH_DAYS}
|
||||
daysToAddToPool={SOLANA_EPOCH_DAYS}
|
||||
validatorWithdrawTime={0}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid>
|
||||
<StakingCard
|
||||
isValidatorsQueueLoading={undefined}
|
||||
daysToAddToPool={SOLANA_EPOCH_DAYS}
|
||||
daysToUnstake={SOLANA_EPOCH_DAYS}
|
||||
/>
|
||||
</Column>
|
||||
</DashboardSection>
|
||||
<Grid
|
||||
columns={isBelowLaptop || !canClaim ? 1 : 2}
|
||||
gap={spacings.sm}
|
||||
>
|
||||
<ClaimCard />
|
||||
<Flex
|
||||
direction={canClaim ? 'column' : 'row'}
|
||||
gap={spacings.sm}
|
||||
>
|
||||
<ApyCard apy={apy} />
|
||||
<PayoutCard
|
||||
nextRewardPayout={SOLANA_EPOCH_DAYS}
|
||||
daysToAddToPool={SOLANA_EPOCH_DAYS}
|
||||
validatorWithdrawTime={0}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid>
|
||||
<StakingCard
|
||||
isValidatorsQueueLoading={undefined}
|
||||
daysToAddToPool={SOLANA_EPOCH_DAYS}
|
||||
daysToUnstake={SOLANA_EPOCH_DAYS}
|
||||
/>
|
||||
</Column>
|
||||
</DashboardSection>
|
||||
<RewardsList account={account} rewards={rewards} />
|
||||
</>
|
||||
) : (
|
||||
<EmptyStakingCard />
|
||||
)}
|
||||
<RewardsList account={account} />
|
||||
</Column>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Banner } from '@trezor/components';
|
||||
|
||||
import { Translation } from 'src/components/suite/Translation';
|
||||
|
||||
export const StakingRewardsWarning = () => (
|
||||
<Banner variant="warning" icon="warning" iconSize="medium">
|
||||
<Translation id="TR_SOL_STAKING_REWARD_WARNING" />
|
||||
</Banner>
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { StakeAccountRewards } from '@suite-common/wallet-core';
|
||||
import { isInt } from '@trezor/utils';
|
||||
|
||||
import { Account } from 'src/types/wallet';
|
||||
|
||||
export function useRewardsNotAvailableYet(
|
||||
account: Account,
|
||||
latestReward?: StakeAccountRewards,
|
||||
): boolean {
|
||||
if (account.networkType !== 'solana') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentEpoch = account.misc?.solEpoch ?? null;
|
||||
const latestRewardEpoch = latestReward?.epoch ?? null;
|
||||
|
||||
if (!isInt(currentEpoch) || !isInt(latestRewardEpoch)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currentEpoch - 1 !== latestRewardEpoch;
|
||||
}
|
||||
@@ -62,3 +62,4 @@ export * from './removeTrailingSlashes';
|
||||
export * from './getIntegerInRangeFromString';
|
||||
export * from './safeBigIntStringify';
|
||||
export * from './union';
|
||||
export * from './isInt';
|
||||
|
||||
6
packages/utils/src/isInt.ts
Normal file
6
packages/utils/src/isInt.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* `Number.isInteger` doesn't work as TS guard, therefore we need this helper
|
||||
*/
|
||||
export function isInt(value: number | null): value is number {
|
||||
return Number.isInteger(value);
|
||||
}
|
||||
@@ -32,9 +32,7 @@ import {
|
||||
export const secondsToDays = (seconds: number) => Math.round(seconds / 60 / 60 / 24);
|
||||
|
||||
export const getAccountTotalStakingBalance = (account: Account) => {
|
||||
if (!account) return null;
|
||||
|
||||
switch (account.networkType) {
|
||||
switch (account?.networkType) {
|
||||
case 'ethereum':
|
||||
return getEthAccountTotalStakingBalance(account);
|
||||
case 'solana':
|
||||
|
||||
Reference in New Issue
Block a user