diff --git a/packages/yoroi-extension/app/Routes.js b/packages/yoroi-extension/app/Routes.js index 1151cce577e..3f85c622596 100644 --- a/packages/yoroi-extension/app/Routes.js +++ b/packages/yoroi-extension/app/Routes.js @@ -204,9 +204,7 @@ export const YoroiRoutes = (stores: StoresMap): Node => { }> } /> - }> - } /> - + }> } /> } /> @@ -256,6 +254,9 @@ export const YoroiRoutes = (stores: StoresMap): Node => { } /> } /> + }> + } /> + }> } /> } /> diff --git a/packages/yoroi-extension/app/UI/features/staking/StakingRoot.tsx b/packages/yoroi-extension/app/UI/features/staking/StakingRoot.tsx index e1ed080e873..096eb99397a 100644 --- a/packages/yoroi-extension/app/UI/features/staking/StakingRoot.tsx +++ b/packages/yoroi-extension/app/UI/features/staking/StakingRoot.tsx @@ -1,12 +1,49 @@ -import { Stack } from '@mui/material'; +import { Box, Stack, styled } from '@mui/material'; import { PoolList } from './useCases/PoolList/PoolList'; import { RewardsSummaryCard } from './useCases/RewardsSummary/RewardsSummaryCard'; +import { StakePoolDelegated } from './useCases/DelegatedStakePoolInfo/StakePoolDelegated'; +import EpochProgress from './useCases/EpochProgress/EpochProgress'; +import { LegacyDialogs } from './useCases/LegacyDialogs/LegacyDialogs'; +import { useStaking } from './module/StakingContextProvider'; +import BuySellDialog from '../../../components/buySell/BuySellDialog'; +import WalletEmptyBanner from './common/components/EmptyWalletBanner'; export const StakingRoot = () => { + const { isWalletWithNoFunds, selectedWallet, legacyUIDialogs, currentlyDelegating } = useStaking(); + return ( - + {isWalletWithNoFunds ? ( + legacyUIDialogs.open({ dialog: BuySellDialog })} + isTestnet={selectedWallet.isTestnet} + /> + ) : null} + {currentlyDelegating && ( + + + + + + + + )} + ); }; + +const WrapperCards = styled(Box)({ + display: 'flex', + gap: '24px', + justifyContent: 'space-between', + marginBottom: '24px', +}); + +const RightCardsWrapper = styled(Box)({ + display: 'flex', + width: '100%', + flexDirection: 'column', + gap: '24px', +}); diff --git a/packages/yoroi-extension/app/UI/features/staking/common/components/EmptyWalletBanner.tsx b/packages/yoroi-extension/app/UI/features/staking/common/components/EmptyWalletBanner.tsx new file mode 100644 index 00000000000..ab07a74f773 --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/common/components/EmptyWalletBanner.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Box } from '@mui/system'; +import { Button, Stack, Typography } from '@mui/material'; +import { ReactComponent as CoverBg } from './wallet-empty-banner.inline.svg'; +import { captureEvent } from '../../../../../../posthog'; +import { TESTNET_FAUCET } from '../constants'; +import { useStrings } from '../hooks/useStrings'; + +export type WalletEmptyBannerProps = { + onBuySellClick: () => void; + isTestnet: boolean; +}; + +const WalletEmptyBanner: React.FC = ({ isTestnet, onBuySellClick }) => { + const strings = useStrings(); + + const handleClick = () => { + if (isTestnet) { + window.open(TESTNET_FAUCET, '_blank'); + } else { + onBuySellClick(); + } + + captureEvent('Wallet Page Exchange Clicked'); + }; + + return ( + + theme.palette.ds.bg_gradient_1, + marginBottom: '40px', + borderRadius: '8px', + overflowY: 'hidden', + position: 'relative', + padding: '16px', + height: 'auto', + }} + id="wallet|staking-emptyWalletBanner-box" + > + + + + + + + {isTestnet ? strings.welcomeMessageTestnet : strings.welcomeMessage} + + + + {isTestnet ? strings.welcomeMessageSubtitleTestnet : strings.welcomeMessageSubtitle} + {isTestnet ? ( + <> +
+ {strings.welcomeMessageSubtitleTestnetExtra} + + ) : null} +
+
+ + + + +
+
+ ); +}; + +export default WalletEmptyBanner; diff --git a/packages/yoroi-extension/app/UI/features/staking/common/components/wallet-empty-banner.inline.svg b/packages/yoroi-extension/app/UI/features/staking/common/components/wallet-empty-banner.inline.svg new file mode 100644 index 00000000000..1aae9ef385c --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/common/components/wallet-empty-banner.inline.svg @@ -0,0 +1,508 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/yoroi-extension/app/UI/features/staking/common/constants.ts b/packages/yoroi-extension/app/UI/features/staking/common/constants.ts new file mode 100644 index 00000000000..23009500948 --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/common/constants.ts @@ -0,0 +1 @@ +export const TESTNET_FAUCET = 'https://docs.cardano.org/cardano-testnets/tools/faucet'; diff --git a/packages/yoroi-extension/app/UI/features/staking/common/hooks/useStrings.ts b/packages/yoroi-extension/app/UI/features/staking/common/hooks/useStrings.ts index 4b7c6bbeb8a..35173e7f8ff 100644 --- a/packages/yoroi-extension/app/UI/features/staking/common/hooks/useStrings.ts +++ b/packages/yoroi-extension/app/UI/features/staking/common/hooks/useStrings.ts @@ -48,6 +48,74 @@ export const messages = Object.freeze( id: 'global.labels.error', defaultMessage: '!!!Error', }, + stakePoolDelegated: { + id: 'wallet.dashboard.upcomingRewards.stakePoolDelegated', + defaultMessage: '!!!Stake Pool Delegated', + }, + roa30dLabel: { + id: 'wallet.staking.banner.roa30d', + defaultMessage: '!!!ROA 30d', + }, + poolSizeLabel: { + id: 'wallet.staking.pool.size', + defaultMessage: '!!!Pool size', + }, + poolSaturation: { + id: 'wallet.staking.pool.saturation', + defaultMessage: '!!!Saturation', + }, + updatePoolLabel: { + id: 'global.updatePool', + defaultMessage: '!!! UPDATE POOL', + }, + undelegatePool: { + id: 'transaction.review.undelegatePool', + defaultMessage: '!!!Unstake entire wallet balance from', + }, + undelegateLabel: { + id: 'global.labael.undelegate', + defaultMessage: '!!!Undelegate', + }, + deregisteringStakingKey: { + id: 'transaction.review.deregisteringStakingKey', + defaultMessage: '!!!Undelegating from the pool', + }, + epochProgress: { + id: 'wallet.staking.epochProgress', + defaultMessage: '!!!Epoch Progress', + }, + welcomeMessage: { + id: 'wallet.emptyWalletMessage', + defaultMessage: '!!!Your wallet is empty', + }, + welcomeMessageSubtitle: { + id: 'wallet.emptyWalletMessageSubtitle', + defaultMessage: '!!!Top up your wallet safely using our trusted partners', + }, + welcomeMessageTestnet: { + id: 'wallet.emptyWalletMessage.testnet', + defaultMessage: '!!!Learn Cardano with test ADA ⭐', + }, + welcomeMessageSubtitleTestnet: { + id: 'wallet.emptyWalletMessageSubtitle.testnet', + defaultMessage: '!!!Stake your test ADA by participating in our testnet staking program.', + }, + welcomeMessageSubtitleTestnetExtra: { + id: 'wallet.emptyWalletMessageSubtitle.testnetExtra', + defaultMessage: "!!!Get your TADA. It's your key to testing a new world of possibilities.", + }, + goToFaucetButton: { + id: 'wallet.emptyWalletMessage.goToFaucet', + defaultMessage: '!!!ADD TEST ADA', + }, + buyAda: { + id: 'button.buyAda', + defaultMessage: '!!!Buy ADA', + }, + stakePoolLabel: { + id: 'wallet.delegation.transaction.stakePoolLabel', + defaultMessage: '!!!Stake pool', + }, }) ); @@ -65,5 +133,22 @@ export const useStrings = () => { rewardValue: intl.formatMessage(messages.rewardValue), rewardsLabel: intl.formatMessage(messages.rewardsLabel), errorLabel: intl.formatMessage(messages.errorLabel), + stakePoolDelegated: intl.formatMessage(messages.stakePoolDelegated), + roa30dLabel: intl.formatMessage(messages.roa30dLabel), + poolSizeLabel: intl.formatMessage(messages.poolSizeLabel), + poolSaturation: intl.formatMessage(messages.poolSaturation), + updatePoolLabel: intl.formatMessage(messages.updatePoolLabel), + undelegatePool: intl.formatMessage(messages.undelegatePool), + undelegateLabel: intl.formatMessage(messages.undelegateLabel), + deregisteringStakingKey: intl.formatMessage(messages.deregisteringStakingKey), + epochProgress: intl.formatMessage(messages.epochProgress), + welcomeMessage: intl.formatMessage(messages.welcomeMessage), + welcomeMessageSubtitle: intl.formatMessage(messages.welcomeMessageSubtitle), + welcomeMessageTestnet: intl.formatMessage(messages.welcomeMessageTestnet), + welcomeMessageSubtitleTestnet: intl.formatMessage(messages.welcomeMessageSubtitleTestnet), + welcomeMessageSubtitleTestnetExtra: intl.formatMessage(messages.welcomeMessageSubtitleTestnetExtra), + goToFaucetButton: intl.formatMessage(messages.goToFaucetButton), + buyAda: intl.formatMessage(messages.buyAda), + stakePoolLabel: intl.formatMessage(messages.stakePoolLabel), }).current; }; diff --git a/packages/yoroi-extension/app/UI/features/staking/common/types/index.ts b/packages/yoroi-extension/app/UI/features/staking/common/types/index.ts index 194977a6154..8d14bcd695a 100644 --- a/packages/yoroi-extension/app/UI/features/staking/common/types/index.ts +++ b/packages/yoroi-extension/app/UI/features/staking/common/types/index.ts @@ -1,4 +1,4 @@ -// Define types +import type { ExplorerPoolInfo as PoolInfo } from '@emurgo/yoroi-lib'; export type StakingActions = {}; // Define state type @@ -13,6 +13,13 @@ export type StakingState = { historyGraphData: GraphData | null; primaryTokenInfo: any; toUnitOfAccount: (entry: any) => void | { currency: string; amount: string }; + defaultDelegatedAsset: any; + selectedWallet: any; + delegationStore: any; + legacyUIDialogs: any; + delegationRequests: any; + isWalletWithNoFunds: boolean; + currentlyDelegating: boolean; }; export interface GraphItems { @@ -33,3 +40,35 @@ export interface RewardsGraphData { export interface GraphData { readonly rewardsGraphData: RewardsGraphData; } + +export interface SocialLinks { + tw?: string; + fb?: string; + gh?: string; + tc?: string; + tg?: string; + di?: string; + yt?: string; + web?: string; + icon?: string; +} + +export interface PoolData { + id: string; + name: string; + ticker?: string; + avatar?: string | null; + roa?: string | null; + poolSize?: string | null; + share?: string | null; + websiteUrl?: string; + socialLinks?: SocialLinks; +} + +export interface PoolTransition { + currentPool?: PoolInfo | null; + deadlineMilliseconds?: number | null; + shouldShowTransitionFunnel: boolean; + suggestedPool?: PoolInfo | null; + deadlinePassed: boolean; +} diff --git a/packages/yoroi-extension/app/UI/features/staking/module/StakingContextProvider.tsx b/packages/yoroi-extension/app/UI/features/staking/module/StakingContextProvider.tsx index 807ad7bb765..a0b5e4df9fd 100644 --- a/packages/yoroi-extension/app/UI/features/staking/module/StakingContextProvider.tsx +++ b/packages/yoroi-extension/app/UI/features/staking/module/StakingContextProvider.tsx @@ -43,7 +43,23 @@ export const StakingContextProvider = observer(({ children, stores }: StakingPro throw new Error(`Page opened for non-reward wallet`); } + // Extract rewardHistory result to make it observable in the dependency array + const rewardHistoryResult = delegationRequests.rewardHistory.result; + + // Reset graphData when wallet changes + React.useEffect(() => { + setGraphData(null); + }, [selectedWallet.publicDeriverId]); + + // Generate graph data when reward history is available React.useEffect(() => { + // Only generate graph data if rewardHistory.result is available + // This prevents generating graph data with null result which causes infinite loading + if (rewardHistoryResult == null) { + setGraphData(null); + return; + } + const historyGraphData = generateGraphData({ delegationRequests, currentEpoch: stores.substores.ada.time.getCurrentTimeRequests(selectedWallet).currentEpoch, @@ -54,9 +70,18 @@ export const StakingContextProvider = observer(({ children, stores }: StakingPro defaultTokenId: selectedWallet.defaultTokenId, }); setGraphData(historyGraphData); - }, [delegationRequests, selectedWallet, currentlyDelegating]); - - React.useEffect(() => {}, []); + }, [ + delegationRequests, + rewardHistoryResult, + selectedWallet.publicDeriverId, + selectedWallet.networkId, + selectedWallet.defaultTokenId, + currentlyDelegating, + stores.profile.shouldHideBalance, + stores.substores.ada.time, + stores.delegation.getLocalPoolInfo, + stores.tokenInfoStore.tokenInfo, + ]); const totalDelegated = () => { if (!showRewardAmount) return undefined; diff --git a/packages/yoroi-extension/app/UI/features/staking/module/state.ts b/packages/yoroi-extension/app/UI/features/staking/module/state.ts index 5a192826144..e05f760d8b4 100644 --- a/packages/yoroi-extension/app/UI/features/staking/module/state.ts +++ b/packages/yoroi-extension/app/UI/features/staking/module/state.ts @@ -12,6 +12,13 @@ export const defaultStakingState: StakingState = { historyGraphData: null, primaryTokenInfo: null, toUnitOfAccount: () => ({ currency: '', amount: '' }), + defaultDelegatedAsset: null, + selectedWallet: null, + delegationStore: null, + legacyUIDialogs: null, + delegationRequests: null, + isWalletWithNoFunds: false, + currentlyDelegating: false, }; // Define action handlers diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/DelegatedStakePoolInfo/DelegatedStakePoolCard.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/DelegatedStakePoolInfo/DelegatedStakePoolCard.tsx new file mode 100644 index 00000000000..a19d2773645 --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/DelegatedStakePoolInfo/DelegatedStakePoolCard.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import { Box, Divider, Stack, styled, Typography } from '@mui/material'; +import { getAvatarFromPoolId } from '../../common/helpers'; +import { PoolData, PoolTransition } from '../../common/types'; +import { useStrings } from '../../common/hooks/useStrings'; +import { poolIdHexToBech32, useStaking } from '../../module/StakingContextProvider'; +import { truncateAddress } from '../../../../../utils/formatters'; +import { UndelegateButton } from './UndelegateButton'; + +export type DelegatedStakePoolCardProps = { + delegatedPool: PoolData; + poolTransition?: PoolTransition | null; + delegateToSpecificPool: (id: string | null) => void; +}; + +const DelegatedStakePoolCard: React.FC = ({ + delegatedPool, + poolTransition, + delegateToSpecificPool, +}) => { + const strings = useStrings(); + const { defaultDelegatedAsset } = useStaking(); + + const { id, name, ticker, poolSize, share, avatar, roa, socialLinks, websiteUrl } = delegatedPool || ({} as PoolData); + + const avatarGenerated = getAvatarFromPoolId(id); + + return ( + + + + {strings.stakePoolDelegated} + + + + + + + + + + + + + + + {ticker != null ? `[${ticker}]` : ''} {name && name !== '' ? name : truncateAddress(poolIdHexToBech32(id))} + + + + + + {roa != null && ( + + + {strings.roa30dLabel} + + + {roa} % + + + )} + + {poolSize != null && ( + + + {strings.poolSizeLabel} + + + {poolSize} {defaultDelegatedAsset.Metadata.ticker} + + + )} + + {share != null && ( + + + {strings.poolSaturation} + + + {share} % + + + )} + + + ); +}; + +export default DelegatedStakePoolCard; + +const Card = styled(Box)({ + borderRadius: '8px', + display: 'flex', + flexDirection: 'column', + justifyContent: 'flex-end', +}); + +const Wrapper = styled(Box)({ + display: 'flex', + padding: 24, +}); + +const AvatarWrapper = styled(Box)({ + width: '40px', + height: '40px', + minWidth: '40px', + marginRight: '12px', + borderRadius: '20px', + overflow: 'hidden', +}); + +const AvatarImg = styled('img')(({ theme }: any) => ({ + width: '100%', + background: theme.palette.ds.primary_100, + objectFit: 'scale-down', +})); diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/DelegatedStakePoolInfo/StakePoolDelegated.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/DelegatedStakePoolInfo/StakePoolDelegated.tsx new file mode 100644 index 00000000000..8f444ac2bb0 --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/DelegatedStakePoolInfo/StakePoolDelegated.tsx @@ -0,0 +1,48 @@ +import { ReactNode } from 'react'; +import { + formatLovelacesHumanReadableShort, + maybe, + roundOneDecimal, + roundTwoDecimal, + useStaking, +} from '../../module/StakingContextProvider'; +import DelegatedStakePoolCard from './DelegatedStakePoolCard'; +import { observer } from 'mobx-react'; + +export const StakePoolDelegated = observer((): ReactNode => { + const { delegationStore, selectedWallet } = useStaking(); + + const currentPool = delegationStore.getDelegatedPoolId(selectedWallet.publicDeriverId); + + if (currentPool == null) return null; + + const poolMeta = delegationStore.getLocalPoolInfo(selectedWallet.networkId, currentPool); + + const localRemote = delegationStore.getLocalRemotePoolInfo(selectedWallet.networkId, currentPool) ?? {}; + + const poolTransition = delegationStore.getPoolTransitionInfo(selectedWallet); + + const { stake, roa, saturation, pic } = localRemote; + + const delegatedPool = { + id: String(currentPool), + name: poolMeta?.info?.name ?? '', + avatar: pic, + roa: maybe(roa, x => roundTwoDecimal(Number(x))), + poolSize: maybe(stake, formatLovelacesHumanReadableShort), + share: maybe(saturation, s => roundOneDecimal(Number(s) * 100)), + websiteUrl: poolMeta?.info?.homepage, + ticker: poolMeta?.info?.ticker, + }; + return ( + => { + if (poolId != null) { + return delegationStore.createDelegationTransaction(poolId); + } + }} + /> + ); +}); diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/DelegatedStakePoolInfo/UndelegateButton.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/DelegatedStakePoolInfo/UndelegateButton.tsx new file mode 100644 index 00000000000..66a3d2c7333 --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/DelegatedStakePoolInfo/UndelegateButton.tsx @@ -0,0 +1,202 @@ +import { Box, Button, Link, Stack, Typography, styled } from '@mui/material'; +import BigNumber from 'bignumber.js'; +import { toSvg } from 'jdenticon'; + +import { useTxReviewModal } from '../../../transaction-review/module/ReviewTxProvider'; +import { asQuantity } from '../../../../utils/createCurrentWalletInfo'; +import { useStrings } from '../../common/hooks/useStrings'; +import { useStaking } from '../../module/StakingContextProvider'; +import { TransactionResult } from '../../../transaction-review/common/types'; + +export const UndelegateButton = ({ poolTransition, delegateToSpecificPool, poolId, poolName, socialMediaInfo }) => { + const { openTxReviewModal, startLoadingTxReview, stakeKeyDeposit, primaryTokenInfo, showTxResultModal, stakingRewards } = + useTxReviewModal(); + const { selectedWallet, stores } = useStaking(); + const strings = useStrings(); + const avatarSource = toSvg(poolId, 36, { padding: 0 }); + const avatarGenerated = `data:image/svg+xml;utf8,${encodeURIComponent(avatarSource)}`; + + if (poolTransition?.shouldShowTransitionFunnel) { + return ( + // @ts-ignore + delegateToSpecificPool(poolTransition.suggestedPool?.hash ?? '')}> + {strings.updatePoolLabel} + + ); + } + + const handleUndelegate = async () => { + stores.substores.ada.delegationTransaction.setShouldDeregister(true); + const unsignedTx = await stores.substores.ada.delegationTransaction.createWithdrawalTxForWallet({ wallet: selectedWallet }); + + openTxReviewModal({ + modalView: 'transactionReview', + submitTx: passswordInput => submitTx(passswordInput), + operations: { + components: [ + { + component: ( + + ), + duplicated: false, + }, + ], + kind: 'undelegate', + }, + unsignedTx: unsignedTx.unsignedTx, + }); + }; + + const submitTx = async password => { + const signRequest = stores.substores.ada.delegationTransaction.createWithdrawalTx.result; + if (signRequest == null) return; + + try { + startLoadingTxReview(); + + await stores.transactionProcessingStore.adaSendAndRefresh({ + wallet: stores.wallets.selected, + signRequest, + password, + callback: async () => {}, + }); + + showTxResultModal(TransactionResult.SUCCESS); + } catch (_error) { + showTxResultModal(TransactionResult.FAIL); + } + }; + + return ( + + {strings.undelegateLabel} + + ); +}; + +const OperationsDetails = ({ stakeKeyDeposit, avatarGenerated, poolName, socialMediaInfo, stakingRewards }) => { + const { socialLinks, websiteUrl } = socialMediaInfo ?? {}; + const urls = getSocialMediaLinks(socialLinks, websiteUrl); + const strings = useStrings(); + const link = websiteUrl ?? urls[0]; + + return ( + + + {strings.undelegatePool} + + + + + {link && ( + + {poolName} + + )} + + + + + {strings.deregisteringStakingKey} + {stakeKeyDeposit} + + + {strings.totalRewardsLabel} + {stakingRewards} + + + ); +}; + +export const getSocialMediaLink = (platform, handle) => { + const baseUrls = { + twitter: 'https://twitter.com/', + telegram: 'https://t.me/', + facebook: 'https://fb.me/', + youtube: 'https://youtube.com/', + twitch: 'https://twitch.com/', + discord: 'https://discord.gg/', + github: 'https://github.com/', + }; + + const baseUrl = baseUrls[platform]; + return baseUrl ? `${baseUrl}${handle}` : ''; +}; + +export const getSocialMediaLinks = (socialLinks, websiteUrl) => { + const urls: string[] = []; + + if (socialLinks?.tw) urls.push(getSocialMediaLink('twitter', socialLinks.tw)); + if (socialLinks?.tg) urls.push(getSocialMediaLink('telegram', socialLinks.tg)); + if (socialLinks?.fb) urls.push(getSocialMediaLink('facebook', socialLinks.fb)); + if (socialLinks?.yt) urls.push(getSocialMediaLink('youtube', socialLinks.yt)); + if (socialLinks?.tc) urls.push(getSocialMediaLink('twitch', socialLinks.tc)); + if (socialLinks?.di) urls.push(getSocialMediaLink('discord', socialLinks.di)); + if (socialLinks?.gh) urls.push(getSocialMediaLink('github', socialLinks.gh)); + + if (websiteUrl) urls.push(websiteUrl); + + return urls; +}; + +const UpdatePoolButton = styled(Button)(({ theme }: any) => ({ + minWidth: 'auto', + width: '140px', + marginLeft: 'auto', + background: theme.palette.ds.sys_magenta_500, + color: 'white', + height: '40px', + padding: '0px !important', + fontSize: '14px', + '&:hover': { + backgroundColor: theme.palette.ds.sys_magenta_500, + color: 'white', + }, +})); + +const UndelegateBtn = styled(Button)({ + minWidth: 'auto', + width: 'unset', + marginLeft: 'auto', +}); + +const StyledLink: any = styled(Link)(({ theme }: any) => ({ + marginRight: '5px', + color: 'inherit', + '& svg': { + '& path': { + fill: theme.palette.ds.el_gray_medium, + }, + }, +})); diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/EpochProgress/EpochProgress.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/EpochProgress/EpochProgress.tsx new file mode 100644 index 00000000000..54a79af4bab --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/EpochProgress/EpochProgress.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import moment from 'moment'; +import EpochProgressWrapper from './EpochProgressWrapper'; +import { useStaking } from '../../module/StakingContextProvider'; + +const EpochProgress: React.FC = () => { + const { stores, selectedWallet } = useStaking(); + const timeCalcRequests = stores.substores.ada.time.getTimeCalcRequests(selectedWallet); + const { toAbsoluteSlot, toRealTime, currentEpochLength } = timeCalcRequests.requests; + + const currTimeRequests = stores.substores.ada.time.getCurrentTimeRequests(selectedWallet); + const currentEpoch: number = currTimeRequests.currentEpoch; + + const epochLength = currentEpochLength(); + + const getDateFromEpoch = (epoch: number, returnEpochTime = false): string | Date => { + const epochTime = toRealTime({ + absoluteSlotNum: toAbsoluteSlot({ + epoch, + // Rewards are calculated at the start of the epoch but distributed at the end + slot: epochLength, + }), + }); + + return returnEpochTime ? epochTime : moment(epochTime).format('lll'); + }; + + const endEpochDate = getDateFromEpoch(currentEpoch) as string; + const endEpochDateTime = getDateFromEpoch(currentEpoch, true) as Date; + const previousEpochDate = getDateFromEpoch(currentEpoch - 1) as string; + + const percentage = Math.floor((100 * currTimeRequests.currentSlot) / epochLength); + + return ( + + ); +}; + +export default EpochProgress; diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/EpochProgress/EpochProgressCard.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/EpochProgress/EpochProgressCard.tsx new file mode 100644 index 00000000000..bb0c41234d2 --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/EpochProgress/EpochProgressCard.tsx @@ -0,0 +1,105 @@ +import React from 'react'; +import { Box, CircularProgress, Stack, Typography } from '@mui/material'; + +export interface EpochProgressCardProps { + percentage: number; + days: number; + currentEpoch: number; + startEpochDate: string | Date; + endEpochDate: string | Date; +} + +export const EpochProgressCard: React.FC = ({ + percentage, + days, + currentEpoch, + startEpochDate, + endEpochDate, +}) => { + return ( + + + + + + <Stack direction="row" gap={16} mt="50px" justifyContent="space-between"> + <LabelWithValue label="Epoch started at" value={startEpochDate} /> + <LabelWithValue label="Epoch ends at" value={endEpochDate} /> + </Stack> + </Stack> + </Stack> + </Box> + ); +}; + +interface TitleProps { + label: string; + value: string | number; +} + +const Title: React.FC<TitleProps> = ({ label, value }) => { + return ( + <Box> + <Typography fontWeight={500} color="ds.primary_600"> + {label}: {value} + </Typography> + </Box> + ); +}; + +interface InfoColumnProps { + label: string; + value: string | number | Date; +} + +const LabelWithValue: React.FC<InfoColumnProps> = ({ label, value }) => { + return ( + <Box minWidth="203px"> + <Typography sx={{ textTransform: 'uppercase' }} variant="caption" mb="4px" color="ds.gray_600"> + {label} + </Typography> + <Typography color="ds.gray_900">{value instanceof Date ? value.toString() : value}</Typography> + </Box> + ); +}; + +interface GraphProps { + value: number; + days: number; +} + +const Graph: React.FC<GraphProps> = ({ value, days }) => { + return ( + <Box mr="8px" position="relative" display="flex" justifyContent="center"> + <CircularProgress + size={120} + thickness={7} + variant="determinate" + value={value} + sx={{ + color: 'primary.600', + animationDuration: '550ms', + position: 'absolute', + zIndex: 1, + }} + /> + <CircularProgress size={120} thickness={7} variant="determinate" sx={{ color: 'ds.gray_100' }} value={100} /> + <Box + position="absolute" + sx={{ + top: '30%', + left: '50%', + transform: 'translate(-50%)', + textAlign: 'center', + }} + > + <Typography variant="h4" color="ds.gray_900"> + {value}% + </Typography> + <Typography variant="caption" fontSize="12px" color="ds.gray_600"> + {days} days + </Typography> + </Box> + </Box> + ); +}; diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/EpochProgress/EpochProgressWrapper.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/EpochProgress/EpochProgressWrapper.tsx new file mode 100644 index 00000000000..b1dda5d1647 --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/EpochProgress/EpochProgressWrapper.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import { Box, Divider, styled, Typography } from '@mui/material'; +import moment from 'moment'; + +import { EpochProgressCard } from './EpochProgressCard'; +import { useStrings } from '../../common/hooks/useStrings'; + +export interface EpochProgressData { + currentEpoch: number; + startEpochDate: string | Date; + endEpochDate: string | Date; + endEpochDateTime: Date; + percentage: number; +} + +export interface EpochProgressWrapperProps { + epochProgress: EpochProgressData; +} + +const EpochProgressWrapper: React.FC<EpochProgressWrapperProps> = ({ epochProgress }) => { + const strings = useStrings(); + + // Days remaining + const days = moment(epochProgress.endEpochDateTime).diff(moment(), 'days'); + + return ( + <Card + sx={{ + border: '1px solid', + borderColor: 'ds.gray_200', + bgcolor: 'ds.bg_color_max', + }} + > + <Box sx={{ padding: '17px 24px' }}> + <Typography variant="h5" color="ds.text_gray_medium" fontWeight={500}> + {strings.epochProgress} + </Typography> + </Box> + + <Divider sx={{ borderColor: 'ds.gray_200' }} /> + + <Box sx={{ padding: '24px' }}> + <EpochProgressCard + percentage={epochProgress.percentage} + days={days} + currentEpoch={epochProgress.currentEpoch} + startEpochDate={epochProgress.startEpochDate} + endEpochDate={epochProgress.endEpochDate} + /> + </Box> + </Card> + ); +}; + +export default EpochProgressWrapper; + +const Card = styled(Box)({ + borderRadius: '8px', + flex: '1 1 100%', + display: 'flex', + flexDirection: 'column', +}); diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/LegacyDialogs/LegacyDialogs.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/LegacyDialogs/LegacyDialogs.tsx new file mode 100644 index 00000000000..790b834290f --- /dev/null +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/LegacyDialogs/LegacyDialogs.tsx @@ -0,0 +1,77 @@ +import { maybe, useStaking } from '../../module/StakingContextProvider'; +import OverviewModal from '../../../../../components/wallet/staking/dashboard-revamp/OverviewDialog'; +import { generateGraphData } from '../../common/helpers/graph'; +import { GovernanceParticipateDialog } from '../../../../../containers/wallet/dialogs/GovernanceParticipateDialog'; +import UnmangleTxDialogContainer from '../../../../../containers/transfer/UnmangleTxDialogContainer'; +import RewardHistoryDialog from '../../../../../components/wallet/staking/dashboard-revamp/RewardHistoryDialog'; +import { observer } from 'mobx-react'; + +export const LegacyDialogs = observer(() => { + const { legacyUIDialogs, stores, totalRewards, toUnitOfAccount, delegationRequests, selectedWallet } = useStaking(); + const errorIfPresent = maybe(delegationRequests.error, error => ({ error })); + + const showRewardAmount = errorIfPresent == null && stores.delegation.isExecutedDelegatedBalance(selectedWallet.publicDeriverId); + const isParticipatingToGovernance = stores.delegation.governanceStatus?.drepDelegation !== null; + const isStakeRegistered = stores.delegation.isStakeRegistered(selectedWallet.publicDeriverId); + + const onClose = () => { + legacyUIDialogs.closeActiveDialog(); + }; + + return ( + <div> + {legacyUIDialogs.isOpen(OverviewModal) ? ( + <OverviewModal + onClose={onClose} + getTokenInfo={genLookupOrFail(stores.tokenInfoStore.tokenInfo)} + totalRewards={showRewardAmount ? totalRewards : undefined} + shouldHideBalance={stores.profile.shouldHideBalance} + unitOfAccount={toUnitOfAccount} + withdrawRewards={ + isParticipatingToGovernance === false + ? () => { + legacyUIDialogs.open({ + dialog: GovernanceParticipateDialog, + }); + } + : isStakeRegistered + ? () => { + legacyUIDialogs.open({ + dialog: GovernanceParticipateDialog, + }); + } + : undefined + } + /> + ) : null} + {legacyUIDialogs.isOpen(GovernanceParticipateDialog) ? ( + <GovernanceParticipateDialog stores={stores} onClose={onClose} /> + ) : null} + {legacyUIDialogs.isOpen(UnmangleTxDialogContainer) ? <UnmangleTxDialogContainer stores={stores} onClose={onClose} /> : null} + {legacyUIDialogs.isOpen(RewardHistoryDialog) ? ( + <RewardHistoryDialog + onClose={onClose} + graphData={generateGraphData({ + delegationRequests, + currentEpoch: stores.substores.ada.time.getCurrentTimeRequests(selectedWallet).currentEpoch, + shouldHideBalance: stores.profile.shouldHideBalance, + getLocalPoolInfo: stores.delegation.getLocalPoolInfo, + tokenInfo: stores.tokenInfoStore.tokenInfo, + networkId: selectedWallet.networkId, + defaultTokenId: selectedWallet.defaultTokenId, + })} + /> + ) : null} + </div> + ); +}); + +export const genLookupOrFail = map => lookup => { + const tokenRow = map.get(lookup.networkId.toString())?.get(lookup.identifier); + + if (tokenRow == null) { + throw new Error(`genLookupOrFail: no token info for ${JSON.stringify(lookup)}`); + } + + return tokenRow; +}; diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/PoolList/PoolList.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/PoolList/PoolList.tsx index 73d5e63caf3..b509690ad6d 100644 --- a/packages/yoroi-extension/app/UI/features/staking/useCases/PoolList/PoolList.tsx +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/PoolList/PoolList.tsx @@ -1,9 +1,20 @@ -import { Typography } from '@mui/material'; +// $FlowIgnore: suppressing this error +import CardanoStakingPage from '../../../../../containers/wallet/staking/CardanoStakingPage'; +import { useStaking } from '../../module/StakingContextProvider'; +import type { ConfigType } from '../../../../../../config/config-types'; + +// populated by ConfigWebpackPlugin +declare var CONFIG: ConfigType; export const PoolList = () => { + const { stores, delegationStore, selectedWallet } = useStaking(); return ( <div> - <Typography variant="h1">PoolList</Typography> + <CardanoStakingPage + stores={stores} + urlTemplate={CONFIG.poolExplorer.simpleTemplate} + poolTransition={delegationStore.getPoolTransitionInfo(selectedWallet)} + /> </div> ); }; diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardGraphClean.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardGraphClean.tsx index 40642439ae0..87136022e29 100644 --- a/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardGraphClean.tsx +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardGraphClean.tsx @@ -39,7 +39,7 @@ const RewardGraphClean: React.FC<Props> = ({ }) => { const theme: any = useTheme(); - const formatYAxis = (value: number): string | number => (!hideYAxis ? value : '∗∗∗ '); + const formatYAxis = (value: number): string | number => (!hideYAxis ? value : '∗∗∗'); const GraphTooltip: React.FC<GraphTooltipProps> = ({ active, payload, label }) => { if (active && payload != null && payload.length > 0 && label != null) { diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardHistoryGraph.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardHistoryGraph.tsx index d38dc2e622d..d932da2da59 100644 --- a/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardHistoryGraph.tsx +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardHistoryGraph.tsx @@ -2,15 +2,16 @@ import React from 'react'; import { Box, styled } from '@mui/system'; import { Button, CircularProgress, Stack, Typography } from '@mui/material'; import RewardGraphClean from './RewardGraphClean'; -// import VerticallyCenteredLayout from '../../../layout/VerticallyCenteredLayout'; // --- IGNORE --- import MuiAccordion, { AccordionProps as MuiAccordionProps } from '@mui/material/Accordion'; import MuiAccordionSummary, { AccordionSummaryProps as MuiAccordionSummaryProps } from '@mui/material/AccordionSummary'; import MuiAccordionDetails from '@mui/material/AccordionDetails'; import { getAvatarFromPoolId } from '../../common/helpers'; import { useStrings } from '../../common/hooks/useStrings'; import { GraphData } from '../../common/types'; +import { useStaking } from '../../module/StakingContextProvider'; -/* ---------- Types ---------- */ +import RewardHistoryDialog from '../../../../../components/wallet/staking/dashboard-revamp/RewardHistoryDialog'; +import { observer } from 'mobx-react'; type RewardHistoryEntry = { type: string; @@ -27,12 +28,12 @@ type RewardHistoryItemProps = { type RewardHistoryGraphProps = { graphData: GraphData; - onOpenRewardList: () => void; }; /* ---------- RewardHistoryItem ---------- */ export const RewardHistoryItem: React.FC<RewardHistoryItemProps> = ({ poolId, poolName, poolAvatar, historyList }) => { + const strings = useStrings(); const avatarGenerated = getAvatarFromPoolId(poolId); return ( @@ -41,7 +42,7 @@ export const RewardHistoryItem: React.FC<RewardHistoryItemProps> = ({ poolId, po <Box> <Box display="block"> <Typography component="div" color="var(--yoroi-palette-gray-600)"> - Stake Pool + {strings.stakePoolLabel} </Typography> </Box> <Box display="flex"> @@ -138,11 +139,15 @@ const AccordionSummary = styled((props: MuiAccordionSummaryProps) => ( }, })); -const RewardHistoryGraph: React.FC<RewardHistoryGraphProps> = ({ graphData, onOpenRewardList }) => { +const RewardHistoryGraph: React.FC<RewardHistoryGraphProps> = observer(({ graphData }) => { const strings = useStrings(); + const { stores } = useStaking(); const { rewardsGraphData } = graphData; const rewardList = rewardsGraphData.items?.perEpochRewards; const title = strings.rewardHistoryLabel; + const isRewardListArray = Array.isArray(rewardList); + const hasError = rewardsGraphData.error && !rewardsGraphData.items; + const isLoading = !isRewardListArray && !hasError; return ( <Box @@ -166,16 +171,20 @@ const RewardHistoryGraph: React.FC<RewardHistoryGraphProps> = ({ graphData, onOp </Typography> <Button // @ts-ignore - variant="primary" + variant="tertiary" size="medium" - onClick={onOpenRewardList} + onClick={() => + stores.uiDialogs.open({ + dialog: RewardHistoryDialog, + }) + } sx={{ lineHeight: '21px' }} > {title} </Button> </Box> - {rewardsGraphData.error && !rewardsGraphData.items && ( + {hasError && ( <div> <Typography variant="body2" color="ds.text_error"> {strings.errorLabel} @@ -183,9 +192,9 @@ const RewardHistoryGraph: React.FC<RewardHistoryGraphProps> = ({ graphData, onOp </div> )} - {!Array.isArray(rewardList) ? ( - <CircularProgress /> - ) : ( + {isLoading && <CircularProgress />} + + {isRewardListArray && ( <Box ml="-50px"> <RewardGraphClean epochTitle={strings.epochLabel} @@ -200,6 +209,6 @@ const RewardHistoryGraph: React.FC<RewardHistoryGraphProps> = ({ graphData, onOp )} </Box> ); -}; +}); export default RewardHistoryGraph; diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardsSummaryCard.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardsSummaryCard.tsx index 9233c37457d..6128ba2e33a 100644 --- a/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardsSummaryCard.tsx +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/RewardsSummaryCard.tsx @@ -1,8 +1,6 @@ -import React, { ReactNode } from 'react'; +import { ReactNode } from 'react'; import { Box, styled } from '@mui/system'; import { Divider, Typography } from '@mui/material'; -// import loadingSpinnerStyles from '../dashboard/LoadingSpinner.scss' -// import LoadingSpinner from '../../../widgets/LoadingSpinner' import RewardHistoryGraph from './RewardHistoryGraph'; import LoadingSpinner from '../../../../../components/widgets/LoadingSpinner'; import { WithdrawButton } from './WithdrawButton'; @@ -10,6 +8,7 @@ import { useStrings } from '../../common/hooks/useStrings'; import { maybe, useStaking } from '../../module/StakingContextProvider'; import { HIDDEN_AMOUNT } from '../../../../common/constants'; import { truncateToken } from '../../../../../utils/formatters'; +import { getTokenName } from '../../../../../stores/stateless/tokenHelpers'; import { Icon } from '../../../../components'; const StakingIconWrapper = styled(Box)(({ theme }) => ({ @@ -57,9 +56,7 @@ const InfoDetails = styled(Box)({}); export const RewardsSummaryCard: React.FC = () => { const strings = useStrings(); - const { getTokenInfo, onOpenRewardList, totalRewards, totalDelegated, shouldHideBalance, historyGraphData, toUnitOfAccount } = - useStaking(); - const govStatusFetched = true; // TODO: get from governance provider + const { getTokenInfo, totalRewards, totalDelegated, shouldHideBalance, historyGraphData, toUnitOfAccount } = useStaking(); const formatTokenEntry = (tokenEntry): ReactNode => { const tokenInfo = getTokenInfo(tokenEntry); @@ -82,7 +79,7 @@ export const RewardsSummaryCard: React.FC = () => { return ( <> <span>{amountNode} </span> - {truncateToken(tokenInfo?.assetName || '', 12)} + {truncateToken(getTokenName(tokenInfo))} </> ); }; @@ -98,8 +95,7 @@ export const RewardsSummaryCard: React.FC = () => { ); }; - // TODO: enable later - // const hasNoRewards = (token?: any | null): boolean => (totalRewards ? token?.getDefaultEntry()?.amount?.isZero() : true); + const hasNoRewards = token => maybe(token, t => t.getDefaultEntry()?.amount?.isZero?.()) ?? false; return ( <Card @@ -123,7 +119,7 @@ export const RewardsSummaryCard: React.FC = () => { {strings.rewardsSummary} </Typography> - <WithdrawButton govStatusFetched={govStatusFetched} isDisabled={totalRewards === undefined} /> + <WithdrawButton isDisabled={hasNoRewards(totalRewards)} /> </Box> <Divider sx={{ borderColor: 'ds.gray_200' }} /> @@ -135,16 +131,19 @@ export const RewardsSummaryCard: React.FC = () => { </StakingIconWrapper> <InfoDetails> - <Typography variant="caption" color="ds.gray_600" sx={{ textTransform: 'uppercase' }}> + {/*@ts-ignore */} + <Typography variant="caption1" color="ds.gray_600" sx={{ textTransform: 'uppercase' }}> {strings.totalRewardsLabel} </Typography> </InfoDetails> <InfoDetails> - <Typography variant="h2" color="ds.text_gray_medium" fontWeight={500}> - {totalRewards ? renderAmount(totalRewards) : <LoadingSpinner small />} + <Typography component="div" variant="h2" color="ds.text_gray_medium" fontWeight={500}> + {renderAmount(totalRewards)} + </Typography> + <Typography component="div" variant="body1" color="grayscale.600" fontWeight={500}> + {renderAmountWithUnitOfAccount(totalRewards)} </Typography> - <Typography variant="body1" color="ds.gray_600" fontWeight={500}></Typography> </InfoDetails> </InfoRow> @@ -154,7 +153,8 @@ export const RewardsSummaryCard: React.FC = () => { </TotalDelegatedIconWrapper> <InfoDetails> - <Typography variant="caption" color="ds.gray_600" marginBottom="4px" sx={{ textTransform: 'uppercase' }}> + {/*@ts-ignore */} + <Typography variant="caption1" color="ds.gray_600" marginBottom="4px" sx={{ textTransform: 'uppercase' }}> {strings.totalDelegated} </Typography> </InfoDetails> @@ -178,7 +178,7 @@ export const RewardsSummaryCard: React.FC = () => { </InfoRow> </Box> - {historyGraphData && <RewardHistoryGraph onOpenRewardList={onOpenRewardList} graphData={historyGraphData} />} + {historyGraphData && <RewardHistoryGraph graphData={historyGraphData} />} </Card> ); }; diff --git a/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/WithdrawButton.tsx b/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/WithdrawButton.tsx index 6d191302d6e..f701b27c7fa 100644 --- a/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/WithdrawButton.tsx +++ b/packages/yoroi-extension/app/UI/features/staking/useCases/RewardsSummary/WithdrawButton.tsx @@ -4,8 +4,11 @@ import { useTxReviewModal } from '../../../transaction-review/module/ReviewTxPro import { TransactionResult } from '../../../transaction-review/common/types'; import { useStrings } from '../../common/hooks/useStrings'; import { useStaking } from '../../module/StakingContextProvider'; +import React from 'react'; + +export const WithdrawButton = ({ isDisabled }) => { + const [govStatusFetched, setStatusFetched] = React.useState(false); -export const WithdrawButton = ({ govStatusFetched, isDisabled }) => { const { openTxReviewModal, stopLoadingTxReview, startLoadingTxReview, showTxResultModal } = useTxReviewModal(); const strings = useStrings(); const { stores } = useStaking(); @@ -14,6 +17,18 @@ export const WithdrawButton = ({ govStatusFetched, isDisabled }) => { const wallet = stores.wallets.selected; const isStakeRegistered = stores.delegation.isStakeRegistered(wallet.publicDeriverId); + React.useEffect(() => { + stores.delegation + .checkGovernanceStatus(wallet) + .then(() => { + setStatusFetched(true); + return null; + }) + .catch(e => { + console.error('Failed to fetch governance status', e); + }); + }, []); + const handleRewardsWithdrawal = async () => { if (!isParticipatingToGovernance) { stores.uiDialogs.open({ diff --git a/packages/yoroi-extension/app/components/layout/TopBarLayout.js b/packages/yoroi-extension/app/components/layout/TopBarLayout.js index 1882f4130bf..89462df35ba 100644 --- a/packages/yoroi-extension/app/components/layout/TopBarLayout.js +++ b/packages/yoroi-extension/app/components/layout/TopBarLayout.js @@ -60,7 +60,7 @@ function TopBarLayout({ flex: '0 1 auto', height: '100%', }), - overflow: 'auto', + overflow: 'scroll', }} > { diff --git a/packages/yoroi-extension/app/components/topbar/banners/NotProductionBanner.scss b/packages/yoroi-extension/app/components/topbar/banners/NotProductionBanner.scss index 3a50594861d..76be1845136 100644 --- a/packages/yoroi-extension/app/components/topbar/banners/NotProductionBanner.scss +++ b/packages/yoroi-extension/app/components/topbar/banners/NotProductionBanner.scss @@ -1,5 +1,5 @@ .notProdWarning { - height: 46px; + height: 27px; display: flex; justify-content: center; align-items: center; diff --git a/packages/yoroi-extension/app/components/topbar/banners/TestnetWarningBanner.scss b/packages/yoroi-extension/app/components/topbar/banners/TestnetWarningBanner.scss index 549a70f6950..fd3e24a9bff 100644 --- a/packages/yoroi-extension/app/components/topbar/banners/TestnetWarningBanner.scss +++ b/packages/yoroi-extension/app/components/topbar/banners/TestnetWarningBanner.scss @@ -1,31 +1,7 @@ @import '../../../themes/mixins/underline'; -.testnetWarning { - font-weight: 700; - text-align: center; - color: var(--yoroi-palette-common-white); - background-color: var(--yoroi-palette-background-banner-warning); - padding: 4px; - text-transform: uppercase; - font-size: 14px; - line-height: 21px; - - .warningIcon { - display: inline-flex; - margin-right: 5px; - vertical-align: top; - } - - a { - color: var(--yoroi-palette-common-white); - font-weight: 700; - text-transform: uppercase; - @include underline(var(--yoroi-palette-common-white)); - } -} - .shelleyTestnetWarning { - height: 46px; + height: 27px; display: flex; justify-content: center; align-items: center; diff --git a/packages/yoroi-extension/app/containers/wallet/WalletDelegationBanner.js b/packages/yoroi-extension/app/containers/wallet/WalletDelegationBanner.js index b0066db5302..047202700c9 100644 --- a/packages/yoroi-extension/app/containers/wallet/WalletDelegationBanner.js +++ b/packages/yoroi-extension/app/containers/wallet/WalletDelegationBanner.js @@ -1,5 +1,6 @@ // @flow import type { Node, ComponentType } from 'react'; +import { useState, useEffect } from 'react'; import { Box, styled } from '@mui/system'; import { Button, Typography, Link } from '@mui/material'; @@ -74,6 +75,14 @@ const messages = defineMessages({ }); function WalletDelegationBanner({ isOpen, isWalletWithNoFunds, isTestnet, intl, ticker, poolInfo, stores }: Props & Intl): Node { + const avatar = poolInfo?.avatar; + const [avatarLoadError, setAvatarLoadError] = useState(false); + + // Reset error state when avatar changes + useEffect(() => { + setAvatarLoadError(false); + }, [avatar]); + if (poolInfo == null) { return ( <Box display="flex" justifyContent="center" alignItems="center" py="40px"> @@ -81,11 +90,13 @@ function WalletDelegationBanner({ isOpen, isWalletWithNoFunds, isTestnet, intl, </Box> ); } - const { id, name, avatar, websiteUrl, roa: estimatedRoa30d, socialLinks } = poolInfo || {}; + const { id, name, websiteUrl, roa: estimatedRoa30d, socialLinks } = poolInfo || {}; const avatarSource = toSvg(id, 36, { padding: 0 }); const avatarGenerated = `data:image/svg+xml;utf8,${encodeURIComponent(avatarSource)}`; + const shouldUseAvatar = avatar && !avatarLoadError; + return isOpen ? ( <WrapperBanner sx={{ @@ -104,7 +115,11 @@ function WalletDelegationBanner({ isOpen, isWalletWithNoFunds, isTestnet, intl, </Typography> <Box sx={{ display: 'flex', mb: '16px', mt: '24px' }}> <AvatarWrapper> - {avatar ? <AvatarImg src={avatar} alt={name} /> : <AvatarImg src={avatarGenerated} alt={name} />} + {shouldUseAvatar ? ( + <AvatarImg src={avatar} alt={name} onError={() => setAvatarLoadError(true)} /> + ) : ( + <AvatarImg src={avatarGenerated} alt={name} /> + )} </AvatarWrapper> <Typography component="div" color="ds.text_gray_medium" variant="body1" fontWeight={500}> {name} diff --git a/packages/yoroi-extension/app/i18n/locales/en-US.json b/packages/yoroi-extension/app/i18n/locales/en-US.json index 7a8d286cdd2..89b530b33f7 100644 --- a/packages/yoroi-extension/app/i18n/locales/en-US.json +++ b/packages/yoroi-extension/app/i18n/locales/en-US.json @@ -853,6 +853,7 @@ "wallet.delegation.transaction.explanationLine2": "You can switch to delegate to a different stake pool at any time", "wallet.delegation.transaction.explanationLine3": "You can cancel your delegation at any time", "wallet.delegation.transaction.generation": "Generating transaction", + "wallet.delegation.transaction.stakePoolLabel": "Stake pool", "wallet.delegation.transaction.stakePoolHash": "Stake pool id", "wallet.delegation.transaction.stakePoolName": "Stake pool name", "wallet.delegation.transaction.success.button.label": "Dashboard page", diff --git a/packages/yoroi-extension/package-lock.json b/packages/yoroi-extension/package-lock.json index 4452d0ff289..e6e3d523f2d 100644 --- a/packages/yoroi-extension/package-lock.json +++ b/packages/yoroi-extension/package-lock.json @@ -34,6 +34,7 @@ "@posthog/react": "1.4.0", "@svgr/webpack": "5.5.0", "@tanstack/react-query": "^5.90.11", + "@yoroi/api": "6.0.0", "@yoroi/exchange": "2.0.1", "@yoroi/explorers": "^1.0.2", "@yoroi/portfolio": "1.0.3", @@ -10496,51 +10497,23 @@ "license": "Apache-2.0" }, "node_modules/@yoroi/api": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@yoroi/api/-/api-1.5.1.tgz", - "integrity": "sha512-upwmeE1a9pdykkDMBRl1Ubwvb4YDWbK5M3OKHAA9b0iKivpblo+vhjOqyqfaaUqRR5GdCljkgvJuaJLYqd0PVA==", - "license": "Apache-2.0", - "dependencies": { - "@emurgo/cip14-js": "^3.0.1", - "@yoroi/common": "1.5.1", - "axios": "^1.5.0", - "zod": "^3.22.1" - }, - "engines": { - "node": ">= 16.19.0" - }, - "peerDependencies": { - "react": ">= 16.8.0 <= 19.0.0", - "react-query": "^3.39.3" - } - }, - "node_modules/@yoroi/api/node_modules/@yoroi/common": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@yoroi/common/-/common-1.5.1.tgz", - "integrity": "sha512-ifOdu0wOoXUqMAWfsK05UAQIBytUHK6W60P5gdcBDjg7s28IykxRP7h0MnbGocgV89p5Sdyd/10yb0eRh2o+Nw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@yoroi/api/-/api-6.0.0.tgz", + "integrity": "sha512-BEsF48KtX9wkEtsbTBCSSCSBKQFQDQ0q5hvwg+qhG3px3FZt4euy8EWpcHuzA+4sMgXw8ZPxIaByC5BXBk8GXg==", "license": "Apache-2.0", "dependencies": { - "axios": "^1.5.0", - "zod": "^3.22.1" + "@emurgo/cip14-js": "^3.0.1" }, "engines": { - "node": ">= 16.19.0" + "node": ">= 22.12.0" }, "peerDependencies": { - "@react-native-async-storage/async-storage": ">= 1.19.3 <= 1.20.0", + "@tanstack/react-query": "^5.76.2", + "@yoroi/common": "6.0.0", + "axios": "^1.10.0", + "immer": "10.1.1", "react": ">= 16.8.0 <= 19.0.0", - "react-query": "^3.39.3" - } - }, - "node_modules/@yoroi/api/node_modules/axios": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", - "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "zod": "3.25.17" } }, "node_modules/@yoroi/common": { @@ -23735,6 +23708,25 @@ "react-query": "^3.39.3" } }, + "node_modules/legacySwap/node_modules/@yoroi/api": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@yoroi/api/-/api-1.5.1.tgz", + "integrity": "sha512-upwmeE1a9pdykkDMBRl1Ubwvb4YDWbK5M3OKHAA9b0iKivpblo+vhjOqyqfaaUqRR5GdCljkgvJuaJLYqd0PVA==", + "license": "Apache-2.0", + "dependencies": { + "@emurgo/cip14-js": "^3.0.1", + "@yoroi/common": "1.5.1", + "axios": "^1.5.0", + "zod": "^3.22.1" + }, + "engines": { + "node": ">= 16.19.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 <= 19.0.0", + "react-query": "^3.39.3" + } + }, "node_modules/legacySwap/node_modules/@yoroi/common": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@yoroi/common/-/common-1.5.1.tgz",