diff --git a/packages/yoroi-extension/app/Routes.js b/packages/yoroi-extension/app/Routes.js index 533f7c3dda..83da39e8e4 100644 --- a/packages/yoroi-extension/app/Routes.js +++ b/packages/yoroi-extension/app/Routes.js @@ -11,10 +11,18 @@ import type { GeneratedData as WalletData } from './containers/wallet/Wallet'; import type { GeneratedData as ReceiveData } from './containers/wallet/Receive'; import type { ConfigType } from '../config/config-types'; import type { GeneratedData as AssetsData } from './containers/wallet/AssetsWrapper'; +import LoadingPage from './containers/LoadingPage'; +import StakingPage, { StakingPageContentPromise } from './containers/wallet/staking/StakingPage'; +import Wallet from './containers/wallet/Wallet'; +import Settings from './containers/settings/Settings'; +import Transfer, { WalletTransferPagePromise } from './containers/transfer/Transfer'; +import VotingPage, { VotingPageContentPromise } from './containers/wallet/voting/VotingPage'; +import ConnectedWebsitesPage, { ConnectedWebsitesPagePromise } from './containers/dapp-connector/ConnectedWebsitesContainer'; +import WalletAddPage, { AddAnotherWalletPromise } from './containers/wallet/WalletAddPage' +import AssetsWrapper from './containers/wallet/AssetsWrapper'; +import NFTsWrapper from './containers/wallet/NFTsWrapper'; // PAGES -const WalletAddPagePromise = () => import('./containers/wallet/WalletAddPage'); -const WalletAddPage = React.lazy(WalletAddPagePromise); const LanguageSelectionPagePromise = () => import('./containers/profile/LanguageSelectionPage'); const LanguageSelectionPage = React.lazy(LanguageSelectionPagePromise); const TermsOfUsePagePromise = () => import('./containers/profile/TermsOfUsePage'); @@ -23,8 +31,6 @@ const UriPromptPagePromise = () => import('./containers/profile/UriPromptPage'); const UriPromptPage = React.lazy(UriPromptPagePromise); // SETTINGS -const SettingsPromise = () => import('./containers/settings/Settings'); -const Settings = React.lazy(SettingsPromise); const GeneralSettingsPagePromise = () => import('./containers/settings/categories/GeneralSettingsPage'); const GeneralSettingsPage = React.lazy(GeneralSettingsPagePromise); const WalletSettingsPagePromise = () => import('./containers/settings/categories/WalletSettingsPage'); @@ -38,16 +44,10 @@ const TermsOfUseSettingsPage = React.lazy(TermsOfUseSettingsPagePromise); const SupportSettingsPagePromise = () => import('./containers/settings/categories/SupportSettingsPage'); const SupportSettingsPage = React.lazy(SupportSettingsPagePromise); -// Dynamic container loading - resolver loads file relative to '/app/' directory -const LoadingPagePromise = () => import('./containers/LoadingPage'); -const LoadingPage = React.lazy(LoadingPagePromise); const NightlyPagePromise = () => import('./containers/profile/NightlyPage'); const NightlyPage = React.lazy(NightlyPagePromise); -const WalletPromise = () => import('./containers/wallet/Wallet'); -const Wallet = React.lazy(WalletPromise); - const MyWalletsPagePromise = () => import('./containers/wallet/MyWalletsPage'); const MyWalletsPage = React.lazy(MyWalletsPagePromise); @@ -66,9 +66,6 @@ const WalletReceivePage = React.lazy(WalletReceivePagePromise); const URILandingPagePromise = () => import('./containers/uri/URILandingPage'); const URILandingPage = React.lazy(URILandingPagePromise); -const TransferPromise = () => import('./containers/transfer/Transfer'); -const Transfer = React.lazy(TransferPromise); - const ReceivePromise = () => import('./containers/wallet/Receive'); const Receive = React.lazy(ReceivePromise); @@ -81,9 +78,6 @@ const CardanoStakingPage = React.lazy(CardanoStakingPagePromise); const NoticeBoardPagePromise = () => import('./containers/notice-board/NoticeBoardPage'); const NoticeBoardPage = React.lazy(NoticeBoardPagePromise); -const VotingPagePromise = () => import('./containers/wallet/voting/VotingPage'); -const VotingPage = React.lazy(VotingPagePromise); - const ComplexityLevelSettingsPagePromise = () => import('./containers/settings/categories/ComplexityLevelSettingsPage'); const ComplexityLevelSettingsPage = React.lazy(ComplexityLevelSettingsPagePromise); @@ -96,15 +90,6 @@ const BlockchainSettingsPage = React.lazy(BlockchainSettingsPagePromise); const WalletSwitchPromise = () => import('./containers/WalletSwitch'); const WalletSwitch = React.lazy(WalletSwitchPromise); -const StakingPagePromise = () => import('./containers/wallet/staking/StakingPage'); -const StakingPage = React.lazy(StakingPagePromise); - -const AssetsWrapperPromise = () => import('./containers/wallet/AssetsWrapper'); -const AssetsWrapper = React.lazy(AssetsWrapperPromise); - -const NFTsWrapperPromise = () => import('./containers/wallet/NFTsWrapper'); -const NFTsWrapper = React.lazy(NFTsWrapperPromise); - const TokensPageRevampPromise = () => import('./containers/wallet/TokensPageRevamp'); const TokensPageRevamp = React.lazy(TokensPageRevampPromise); @@ -117,9 +102,6 @@ const NFTsPageRevamp = React.lazy(NFTsPageRevampPromise); const NFTDetailPageRevampPromise = () => import('./containers/wallet/NFTDetailPageRevamp'); const NFTDetailPageRevamp = React.lazy(NFTDetailPageRevampPromise); -const ConnectedWebsitesPagePromise = () => import('./containers/dapp-connector/ConnectedWebsitesContainer'); -const ConnectedWebsitesPage = React.lazy(ConnectedWebsitesPagePromise); - const YoroiPalettePagePromise = () => import('./containers/experimental/YoroiPalette'); const YoroiPalettePage = React.lazy(YoroiPalettePagePromise); @@ -127,38 +109,34 @@ const YoroiThemesPagePromise = () => import('./containers/experimental/yoroiThem const YoroiThemesPage = React.lazy(YoroiThemesPagePromise); export const LazyLoadPromises: Array<() => any> = [ - WalletAddPagePromise, + AddAnotherWalletPromise, + StakingPageContentPromise, LanguageSelectionPagePromise, TermsOfUsePagePromise, UriPromptPagePromise, - SettingsPromise, GeneralSettingsPagePromise, WalletSettingsPagePromise, ExternalStorageSettingsPagePromise, OAuthDropboxPagePromise, TermsOfUseSettingsPagePromise, SupportSettingsPagePromise, - LoadingPagePromise, NightlyPagePromise, - WalletPromise, MyWalletsPagePromise, WalletSummaryPagePromise, WalletSendPagePromise, WalletAssetsPagePromise, WalletReceivePagePromise, URILandingPagePromise, - TransferPromise, + WalletTransferPagePromise, ReceivePromise, StakingDashboardPagePromise, CardanoStakingPagePromise, NoticeBoardPagePromise, - VotingPagePromise, + VotingPageContentPromise, ComplexityLevelSettingsPagePromise, ComplexityLevelPagePromise, BlockchainSettingsPagePromise, WalletSwitchPromise, - StakingPagePromise, - AssetsWrapperPromise, TokensPageRevampPromise, TokensDetailPageRevampPromise, NFTsPageRevampPromise, @@ -313,9 +291,7 @@ const WalletsSubpages = (stores, actions) => ( { - return - }} + component={(props) => } /> ( /> ) -/* eslint-enable max-len */ export function wrapSettings( settingsProps: InjectedOrGenerated, @@ -443,7 +418,9 @@ export function wrapSettings( - {children} + + {children} + ); } @@ -456,7 +433,9 @@ export function wrapAssets( - {children} + + {children} + ); } @@ -469,7 +448,9 @@ export function wrapNFTs( - {children} + + {children} + ); } @@ -482,7 +463,9 @@ export function wrapWallet( - {children} + + {children} + ); } @@ -498,4 +481,4 @@ export function wrapReceive( {children} ); -} +} \ No newline at end of file diff --git a/packages/yoroi-extension/app/containers/dapp-connector/ConnectedWebsitesContainer.js b/packages/yoroi-extension/app/containers/dapp-connector/ConnectedWebsitesContainer.js index 19da59de3f..e450565ca2 100644 --- a/packages/yoroi-extension/app/containers/dapp-connector/ConnectedWebsitesContainer.js +++ b/packages/yoroi-extension/app/containers/dapp-connector/ConnectedWebsitesContainer.js @@ -1,6 +1,6 @@ // @flow import type { Node, ComponentType } from 'react' -import { Component } from 'react' +import { Component, lazy, Suspense } from 'react' import { computed } from 'mobx' import { observer } from 'mobx-react' import type { $npm$ReactIntl$IntlFormat } from 'react-intl' @@ -14,7 +14,6 @@ import { getReceiveAddress } from '../../stores/stateless/addressStores'; import { withLayout } from '../../styles/context/layout' import type { LayoutComponentMap } from '../../styles/context/layout' import SidebarContainer from '../SidebarContainer' -import ConnectedWebsitesPage from '../../components/dapp-connector/ConnectedWebsites/ConnectedWebsitesPage' import DappConnectorNavbar from '../../components/dapp-connector/Layout/DappConnectorNavbar' import { genLookupOrFail } from '../../stores/stateless/tokenHelpers' import FullscreenLayout from '../../components/layout/FullscreenLayout' @@ -33,6 +32,9 @@ import type { WalletChecksum } from '@emurgo/cip4-js'; import type { MultiToken } from '../../api/common/lib/MultiToken' +export const ConnectedWebsitesPagePromise: void => Promise = () => import('../../components/dapp-connector/ConnectedWebsites/ConnectedWebsitesPage'); +const ConnectedWebsitesPage = lazy(ConnectedWebsitesPagePromise); + export type GeneratedData = typeof ConnectedWebsitesPageContainer.prototype.generated; type Props = InjectedOrGenerated @@ -103,16 +105,18 @@ class ConnectedWebsitesPageContainer extends Component { navbar={} > - ) + + + ); diff --git a/packages/yoroi-extension/app/containers/transfer/Transfer.js b/packages/yoroi-extension/app/containers/transfer/Transfer.js index c14748beee..5e7e70a59e 100644 --- a/packages/yoroi-extension/app/containers/transfer/Transfer.js +++ b/packages/yoroi-extension/app/containers/transfer/Transfer.js @@ -1,5 +1,5 @@ // @flow -import { Component } from 'react'; +import { Component, lazy, Suspense } from 'react'; import type { Node, ComponentType } from 'react'; import { computed } from 'mobx'; import { observer } from 'mobx-react'; @@ -15,7 +15,6 @@ import UnsupportedWallet from '../wallet/UnsupportedWallet'; import NavBarTitle from '../../components/topbar/NavBarTitle'; import NavBarContainer from '../NavBarContainer'; import globalMessages from '../../i18n/global-messages'; -import WalletTransferPage from './WalletTransferPage'; import type { GeneratedData as WalletTransferPageData } from './WalletTransferPage'; import type { GeneratedData as SidebarContainerData } from '../SidebarContainer'; import type { GeneratedData as NavBarContainerData } from '../NavBarContainer'; @@ -27,6 +26,9 @@ import { withLayout } from '../../styles/context/layout'; import type { LayoutComponentMap } from '../../styles/context/layout'; import type { GeneratedData as NavBarContainerRevampData } from '../NavBarContainerRevamp'; +export const WalletTransferPagePromise: void => Promise = () => import('./WalletTransferPage'); +const WalletTransferPage = lazy(WalletTransferPagePromise); + export type GeneratedData = typeof Transfer.prototype.generated; type Props = {| @@ -84,14 +86,17 @@ class Transfer extends Component { if (wallet.getParent().getNetworkInfo().CoinType !== CoinTypes.CARDANO) { return (); } + const isRevamp = this.generated.stores.profile.isRevampTheme; return ( <> - + {!isRevamp && } - + + + ); @@ -116,7 +121,10 @@ class Transfer extends Component { |}, stores: {| app: {| currentRoute: string |}, - wallets: {| selected: null | PublicDeriver<> |} + wallets: {| selected: null | PublicDeriver<> |}, + profile: {| + isRevampTheme: boolean, + |}, |} |} { if (this.props.generated !== undefined) { @@ -133,6 +141,9 @@ class Transfer extends Component { }, wallets: { selected: stores.wallets.selected, + }, + profile: { + isRevampTheme: stores.profile.isRevampTheme, } }, actions: { diff --git a/packages/yoroi-extension/app/containers/transfer/Transfer.mock.js b/packages/yoroi-extension/app/containers/transfer/Transfer.mock.js index 5e482b0d28..ff87ab264a 100644 --- a/packages/yoroi-extension/app/containers/transfer/Transfer.mock.js +++ b/packages/yoroi-extension/app/containers/transfer/Transfer.mock.js @@ -46,6 +46,7 @@ export const mockTransferProps: { } => {| generated: GeneratedData |} = (request) => ({ // $FlowFixMe[prop-missing]: Some props are quite different for revamp components generated: { + // $FlowFixMe[prop-missing]: Some props are quite different for revamp components stores: { app: { currentRoute: request.currentRoute, diff --git a/packages/yoroi-extension/app/containers/wallet/WalletAddPage.js b/packages/yoroi-extension/app/containers/wallet/WalletAddPage.js index 7f8f032a33..75e0ed60c4 100644 --- a/packages/yoroi-extension/app/containers/wallet/WalletAddPage.js +++ b/packages/yoroi-extension/app/containers/wallet/WalletAddPage.js @@ -1,6 +1,6 @@ // @flow import type { Node, ComponentType } from 'react'; -import { Component } from 'react'; +import { Component, lazy } from 'react'; import { observer } from 'mobx-react'; import { computed } from 'mobx'; import { intlShape } from 'react-intl'; @@ -13,7 +13,6 @@ import TopBarLayout from '../../components/layout/TopBarLayout'; import BannerContainer from '../banners/BannerContainer'; import type { GeneratedData as BannerContainerData } from '../banners/BannerContainer'; import WalletAdd from '../../components/wallet/WalletAdd'; -import AddAnotherWallet from '../../components/wallet/add/AddAnotherWallet'; import WalletCreateDialogContainer from './dialogs/WalletCreateDialogContainer'; import type { GeneratedData as WalletCreateDialogContainerData } from './dialogs/WalletCreateDialogContainer'; @@ -68,6 +67,9 @@ import NavBarRevamp from '../../components/topbar/NavBarRevamp'; import { withLayout } from '../../styles/context/layout' import type { LayoutComponentMap } from '../../styles/context/layout' +export const AddAnotherWalletPromise: void => Promise = () => import('../../components/wallet/add/AddAnotherWallet'); +const AddAnotherWallet = lazy(AddAnotherWalletPromise); + export type GeneratedData = typeof WalletAddPage.prototype.generated; type Props = InjectedOrGenerated; diff --git a/packages/yoroi-extension/app/containers/wallet/staking/StakingPage.js b/packages/yoroi-extension/app/containers/wallet/staking/StakingPage.js index 18e9d8d47d..4370b01880 100644 --- a/packages/yoroi-extension/app/containers/wallet/staking/StakingPage.js +++ b/packages/yoroi-extension/app/containers/wallet/staking/StakingPage.js @@ -1,376 +1,38 @@ // @flow -import type { ComponentType, Node } from 'react'; -import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; +import { Component, Suspense, lazy } from 'react'; +import type { Node } from 'react'; import type { GeneratedData as BannerContainerData } from '../../banners/BannerContainer'; import type { InjectedOrGenerated } from '../../../types/injectedPropsType'; import type { GeneratedData as SidebarContainerData } from '../../SidebarContainer'; import type { GeneratedData as NavBarContainerRevampData } from '../../NavBarContainerRevamp'; -import type { LayoutComponentMap } from '../../../styles/context/layout'; import type { ConfigType } from '../../../../config/config-types'; -import type { TxRequests } from '../../../stores/toplevel/TransactionsStore'; -import type { DelegationRequests, PoolMeta } from '../../../stores/toplevel/DelegationStore'; -import type { GeneratedData as UnmangleTxDialogContainerData } from '../../transfer/UnmangleTxDialogContainer'; -import type { GeneratedData as DeregisterDialogContainerData } from '../../transfer/DeregisterDialogContainer'; -import type { TokenInfoMap } from '../../../stores/toplevel/TokenInfoStore'; -import type { NetworkRow } from '../../../api/ada/lib/storage/database/primitives/tables'; -import type { UnitOfAccountSettingType } from '../../../types/unitOfAccountType'; -import type { AdaDelegationRequests } from '../../../stores/ada/AdaDelegationStore'; -import type { GeneratedData as WithdrawalTxDialogContainerData } from '../../transfer/WithdrawalTxDialogContainer'; -import type { PoolRequest } from '../../../api/jormungandr/lib/storage/bridge/delegationUtils'; -import type { TokenEntry } from '../../../api/common/lib/MultiToken'; -import type { - CurrentTimeRequests, - TimeCalcRequests, -} from '../../../stores/base/BaseCardanoTimeStore'; - -import { Component } from 'react'; +import { intlShape } from 'react-intl'; +import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import { computed } from 'mobx'; import { observer } from 'mobx-react'; -import { intlShape } from 'react-intl'; -import moment from 'moment'; - import globalMessages from '../../../i18n/global-messages'; import BannerContainer from '../../banners/BannerContainer'; import SidebarContainer from '../../SidebarContainer'; import NavBarContainerRevamp from '../../NavBarContainerRevamp'; import TopBarLayout from '../../../components/layout/TopBarLayout'; import NavBarTitle from '../../../components/topbar/NavBarTitle'; -import { PublicDeriver } from '../../../api/ada/lib/storage/models/PublicDeriver/index'; -import { withLayout } from '../../../styles/context/layout'; -import WalletEmptyBanner from '../WalletEmptyBanner'; -import BuySellDialog from '../../../components/buySell/BuySellDialog'; -import CardanoStakingPage from './CardanoStakingPage'; -import { Box, styled } from '@mui/system'; -import SummaryCard from '../../../components/wallet/staking/dashboard-revamp/SummaryCard'; -import EpochProgressWrapper from '../../../components/wallet/staking/dashboard-revamp/EpochProgressWrapper'; -import OverviewModal from '../../../components/wallet/staking/dashboard-revamp/OverviewDialog'; -import LocalizableError from '../../../i18n/LocalizableError'; -import { MultiToken } from '../../../api/common/lib/MultiToken'; -import { genLookupOrFail } from '../../../stores/stateless/tokenHelpers'; -import UnmangleTxDialogContainer from '../../transfer/UnmangleTxDialogContainer'; -import DeregisterDialogContainer from '../../transfer/DeregisterDialogContainer'; -import { calculateAndFormatValue } from '../../../utils/unit-of-account'; -import { - isCardanoHaskell, - isJormungandr, -} from '../../../api/ada/lib/storage/database/prepackaged/networks'; -import EpochProgressContainer from './EpochProgressContainer'; -import WithdrawalTxDialogContainer from '../../transfer/WithdrawalTxDialogContainer'; -import UndelegateDialog from '../../../components/wallet/staking/dashboard/UndelegateDialog'; -import { generateGraphData } from '../../../utils/graph'; -import { ApiOptions, getApiForNetwork } from '../../../api/common/utils'; -import RewardHistoryDialog from '../../../components/wallet/staking/dashboard-revamp/RewardHistoryDialog'; -import DelegatedStakePoolCard from '../../../components/wallet/staking/dashboard-revamp/DelegatedStakePoolCard'; + +export const StakingPageContentPromise: void => Promise = () => import('./StakingPageContent'); +const StakingPageContent = lazy(StakingPageContentPromise); export type GeneratedData = typeof StakingPage.prototype.generated; // populated by ConfigWebpackPlugin declare var CONFIG: ConfigType; -type Props = {| - ...InjectedOrGenerated, - actions: any, - stores: any, -|}; -type InjectedProps = {| - +renderLayoutComponent: LayoutComponentMap => Node, -|}; +type Props = {| ...InjectedOrGenerated, stores: any, actions: any |}; -type AllProps = {| ...Props, ...InjectedProps |}; @observer -class StakingPage extends Component { +class StakingPage extends Component { static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { intl: intlShape.isRequired, }; - onClose: void => void = () => { - this.generated.actions.dialogs.closeActiveDialog.trigger(); - }; - - _isRegistered: (PublicDeriver<>) => ?boolean = publicDeriver => { - if (!isCardanoHaskell(publicDeriver.getParent().getNetworkInfo())) { - return undefined; - } - const adaDelegationRequests = this.generated.stores.substores.ada.delegation.getDelegationRequests( - publicDeriver - ); - if (adaDelegationRequests == null) return undefined; - return adaDelegationRequests.getRegistrationHistory.result?.current; - }; - - async componentDidMount() { - const timeStore = this.generated.stores.time; - const publicDeriver = this.generated.stores.wallets.selected; - if (publicDeriver == null) { - throw new Error(`${nameof(StakingPage)} no public deriver. Should never happen`); - } - const timeCalcRequests = timeStore.getTimeCalcRequests(publicDeriver); - await timeCalcRequests.requests.toAbsoluteSlot.execute().promise; - await timeCalcRequests.requests.toRealTime.execute().promise; - await timeCalcRequests.requests.currentEpochLength.execute().promise; - await timeCalcRequests.requests.currentSlotLength.execute().promise; - await timeCalcRequests.requests.timeSinceGenesis.execute().promise; - } - - getErrorInFetch: (PublicDeriver<>) => void | {| error: LocalizableError |} = publicDeriver => { - const delegationStore = this.generated.stores.delegation; - const delegationRequests = delegationStore.getDelegationRequests(publicDeriver); - if (delegationRequests == null) { - throw new Error(`${nameof(StakingPage)} opened for non-reward wallet`); - } - if (delegationRequests.error != null) { - return { error: delegationRequests.error }; - } - if (delegationRequests.getCurrentDelegation.result != null) { - const currentDelegation = delegationRequests.getCurrentDelegation.result; - const currEpochInfo = currentDelegation.currEpoch; - if (currEpochInfo == null) { - return undefined; - } - } - return undefined; - }; - - getEpochLengthInDays: (PublicDeriver<>) => ?number = publicDeriver => { - const timeStore = this.generated.stores.time; - const timeCalcRequests = timeStore.getTimeCalcRequests(publicDeriver); - const getEpochLength = timeCalcRequests.requests.currentEpochLength.result; - if (getEpochLength == null) return null; - - const getSlotLength = timeCalcRequests.requests.currentSlotLength.result; - if (getSlotLength == null) return null; - - const epochLengthInSeconds = getEpochLength() * getSlotLength(); - const epochLengthInDays = epochLengthInSeconds / (60 * 60 * 24); - return epochLengthInDays; - }; - - getUserSummary: ({| - delegationRequests: DelegationRequests, - publicDeriver: PublicDeriver<>, - errorIfPresent: void | {| error: LocalizableError |}, - |}) => Node = request => { - const { actions, stores } = this.generated; - - const showRewardAmount = - request.delegationRequests.getCurrentDelegation.wasExecuted && - request.delegationRequests.getDelegatedBalance.wasExecuted && - request.errorIfPresent == null; - - const defaultToken = request.publicDeriver.getParent().getDefaultToken(); - - const currentlyDelegating = - (request.delegationRequests.getCurrentDelegation.result?.currEpoch?.pools ?? []).length > 0; - - return ( - - actions.dialogs.open.trigger({ - dialog: OverviewModal, - }) - } - unitOfAccount={this.toUnitOfAccount} - getTokenInfo={genLookupOrFail(stores.tokenInfoStore.tokenInfo)} - shouldHideBalance={stores.profile.shouldHideBalance} - totalRewards={ - !showRewardAmount || request.delegationRequests.getDelegatedBalance.result == null - ? undefined - : request.delegationRequests.getDelegatedBalance.result.accountPart - } - totalDelegated={(() => { - if (!showRewardAmount) return undefined; - if (request.delegationRequests.getDelegatedBalance.result == null) return undefined; - - return currentlyDelegating - ? request.delegationRequests.getDelegatedBalance.result.utxoPart.joinAddCopy( - request.delegationRequests.getDelegatedBalance.result.accountPart - ) - : new MultiToken([], defaultToken); - })()} - epochLength={this.getEpochLengthInDays(request.publicDeriver)} - graphData={generateGraphData({ - delegationRequests: request.delegationRequests, - publicDeriver: request.publicDeriver, - currentEpoch: stores.time.getCurrentTimeRequests(request.publicDeriver).currentEpoch, - shouldHideBalance: stores.profile.shouldHideBalance, - getLocalPoolInfo: stores.delegation.getLocalPoolInfo, - tokenInfo: stores.tokenInfoStore.tokenInfo, - })} - onOpenRewardList={() => - actions.dialogs.open.trigger({ - dialog: RewardHistoryDialog, - }) - } - /> - ); - }; - - getStakePoolMeta: (PublicDeriver<>) => Node = publicDeriver => { - const delegationStore = this.generated.stores.delegation; - const delegationRequests = delegationStore.getDelegationRequests(publicDeriver); - if (delegationRequests == null) { - throw new Error(`${nameof(StakingPage)} opened for non-reward wallet`); - } - if ( - !delegationRequests.getCurrentDelegation.wasExecuted || - delegationRequests.getCurrentDelegation.isExecuting || - delegationRequests.getCurrentDelegation.result == null - ) { - return null; - } - if (delegationRequests.getCurrentDelegation.result.currEpoch == null) { - return null; - } - const currentPools = delegationRequests.getCurrentDelegation.result.currEpoch.pools; - const currentPage = this.generated.stores.delegation.selectedPage; - - const currentPool = currentPools[0][currentPage]; - const meta = this.generated.stores.delegation.getLocalPoolInfo( - publicDeriver.getParent().getNetworkInfo(), - String(currentPool) - ); - if (meta == null) { - // server hasn't returned information about the stake pool yet - return null; - } - const { intl } = this.context; - const name = meta.info?.name ?? intl.formatMessage(globalMessages.unknownPoolLabel); - // TODO: remove placeholders - const delegatedPool = { - id: String(currentPool), - name, - roa: '5.1', - poolSize: 2560000, - share: '0.3', - websiteUrl: meta.info?.homepage, - ticker: meta.info?.ticker, - }; - - // TODO: implement this eventually - // const stakePoolMeta = { - // avatar: '', - // websiteUrl: '', - // roa: ' 5.08%', - // socialLinks: { - // fb: '', - // tw: '', - // }, - // }; - - // don't support undelegation for ratio stake since it's a less intuitive UX - const undelegate = - currentPools.length === 1 && isJormungandr(publicDeriver.getParent().getNetworkInfo()) - ? async () => { - this.generated.actions.dialogs.open.trigger({ dialog: UndelegateDialog }); - await this.generated.actions.jormungandr.delegationTransaction.createTransaction.trigger( - { - publicDeriver, - poolRequest: undefined, - } - ); - } - : undefined; - - return ( - - ); - }; - - getEpochProgress: (PublicDeriver<>) => Node | void = publicDeriver => { - const timeStore = this.generated.stores.time; - const timeCalcRequests = timeStore.getTimeCalcRequests(publicDeriver); - const currTimeRequests = timeStore.getCurrentTimeRequests(publicDeriver); - const toAbsoluteSlot = timeCalcRequests.requests.toAbsoluteSlot.result; - if (toAbsoluteSlot == null) return undefined; - const toRealTime = timeCalcRequests.requests.toRealTime.result; - if (toRealTime == null) return undefined; - const timeSinceGenesis = timeCalcRequests.requests.timeSinceGenesis.result; - if (timeSinceGenesis == null) return undefined; - const getEpochLength = timeCalcRequests.requests.currentEpochLength.result; - if (getEpochLength == null) return undefined; - const currentEpoch = currTimeRequests.currentEpoch; - const epochLength = getEpochLength(); - - const getDateFromEpoch = epoch => { - const epochTime = toRealTime({ - absoluteSlotNum: toAbsoluteSlot({ - epoch, - // in Jormungandr, rewards were distributed at the start of the epoch - // in Haskell, rewards are calculated at the start of the epoch but distributed at the end - slot: isJormungandr(publicDeriver.getParent().getNetworkInfo()) ? 0 : getEpochLength(), - }), - timeSinceGenesisFunc: timeSinceGenesis, - }); - const epochMoment = moment(epochTime).format('lll'); - return epochMoment; - }; - - const endEpochDate = getDateFromEpoch(currentEpoch); - const previousEpochDate = getDateFromEpoch(currentEpoch - 1); - - return ( - - ); - }; - - toUnitOfAccount: TokenEntry => void | {| currency: string, amount: string |} = entry => { - const { stores } = this.generated; - const tokenRow = stores.tokenInfoStore.tokenInfo - .get(entry.networkId.toString()) - ?.get(entry.identifier); - if (tokenRow == null) return undefined; - - if (!stores.profile.unitOfAccount.enabled) return undefined; - const currency = stores.profile.unitOfAccount.currency; - - const shiftedAmount = entry.amount.shiftedBy(-tokenRow.Metadata.numberOfDecimals); - const ticker = tokenRow.Metadata.ticker; - if (ticker == null) { - throw new Error('unexpected main token type'); - } - const coinPrice = stores.coinPriceStore.getCurrentPrice(ticker, currency); - if (coinPrice == null) return { currency, amount: '-' }; - return { - currency, - amount: calculateAndFormatValue(shiftedAmount, coinPrice), - }; - }; - render(): Node { const sidebarContainer = ; - const publicDeriver = this.generated.stores.wallets.selected; - if (publicDeriver == null) { - throw new Error(`${nameof(StakingPage)} no public deriver. Should never happen`); - } - const { stores } = this.generated; - const { uiDialogs, delegation: delegationStore } = stores; - const delegationRequests = delegationStore.getDelegationRequests(publicDeriver); - if (delegationRequests == null) { - throw new Error(`${nameof(StakingPage)} opened for non-reward wallet`); - } - const txRequests = stores.transactions.getTxRequests(publicDeriver); - const balance = txRequests.requests.getBalanceRequest.result; - const isWalletWithNoFunds = balance != null && balance.getDefaultEntry().amount.isZero(); - - const errorIfPresent = this.getErrorInFetch(publicDeriver); - - const showRewardAmount = - delegationRequests.getCurrentDelegation.wasExecuted && - delegationRequests.getDelegatedBalance.wasExecuted && - errorIfPresent == null; - - const delegationHistory = delegationRequests.getCurrentDelegation.result?.fullHistory; - const hasNeverDelegated = delegationHistory != null && delegationHistory.length === 0; - return ( } @@ -388,100 +50,12 @@ class StakingPage extends Component { showInContainer showAsCard > - - {isWalletWithNoFunds ? ( - - this.generated.actions.dialogs.open.trigger({ dialog: BuySellDialog }) - } - /> - ) : null} - {hasNeverDelegated ? null : ( - - {this.getUserSummary({ delegationRequests, publicDeriver, errorIfPresent })} - - {errorIfPresent} - {!errorIfPresent && this.getStakePoolMeta(publicDeriver)} - {!errorIfPresent && this.getEpochProgress(publicDeriver)} - - - )} - + - {uiDialogs.isOpen(OverviewModal) ? ( - { - this.generated.actions.dialogs.open.trigger({ - dialog: DeregisterDialogContainer, - }); - } - : undefined - } - /> - ) : null} - {uiDialogs.isOpen(DeregisterDialogContainer) ? ( - { - // note: purposely don't await - // since the next dialog will properly render the spinner - this.generated.actions.ada.delegationTransaction.createWithdrawalTxForWallet.trigger( - { publicDeriver } - ); - this.generated.actions.dialogs.open.trigger({ - dialog: WithdrawalTxDialogContainer, - }); - }} - /> - ) : null} - {uiDialogs.isOpen(UnmangleTxDialogContainer) ? ( - - ) : null} - {uiDialogs.isOpen(WithdrawalTxDialogContainer) ? ( - { - this.generated.actions.ada.delegationTransaction.reset.trigger({ - justTransaction: false, - }); - this.generated.actions.dialogs.closeActiveDialog.trigger(); - }} - /> - ) : null} - {uiDialogs.isOpen(RewardHistoryDialog) ? ( - - ) : null} - + ); } @@ -490,86 +64,6 @@ class StakingPage extends Component { BannerContainerProps: InjectedOrGenerated, NavBarContainerRevampProps: InjectedOrGenerated, SidebarContainerProps: InjectedOrGenerated, - DeregisterDialogContainerProps: InjectedOrGenerated, - UnmangleTxDialogContainerProps: InjectedOrGenerated, - WithdrawalTxDialogContainerProps: InjectedOrGenerated, - actions: {| - ada: {| - delegationTransaction: {| - reset: {| trigger: (params: {| justTransaction: boolean |}) => void |}, - createWithdrawalTxForWallet: {| - trigger: (params: {| publicDeriver: PublicDeriver<> |}) => Promise, - |}, - |}, - |}, - jormungandr: {| - delegationTransaction: {| - createTransaction: {| - trigger: (params: {| - poolRequest: PoolRequest, - publicDeriver: PublicDeriver<>, - |}) => Promise, - |}, - |}, - |}, - dialogs: {| - open: {| - trigger: (params: {| - dialog: any, - params?: any, - |}) => void, - |}, - closeActiveDialog: {| - trigger: (params: void) => void, - |}, - |}, - transactions: {| - closeWalletEmptyBanner: {| - trigger: (params: void) => void, - |}, - closeDelegationBanner: {| - trigger: (params: void) => void, - |}, - |}, - |}, - stores: {| - uiDialogs: {| - getParam: (number | string) => T, - isOpen: any => boolean, - |}, - coinPriceStore: {| - getCurrentPrice: (from: string, to: string) => ?string, - |}, - substores: {| - ada: {| - delegation: {| - getDelegationRequests: (PublicDeriver<>) => void | AdaDelegationRequests, - |}, - |}, - |}, - delegation: {| - selectedPage: number, - getLocalPoolInfo: ($ReadOnly, string) => void | PoolMeta, - getDelegationRequests: (PublicDeriver<>) => void | DelegationRequests, - |}, - profile: {| - shouldHideBalance: boolean, - unitOfAccount: UnitOfAccountSettingType, - |}, - tokenInfoStore: {| - tokenInfo: TokenInfoMap, - |}, - wallets: {| selected: null | PublicDeriver<> |}, - transactions: {| - showWalletEmptyBanner: boolean, - showDelegationBanner: boolean, - getTxRequests: (PublicDeriver<>) => TxRequests, - |}, - time: {| - getCurrentTimeRequests: (PublicDeriver<>) => CurrentTimeRequests, - getTimeCalcRequests: (PublicDeriver<>) => TimeCalcRequests, - |}, - |}, |} { if (this.props.generated !== undefined) { return this.props.generated; @@ -577,139 +71,17 @@ class StakingPage extends Component { if (this.props.stores == null || this.props.actions == null) { throw new Error(`${nameof(StakingPage)} no way to generated props`); } + const { stores, actions } = this.props; - const selected = stores.wallets.selected; - if (selected == null) { - throw new Error(`${nameof(EpochProgressContainer)} no wallet selected`); - } - const api = getApiForNetwork(selected.getParent().getNetworkInfo()); - const time = (() => { - if (api === ApiOptions.ada) { - return { - getTimeCalcRequests: stores.substores.ada.time.getTimeCalcRequests, - getCurrentTimeRequests: stores.substores.ada.time.getCurrentTimeRequests, - }; - } - if (api === ApiOptions.jormungandr) { - return { - getTimeCalcRequests: stores.substores.jormungandr.time.getTimeCalcRequests, - getCurrentTimeRequests: stores.substores.jormungandr.time.getCurrentTimeRequests, - }; - } - return { - getTimeCalcRequests: (undefined: any), - getCurrentTimeRequests: () => { - throw new Error(`${nameof(StakingPage)} api not supported`); - }, - }; - })(); return Object.freeze({ - stores: { - wallets: { - selected: stores.wallets.selected, - }, - profile: { - shouldHideBalance: stores.profile.shouldHideBalance, - unitOfAccount: stores.profile.unitOfAccount, - }, - delegation: { - selectedPage: stores.delegation.selectedPage, - getLocalPoolInfo: stores.delegation.getLocalPoolInfo, - getDelegationRequests: stores.delegation.getDelegationRequests, - }, - uiDialogs: { - isOpen: stores.uiDialogs.isOpen, - getParam: stores.uiDialogs.getParam, - }, - transactions: { - showWalletEmptyBanner: stores.transactions.showWalletEmptyBanner, - showDelegationBanner: stores.transactions.showDelegationBanner, - getTxRequests: stores.transactions.getTxRequests, - }, - tokenInfoStore: { - tokenInfo: stores.tokenInfoStore.tokenInfo, - }, - coinPriceStore: { - getCurrentPrice: stores.coinPriceStore.getCurrentPrice, - }, - substores: { - ada: { - delegation: { - getDelegationRequests: stores.substores.ada.delegation.getDelegationRequests, - }, - }, - }, - time, - }, - actions: { - ada: { - delegationTransaction: { - reset: { - trigger: actions.ada.delegationTransaction.reset.trigger, - }, - createWithdrawalTxForWallet: { - trigger: actions.ada.delegationTransaction.createWithdrawalTxForWallet.trigger, - }, - }, - }, - jormungandr: { - delegationTransaction: { - createTransaction: { - trigger: actions.jormungandr.delegationTransaction.createTransaction.trigger, - }, - }, - }, - transactions: { - closeWalletEmptyBanner: { - trigger: actions.transactions.closeWalletEmptyBanner.trigger, - }, - closeDelegationBanner: { - trigger: actions.transactions.closeDelegationBanner.trigger, - }, - }, - dialogs: { - open: { - trigger: actions.dialogs.open.trigger, - }, - closeActiveDialog: { trigger: actions.dialogs.closeActiveDialog.trigger }, - }, - }, SidebarContainerProps: ({ actions, stores }: InjectedOrGenerated), NavBarContainerRevampProps: ({ actions, stores, }: InjectedOrGenerated), BannerContainerProps: ({ actions, stores }: InjectedOrGenerated), - UnmangleTxDialogContainerProps: ({ - stores, - actions, - }: InjectedOrGenerated), - WithdrawalTxDialogContainerProps: ({ - stores, - actions, - }: InjectedOrGenerated), - DeregisterDialogContainerProps: ({ - stores, - actions, - }: InjectedOrGenerated), }); } } -export default (withLayout(StakingPage): ComponentType); - -const WrapperCards = styled(Box)({ - display: 'flex', - gap: '40px', - justifyContent: 'space-between', - marginBottom: '40px', - height: '556px', -}); - -const RightCardsWrapper = styled(Box)({ - display: 'flex', - flex: '1 1 48.5%', - maxWidth: '48.5%', - flexDirection: 'column', - gap: '40px', -}); +export default StakingPage; \ No newline at end of file diff --git a/packages/yoroi-extension/app/containers/wallet/staking/StakingPageContent.js b/packages/yoroi-extension/app/containers/wallet/staking/StakingPageContent.js new file mode 100644 index 0000000000..b0a048408d --- /dev/null +++ b/packages/yoroi-extension/app/containers/wallet/staking/StakingPageContent.js @@ -0,0 +1,679 @@ +// @flow +import type { ComponentType, Node } from 'react'; +import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; +import type { InjectedOrGenerated } from '../../../types/injectedPropsType'; +import type { LayoutComponentMap } from '../../../styles/context/layout'; +import type { ConfigType } from '../../../../config/config-types'; +import type { TxRequests } from '../../../stores/toplevel/TransactionsStore'; +import type { DelegationRequests, PoolMeta } from '../../../stores/toplevel/DelegationStore'; +import type { GeneratedData as UnmangleTxDialogContainerData } from '../../transfer/UnmangleTxDialogContainer'; +import type { GeneratedData as DeregisterDialogContainerData } from '../../transfer/DeregisterDialogContainer'; +import type { TokenInfoMap } from '../../../stores/toplevel/TokenInfoStore'; +import type { NetworkRow } from '../../../api/ada/lib/storage/database/primitives/tables'; +import type { UnitOfAccountSettingType } from '../../../types/unitOfAccountType'; +import type { AdaDelegationRequests } from '../../../stores/ada/AdaDelegationStore'; +import type { GeneratedData as WithdrawalTxDialogContainerData } from '../../transfer/WithdrawalTxDialogContainer'; +import type { PoolRequest } from '../../../api/jormungandr/lib/storage/bridge/delegationUtils'; +import type { TokenEntry } from '../../../api/common/lib/MultiToken'; +import type { + CurrentTimeRequests, + TimeCalcRequests, +} from '../../../stores/base/BaseCardanoTimeStore'; + +import { Component } from 'react'; +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import { intlShape } from 'react-intl'; +import moment from 'moment'; + +import globalMessages from '../../../i18n/global-messages'; +import { PublicDeriver } from '../../../api/ada/lib/storage/models/PublicDeriver/index'; +import { withLayout } from '../../../styles/context/layout'; +import WalletEmptyBanner from '../WalletEmptyBanner'; +import BuySellDialog from '../../../components/buySell/BuySellDialog'; +import CardanoStakingPage from './CardanoStakingPage'; +import { Box, styled } from '@mui/system'; +import SummaryCard from '../../../components/wallet/staking/dashboard-revamp/SummaryCard'; +import EpochProgressWrapper from '../../../components/wallet/staking/dashboard-revamp/EpochProgressWrapper'; +import OverviewModal from '../../../components/wallet/staking/dashboard-revamp/OverviewDialog'; +import LocalizableError from '../../../i18n/LocalizableError'; +import { MultiToken } from '../../../api/common/lib/MultiToken'; +import { genLookupOrFail } from '../../../stores/stateless/tokenHelpers'; +import UnmangleTxDialogContainer from '../../transfer/UnmangleTxDialogContainer'; +import DeregisterDialogContainer from '../../transfer/DeregisterDialogContainer'; +import { calculateAndFormatValue } from '../../../utils/unit-of-account'; +import { + isCardanoHaskell, + isJormungandr, +} from '../../../api/ada/lib/storage/database/prepackaged/networks'; +import EpochProgressContainer from './EpochProgressContainer'; +import WithdrawalTxDialogContainer from '../../transfer/WithdrawalTxDialogContainer'; +import UndelegateDialog from '../../../components/wallet/staking/dashboard/UndelegateDialog'; +import { generateGraphData } from '../../../utils/graph'; +import { ApiOptions, getApiForNetwork } from '../../../api/common/utils'; +import RewardHistoryDialog from '../../../components/wallet/staking/dashboard-revamp/RewardHistoryDialog'; +import DelegatedStakePoolCard from '../../../components/wallet/staking/dashboard-revamp/DelegatedStakePoolCard'; + +export type GeneratedData = typeof StakingPageContent.prototype.generated; +// populated by ConfigWebpackPlugin +declare var CONFIG: ConfigType; +type Props = {| + ...InjectedOrGenerated, + actions: any, + stores: any, +|}; +type InjectedProps = {| + +renderLayoutComponent: LayoutComponentMap => Node, +|}; + +type AllProps = {| ...Props, ...InjectedProps |}; +@observer +class StakingPageContent extends Component { + static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { + intl: intlShape.isRequired, + }; + + onClose: void => void = () => { + this.generated.actions.dialogs.closeActiveDialog.trigger(); + }; + + _isRegistered: (PublicDeriver<>) => ?boolean = publicDeriver => { + if (!isCardanoHaskell(publicDeriver.getParent().getNetworkInfo())) { + return undefined; + } + const delegation = this.generated.stores.substores.ada.delegation; + const adaDelegationRequests = delegation.getDelegationRequests(publicDeriver); + if (adaDelegationRequests == null) return undefined; + return adaDelegationRequests.getRegistrationHistory.result?.current; + }; + + async componentDidMount() { + const timeStore = this.generated.stores.time; + const publicDeriver = this.generated.stores.wallets.selected; + if (publicDeriver == null) { + throw new Error(`${nameof(StakingPageContent)} no public deriver. Should never happen`); + } + const timeCalcRequests = timeStore.getTimeCalcRequests(publicDeriver); + await timeCalcRequests.requests.toAbsoluteSlot.execute().promise; + await timeCalcRequests.requests.toRealTime.execute().promise; + await timeCalcRequests.requests.currentEpochLength.execute().promise; + await timeCalcRequests.requests.currentSlotLength.execute().promise; + await timeCalcRequests.requests.timeSinceGenesis.execute().promise; + } + + getErrorInFetch: (PublicDeriver<>) => void | {| error: LocalizableError |} = publicDeriver => { + const delegationStore = this.generated.stores.delegation; + const delegationRequests = delegationStore.getDelegationRequests(publicDeriver); + if (delegationRequests == null) { + throw new Error(`${nameof(StakingPageContent)} opened for non-reward wallet`); + } + if (delegationRequests.error != null) { + return { error: delegationRequests.error }; + } + if (delegationRequests.getCurrentDelegation.result != null) { + const currentDelegation = delegationRequests.getCurrentDelegation.result; + const currEpochInfo = currentDelegation.currEpoch; + if (currEpochInfo == null) { + return undefined; + } + } + return undefined; + }; + + getEpochLengthInDays: (PublicDeriver<>) => ?number = publicDeriver => { + const timeStore = this.generated.stores.time; + const timeCalcRequests = timeStore.getTimeCalcRequests(publicDeriver); + const getEpochLength = timeCalcRequests.requests.currentEpochLength.result; + if (getEpochLength == null) return null; + + const getSlotLength = timeCalcRequests.requests.currentSlotLength.result; + if (getSlotLength == null) return null; + + const epochLengthInSeconds = getEpochLength() * getSlotLength(); + const epochLengthInDays = epochLengthInSeconds / (60 * 60 * 24); + return epochLengthInDays; + }; + + getUserSummary: ({| + delegationRequests: DelegationRequests, + publicDeriver: PublicDeriver<>, + errorIfPresent: void | {| error: LocalizableError |}, + |}) => Node = request => { + const { actions, stores } = this.generated; + + const showRewardAmount = + request.delegationRequests.getCurrentDelegation.wasExecuted && + request.delegationRequests.getDelegatedBalance.wasExecuted && + request.errorIfPresent == null; + + const defaultToken = request.publicDeriver.getParent().getDefaultToken(); + + const currentlyDelegating = + (request.delegationRequests.getCurrentDelegation.result?.currEpoch?.pools ?? []).length > 0; + + return ( + + actions.dialogs.open.trigger({ + dialog: OverviewModal, + }) + } + unitOfAccount={this.toUnitOfAccount} + getTokenInfo={genLookupOrFail(stores.tokenInfoStore.tokenInfo)} + shouldHideBalance={stores.profile.shouldHideBalance} + totalRewards={ + !showRewardAmount || request.delegationRequests.getDelegatedBalance.result == null + ? undefined + : request.delegationRequests.getDelegatedBalance.result.accountPart + } + totalDelegated={(() => { + if (!showRewardAmount) return undefined; + if (request.delegationRequests.getDelegatedBalance.result == null) return undefined; + + return currentlyDelegating + ? request.delegationRequests.getDelegatedBalance.result.utxoPart.joinAddCopy( + request.delegationRequests.getDelegatedBalance.result.accountPart + ) + : new MultiToken([], defaultToken); + })()} + epochLength={this.getEpochLengthInDays(request.publicDeriver)} + graphData={generateGraphData({ + delegationRequests: request.delegationRequests, + publicDeriver: request.publicDeriver, + currentEpoch: stores.time.getCurrentTimeRequests(request.publicDeriver).currentEpoch, + shouldHideBalance: stores.profile.shouldHideBalance, + getLocalPoolInfo: stores.delegation.getLocalPoolInfo, + tokenInfo: stores.tokenInfoStore.tokenInfo, + })} + onOpenRewardList={() => + actions.dialogs.open.trigger({ + dialog: RewardHistoryDialog, + }) + } + /> + ); + }; + + getStakePoolMeta: (PublicDeriver<>) => Node = publicDeriver => { + const delegationStore = this.generated.stores.delegation; + const delegationRequests = delegationStore.getDelegationRequests(publicDeriver); + if (delegationRequests == null) { + throw new Error(`${nameof(StakingPageContent)} opened for non-reward wallet`); + } + if ( + !delegationRequests.getCurrentDelegation.wasExecuted || + delegationRequests.getCurrentDelegation.isExecuting || + delegationRequests.getCurrentDelegation.result == null + ) { + return null; + } + if (delegationRequests.getCurrentDelegation.result.currEpoch == null) { + return null; + } + const currentPools = delegationRequests.getCurrentDelegation.result.currEpoch.pools; + const currentPage = this.generated.stores.delegation.selectedPage; + + const currentPool = currentPools[0][currentPage]; + const meta = this.generated.stores.delegation.getLocalPoolInfo( + publicDeriver.getParent().getNetworkInfo(), + String(currentPool) + ); + if (meta == null) { + // server hasn't returned information about the stake pool yet + return null; + } + const { intl } = this.context; + const name = meta.info?.name ?? intl.formatMessage(globalMessages.unknownPoolLabel); + // TODO: remove placeholders + const delegatedPool = { + id: String(currentPool), + name, + roa: '5.1', + poolSize: 2560000, + share: '0.3', + websiteUrl: meta.info?.homepage, + ticker: meta.info?.ticker, + }; + + // TODO: implement this eventually + // const stakePoolMeta = { + // avatar: '', + // websiteUrl: '', + // roa: ' 5.08%', + // socialLinks: { + // fb: '', + // tw: '', + // }, + // }; + + // don't support undelegation for ratio stake since it's a less intuitive UX + const undelegate = + currentPools.length === 1 && isJormungandr(publicDeriver.getParent().getNetworkInfo()) + ? async () => { + this.generated.actions.dialogs.open.trigger({ dialog: UndelegateDialog }); + const delegationTransaction = this.generated.actions.jormungandr.delegationTransaction; + await delegationTransaction.createTransaction.trigger( + { + publicDeriver, + poolRequest: undefined, + } + ); + } + : undefined; + + return ( + + ); + }; + + getEpochProgress: (PublicDeriver<>) => Node | void = publicDeriver => { + const timeStore = this.generated.stores.time; + const timeCalcRequests = timeStore.getTimeCalcRequests(publicDeriver); + const currTimeRequests = timeStore.getCurrentTimeRequests(publicDeriver); + const toAbsoluteSlot = timeCalcRequests.requests.toAbsoluteSlot.result; + if (toAbsoluteSlot == null) return undefined; + const toRealTime = timeCalcRequests.requests.toRealTime.result; + if (toRealTime == null) return undefined; + const timeSinceGenesis = timeCalcRequests.requests.timeSinceGenesis.result; + if (timeSinceGenesis == null) return undefined; + const getEpochLength = timeCalcRequests.requests.currentEpochLength.result; + if (getEpochLength == null) return undefined; + const currentEpoch = currTimeRequests.currentEpoch; + const epochLength = getEpochLength(); + + const getDateFromEpoch = epoch => { + const epochTime = toRealTime({ + absoluteSlotNum: toAbsoluteSlot({ + epoch, + // in Jormungandr, rewards were distributed at the start of the epoch + // in Haskell, rewards are calculated at the start of the epoch but distributed at the end + slot: isJormungandr(publicDeriver.getParent().getNetworkInfo()) ? 0 : getEpochLength(), + }), + timeSinceGenesisFunc: timeSinceGenesis, + }); + const epochMoment = moment(epochTime).format('lll'); + return epochMoment; + }; + + const endEpochDate = getDateFromEpoch(currentEpoch); + const previousEpochDate = getDateFromEpoch(currentEpoch - 1); + + return ( + + ); + }; + + toUnitOfAccount: TokenEntry => void | {| currency: string, amount: string |} = entry => { + const { stores } = this.generated; + const tokenRow = stores.tokenInfoStore.tokenInfo + .get(entry.networkId.toString()) + ?.get(entry.identifier); + if (tokenRow == null) return undefined; + + if (!stores.profile.unitOfAccount.enabled) return undefined; + const currency = stores.profile.unitOfAccount.currency; + + const shiftedAmount = entry.amount.shiftedBy(-tokenRow.Metadata.numberOfDecimals); + const ticker = tokenRow.Metadata.ticker; + if (ticker == null) { + throw new Error('unexpected main token type'); + } + const coinPrice = stores.coinPriceStore.getCurrentPrice(ticker, currency); + if (coinPrice == null) return { currency, amount: '-' }; + return { + currency, + amount: calculateAndFormatValue(shiftedAmount, coinPrice), + }; + }; + + render(): Node { + const publicDeriver = this.generated.stores.wallets.selected; + if (publicDeriver == null) { + throw new Error(`${nameof(StakingPageContent)} no public deriver. Should never happen`); + } + const { stores } = this.generated; + const { uiDialogs, delegation: delegationStore } = stores; + const delegationRequests = delegationStore.getDelegationRequests(publicDeriver); + if (delegationRequests == null) { + throw new Error(`${nameof(StakingPageContent)} opened for non-reward wallet`); + } + const txRequests = stores.transactions.getTxRequests(publicDeriver); + const balance = txRequests.requests.getBalanceRequest.result; + const isWalletWithNoFunds = balance != null && balance.getDefaultEntry().amount.isZero(); + + const errorIfPresent = this.getErrorInFetch(publicDeriver); + + const showRewardAmount = + delegationRequests.getCurrentDelegation.wasExecuted && + delegationRequests.getDelegatedBalance.wasExecuted && + errorIfPresent == null; + + const delegationHistory = delegationRequests.getCurrentDelegation.result?.fullHistory; + const hasNeverDelegated = delegationHistory != null && delegationHistory.length === 0; + + return ( + + {isWalletWithNoFunds ? ( + + this.generated.actions.dialogs.open.trigger({ dialog: BuySellDialog }) + } + /> + ) : null} + {hasNeverDelegated ? null : ( + + {this.getUserSummary({ delegationRequests, publicDeriver, errorIfPresent })} + + {errorIfPresent} + {!errorIfPresent && this.getStakePoolMeta(publicDeriver)} + {!errorIfPresent && this.getEpochProgress(publicDeriver)} + + + )} + + {uiDialogs.isOpen(OverviewModal) ? ( + { + this.generated.actions.dialogs.open.trigger({ + dialog: DeregisterDialogContainer, + }); + } + : undefined + } + /> + ) : null} + {uiDialogs.isOpen(DeregisterDialogContainer) ? ( + { + // note: purposely don't await + // since the next dialog will properly render the spinner + const { delegationTransaction } = this.generated.actions.ada; + delegationTransaction.createWithdrawalTxForWallet.trigger({ publicDeriver }); + this.generated.actions.dialogs.open.trigger({ + dialog: WithdrawalTxDialogContainer, + }); + }} + /> + ) : null} + {uiDialogs.isOpen(UnmangleTxDialogContainer) ? ( + + ) : null} + {uiDialogs.isOpen(WithdrawalTxDialogContainer) ? ( + { + this.generated.actions.ada.delegationTransaction.reset.trigger({ + justTransaction: false, + }); + this.generated.actions.dialogs.closeActiveDialog.trigger(); + }} + /> + ) : null} + {uiDialogs.isOpen(RewardHistoryDialog) ? ( + + ) : null} + + ); + } + + @computed get generated(): {| + DeregisterDialogContainerProps: InjectedOrGenerated, + UnmangleTxDialogContainerProps: InjectedOrGenerated, + WithdrawalTxDialogContainerProps: InjectedOrGenerated, + actions: {| + ada: {| + delegationTransaction: {| + reset: {| trigger: (params: {| justTransaction: boolean |}) => void |}, + createWithdrawalTxForWallet: {| + trigger: (params: {| publicDeriver: PublicDeriver<> |}) => Promise, + |}, + |}, + |}, + jormungandr: {| + delegationTransaction: {| + createTransaction: {| + trigger: (params: {| + poolRequest: PoolRequest, + publicDeriver: PublicDeriver<>, + |}) => Promise, + |}, + |}, + |}, + dialogs: {| + open: {| + trigger: (params: {| + dialog: any, + params?: any, + |}) => void, + |}, + closeActiveDialog: {| + trigger: (params: void) => void, + |}, + |}, + transactions: {| + closeWalletEmptyBanner: {| + trigger: (params: void) => void, + |}, + closeDelegationBanner: {| + trigger: (params: void) => void, + |}, + |}, + |}, + stores: {| + uiDialogs: {| + getParam: (number | string) => T, + isOpen: any => boolean, + |}, + coinPriceStore: {| + getCurrentPrice: (from: string, to: string) => ?string, + |}, + substores: {| + ada: {| + delegation: {| + getDelegationRequests: (PublicDeriver<>) => void | AdaDelegationRequests, + |}, + |}, + |}, + delegation: {| + selectedPage: number, + getLocalPoolInfo: ($ReadOnly, string) => void | PoolMeta, + getDelegationRequests: (PublicDeriver<>) => void | DelegationRequests, + |}, + profile: {| + shouldHideBalance: boolean, + unitOfAccount: UnitOfAccountSettingType, + |}, + tokenInfoStore: {| + tokenInfo: TokenInfoMap, + |}, + wallets: {| selected: null | PublicDeriver<> |}, + transactions: {| + showWalletEmptyBanner: boolean, + showDelegationBanner: boolean, + getTxRequests: (PublicDeriver<>) => TxRequests, + |}, + time: {| + getCurrentTimeRequests: (PublicDeriver<>) => CurrentTimeRequests, + getTimeCalcRequests: (PublicDeriver<>) => TimeCalcRequests, + |}, + |}, + |} { + if (this.props.generated !== undefined) { + return this.props.generated; + } + if (this.props.stores == null || this.props.actions == null) { + throw new Error(`${nameof(StakingPageContent)} no way to generated props`); + } + const { stores, actions } = this.props; + + const selected = stores.wallets.selected; + if (selected == null) { + throw new Error(`${nameof(EpochProgressContainer)} no wallet selected`); + } + const api = getApiForNetwork(selected.getParent().getNetworkInfo()); + const time = (() => { + if (api === ApiOptions.ada) { + return { + getTimeCalcRequests: stores.substores.ada.time.getTimeCalcRequests, + getCurrentTimeRequests: stores.substores.ada.time.getCurrentTimeRequests, + }; + } + if (api === ApiOptions.jormungandr) { + return { + getTimeCalcRequests: stores.substores.jormungandr.time.getTimeCalcRequests, + getCurrentTimeRequests: stores.substores.jormungandr.time.getCurrentTimeRequests, + }; + } + return { + getTimeCalcRequests: (undefined: any), + getCurrentTimeRequests: () => { + throw new Error(`${nameof(StakingPageContent)} api not supported`); + }, + }; + })(); + return Object.freeze({ + stores: { + wallets: { + selected: stores.wallets.selected, + }, + profile: { + shouldHideBalance: stores.profile.shouldHideBalance, + unitOfAccount: stores.profile.unitOfAccount, + }, + delegation: { + selectedPage: stores.delegation.selectedPage, + getLocalPoolInfo: stores.delegation.getLocalPoolInfo, + getDelegationRequests: stores.delegation.getDelegationRequests, + }, + uiDialogs: { + isOpen: stores.uiDialogs.isOpen, + getParam: stores.uiDialogs.getParam, + }, + transactions: { + showWalletEmptyBanner: stores.transactions.showWalletEmptyBanner, + showDelegationBanner: stores.transactions.showDelegationBanner, + getTxRequests: stores.transactions.getTxRequests, + }, + tokenInfoStore: { + tokenInfo: stores.tokenInfoStore.tokenInfo, + }, + coinPriceStore: { + getCurrentPrice: stores.coinPriceStore.getCurrentPrice, + }, + substores: { + ada: { + delegation: { + getDelegationRequests: stores.substores.ada.delegation.getDelegationRequests, + }, + }, + }, + time, + }, + actions: { + ada: { + delegationTransaction: { + reset: { + trigger: actions.ada.delegationTransaction.reset.trigger, + }, + createWithdrawalTxForWallet: { + trigger: actions.ada.delegationTransaction.createWithdrawalTxForWallet.trigger, + }, + }, + }, + jormungandr: { + delegationTransaction: { + createTransaction: { + trigger: actions.jormungandr.delegationTransaction.createTransaction.trigger, + }, + }, + }, + transactions: { + closeWalletEmptyBanner: { + trigger: actions.transactions.closeWalletEmptyBanner.trigger, + }, + closeDelegationBanner: { + trigger: actions.transactions.closeDelegationBanner.trigger, + }, + }, + dialogs: { + open: { + trigger: actions.dialogs.open.trigger, + }, + closeActiveDialog: { trigger: actions.dialogs.closeActiveDialog.trigger }, + }, + }, + UnmangleTxDialogContainerProps: ({ + stores, + actions, + }: InjectedOrGenerated), + WithdrawalTxDialogContainerProps: ({ + stores, + actions, + }: InjectedOrGenerated), + DeregisterDialogContainerProps: ({ + stores, + actions, + }: InjectedOrGenerated), + }); + } +} +export default (withLayout(StakingPageContent): ComponentType); + +const WrapperCards = styled(Box)({ + display: 'flex', + gap: '40px', + justifyContent: 'space-between', + marginBottom: '40px', + height: '556px', +}); + +const RightCardsWrapper = styled(Box)({ + display: 'flex', + flex: '1 1 48.5%', + maxWidth: '48.5%', + flexDirection: 'column', + gap: '40px', +}); + diff --git a/packages/yoroi-extension/app/containers/wallet/voting/VotingPage.js b/packages/yoroi-extension/app/containers/wallet/voting/VotingPage.js index db98eb5096..2aa6e7db47 100644 --- a/packages/yoroi-extension/app/containers/wallet/voting/VotingPage.js +++ b/packages/yoroi-extension/app/containers/wallet/voting/VotingPage.js @@ -1,34 +1,12 @@ // @flow -import type { Node, ComponentType } from 'react'; -import { Component } from 'react'; +import { lazy, Component, Suspense } from 'react'; +import type { Node, ComponentType, } from 'react'; import { observer } from 'mobx-react'; import { computed } from 'mobx'; -import { defineMessages, intlShape } from 'react-intl'; +import { intlShape } from 'react-intl'; import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; import type { InjectedOrGenerated } from '../../../types/injectedPropsType'; -import Voting from '../../../components/wallet/voting/Voting'; -import VotingRegistrationDialogContainer from '../dialogs/voting/VotingRegistrationDialogContainer'; -import type { GeneratedData as VotingRegistrationDialogContainerData } from '../dialogs/voting/VotingRegistrationDialogContainer'; -import { handleExternalLinkClick } from '../../../utils/routing'; -import { - WalletTypeOption, -} from '../../../api/ada/lib/storage/models/ConceptualWallet/interfaces'; -import { PublicDeriver } from '../../../api/ada/lib/storage/models/PublicDeriver/index'; -import LoadingSpinner from '../../../components/widgets/LoadingSpinner'; -import VerticallyCenteredLayout from '../../../components/layout/VerticallyCenteredLayout'; -import { CATALYST_MIN_AMOUNT, CATALYST_DISPLAYED_MIN_AMOUNT } from '../../../config/numbersConfig'; -import InsufficientFundsPage from './InsufficientFundsPage'; -import { getTokenName, genLookupOrFail } from '../../../stores/stateless/tokenHelpers'; -import type { TokenInfoMap } from '../../../stores/toplevel/TokenInfoStore'; -import environment from '../../../environment'; -import { MultiToken } from '../../../api/common/lib/MultiToken'; -import RegistrationOver from './RegistrationOver'; -import type { DelegationRequests } from '../../../stores/toplevel/DelegationStore'; -import { - isLedgerNanoWallet, - isTrezorTWallet, -} from '../../../api/ada/lib/storage/models/ConceptualWallet/index'; -import type { CatalystRoundInfoResponse } from '../../../api/ada/lib/state-fetch/types' +import type { GeneratedData as VotingPageContentProps } from './VotingPageContent'; import TopBarLayout from '../../../components/layout/TopBarLayout'; import BannerContainer from '../../banners/BannerContainer'; import SidebarContainer from '../../SidebarContainer'; @@ -41,6 +19,10 @@ import type { GeneratedData as SidebarContainerData } from '../../SidebarContain import type { GeneratedData as BannerContainerData } from '../../banners/BannerContainer' import type { GeneratedData as NavBarContainerRevampData } from '../../NavBarContainerRevamp'; +// $FlowFixMe[signature-verification-failure] +export const VotingPageContentPromise = () => import('./VotingPageContent'); +const VotingPageContent = lazy(VotingPageContentPromise); + export type GeneratedData = typeof VotingPage.prototype.generated; type Props = InjectedOrGenerated type InjectedProps = {| +renderLayoutComponent: LayoutComponentMap => Node |}; @@ -49,90 +31,21 @@ type AllProps = {| ...InjectedProps, |}; -const messages: * = defineMessages({ - mainTitle: { - id: 'wallet.registrationOver.mainTitle', - defaultMessage: '!!!Registration is now closed.', - }, - mainSubtitle: { - id: 'wallet.registrationOver.mainSubtitle', - defaultMessage: '!!!The registration period for fund {roundNumber} has ended. For more information, check the Catalyst app.', - }, - unavailableTitle: { - id: 'wallet.registrationOver.unavailableTitle', - defaultMessage: '!!!Catalyst Round information is currently unavailable.', - }, - unavailableSubtitle: { - id: 'wallet.registrationOver.unavailableSubtitle', - defaultMessage: '!!!Please check the Catalyst app for more info', - }, - earlyForRegistrationTitle: { - id: 'wallet.registrationOver.earlyForRegistrationTitle', - defaultMessage: '!!!Registration hasn\'t started yet.' - }, - earlyForRegistrationSubTitle: { - id: 'wallet.registrationOver.earlyForRegistrationSubTitle', - defaultMessage: '!!!Registration for Round {roundNumber} begins at {registrationStart}.' - }, - beforeVotingSubtitle: { - id: 'wallet.registrationOver.beforeVotingSubtitle', - defaultMessage: '!!!Registration has ended. Voting starts at {votingStart}' - }, - betweenVotingSubtitle: { - id: 'wallet.registrationOver.betweenVotingSubtitle', - defaultMessage: '!!!"Registration has ended. Voting ends at {votingEnd}' - }, - nextFundRegistration: { - id: 'wallet.registrationOver.nextFundRegistration', - defaultMessage: 'Round {roundNumber} starts at {registrationStart}' - } -}); - @observer class VotingPage extends Component { static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { intl: intlShape.isRequired }; - onClose: void => void = () => { - this.generated.actions.dialogs.closeActiveDialog.trigger(); - }; - - start: void => void = () => { - this.generated.actions.dialogs.open.trigger({ dialog: VotingRegistrationDialogContainer }); - }; - - get isDelegated(): ?boolean { - const publicDeriver = this.generated.stores.wallets.selected; - const delegationStore = this.generated.stores.delegation; - - if (!publicDeriver) { - throw new Error(`${nameof(this.isDelegated)} no public deriver. Should never happen`); - } - - const delegationRequests = delegationStore.getDelegationRequests(publicDeriver); - if (delegationRequests == null) { - throw new Error(`${nameof(this.isDelegated)} called for non-reward wallet`); - } - const currentDelegation = delegationRequests.getCurrentDelegation; - - if ( - !currentDelegation.wasExecuted || - currentDelegation.isExecuting || - currentDelegation.result == null - ) { - return undefined; - } - if( - !currentDelegation.result.currEpoch || - currentDelegation.result.currEpoch.pools.length === 0 - ) { - return false; - } - return true; - } - render(): Node { const { intl } = this.context; - const pageContent = this.getPageContent(); + + const content = ( + + + + ) + const revampLayout = ( } @@ -146,308 +59,37 @@ class VotingPage extends Component { showInContainer showAsCard > - {pageContent} + {content} ); return this.props.renderLayoutComponent({ - CLASSIC: pageContent, + CLASSIC: content, REVAMP: revampLayout, }); } - getPageContent(): Node { - const { intl } = this.context - const { - uiDialogs, - wallets: { selected }, - } = this.generated.stores; - let activeDialog = null; - if(selected == null){ - throw new Error(`${nameof(VotingPage)} no wallet selected`); - } - - const balance = this.generated.balance; - if (balance == null) { - return ( - - - - ); - } - // keep enabled on the testnet - const { catalystRoundInfo, loadingCatalystRoundInfo } = - this.generated.stores.substores.ada.votingStore; - if (loadingCatalystRoundInfo) { - return ( - - - - ); - } - if (!environment.isTest()) { - if (!catalystRoundInfo || (!catalystRoundInfo.currentFund && !catalystRoundInfo.nextFund)){ - return ( - - ); - } - const { currentFund, nextFund } = catalystRoundInfo; - const nextFundRegistrationSubtitle = intl.formatMessage(messages.nextFundRegistration, { - roundNumber: nextFund?.id, - registrationStart: nextFund?.registrationStart - }) - - const fund = { - 'id': 8, - 'name': 'Fund9', - 'registrationStart': '2021-01-27T11:00:00Z', - 'registrationEnd': '2023-08-04T11:00:00Z', - 'votingStart': '2021-08-11T11:00:00Z', - 'votingEnd': '2023-08-25T11:00:00Z', - 'votingPowerThreshold': '450' - }; - if (currentFund) { - console.log(JSON.parse(JSON.stringify(currentFund))) - const isLate = new Date() >= new Date(Date.parse(fund.registrationEnd)) - const isEarly = new Date() <= new Date(Date.parse(fund.registrationStart)) - const isBeforeVoting = new Date() <= new Date(Date.parse(fund.votingStart)) - const isAfterVoting = new Date() >= new Date(Date.parse(fund.votingEnd)) - const isBetweenVoting = !isBeforeVoting && !isAfterVoting; - - if (isEarly) { - return ( - - ); - } - - // registeration is ended -> check for voting start and end dates - if (isLate) { - if (isBeforeVoting) { - return ( - - ); - } - - if (isBetweenVoting) { - return ( - - ); - } - - if (isAfterVoting) { - /* if we after the voting date (= between funds) and no next funds - will dispaly "round is over" */ - let subtitle = intl.formatMessage(messages.mainSubtitle, { - roundNumber: currentFund.id - }) - // Check for the next funds if we are after voting - if(nextFund) { - subtitle = nextFundRegistrationSubtitle - } - return ( - - ); - } - } - } else if (nextFund) { - // No current funds -> check for next funds - return ( - - ); - } - } - - // disable the minimum on E2E tests - if (!environment.isTest() && balance.getDefaultEntry().amount.lt(CATALYST_MIN_AMOUNT)) { - const getTokenInfo = genLookupOrFail(this.generated.stores.tokenInfoStore.tokenInfo); - const tokenInfo = getTokenInfo({ - identifier: selected.getParent().getDefaultToken().defaultIdentifier, - networkId: selected.getParent().getDefaultToken().defaultNetworkId, - }); - return ; - } - - let walletType; - if ( - selected.getParent().getWalletType() !== WalletTypeOption.HARDWARE_WALLET - ) { - walletType = 'mnemonic'; - } else if (isTrezorTWallet(selected.getParent())) { - walletType = 'trezorT'; - } else if (isLedgerNanoWallet(selected.getParent())) { - walletType = 'ledgerNano'; - } else { - throw new Error(`${nameof(VotingPage)} unexpected wallet type`); - } - - if (uiDialogs.isOpen(VotingRegistrationDialogContainer)) { - activeDialog = ( - - ); - } - /* - At this point we are sure that we have current funds - I added the "5" for two reasons - 1. As a placeholder as the component will not be rendered without it. - 2. this page you can see it in test environment even if you - out of the registration dates. - */ - const round = catalystRoundInfo?.currentFund?.id || catalystRoundInfo?.nextFund?.id || 5 - const fundName = catalystRoundInfo?.currentFund?.name || round.toString(); - return ( -
- {activeDialog} - -
- ); - } - @computed get generated(): {| - VotingRegistrationDialogProps: InjectedOrGenerated, BannerContainerProps: InjectedOrGenerated, NavBarContainerRevampProps: InjectedOrGenerated, SidebarContainerProps: InjectedOrGenerated, - actions: {| - dialogs: {| - closeActiveDialog: {| - trigger: (params: void) => void, - |}, - open: {| - trigger: (params: {| - dialog: any, - params?: any, - |}) => void, - |}, - |}, - |}, - hasAnyPending: boolean, - balance: ?MultiToken, - stores: {| - uiDialogs: {| - isOpen: any => boolean, - |}, - tokenInfoStore: {| - tokenInfo: TokenInfoMap, - |}, - wallets: {| - selected: null | PublicDeriver<>, - |}, - delegation: {| - getDelegationRequests: (PublicDeriver<>) => void | DelegationRequests, - |}, - substores: {| - ada: {| - votingStore: {| - catalystRoundInfo: ?CatalystRoundInfoResponse, - loadingCatalystRoundInfo: boolean, - |} - |} - |} - |}, + VotingPageContentProps: InjectedOrGenerated, |} { if (this.props.generated !== undefined) { return this.props.generated; } + if (this.props.stores == null || this.props.actions == null) { throw new Error(`${nameof(VotingPage)} no way to generated props`); } const { stores, actions } = this.props; - const txInfo = (() => { - const selected = stores.wallets.selected; - if (selected == null) return { - hasAnyPending: false, - balance: null, - }; - const txRequests = stores.transactions.getTxRequests(selected); - return { - hasAnyPending: (txRequests.requests.pendingRequest.result ?? []).length > 0, - // note: Catalyst balance depends on UTXO balance -- not on rewards - balance: txRequests.requests.getBalanceRequest.result, - }; - })(); + return Object.freeze({ - ...txInfo, - actions: { - dialogs: { - closeActiveDialog: { - trigger: actions.dialogs.closeActiveDialog.trigger, - }, - open: { - trigger: actions.dialogs.open.trigger, - }, - }, - }, - stores: { - uiDialogs: { - isOpen: stores.uiDialogs.isOpen, - }, - wallets: { - selected: stores.wallets.selected, - }, - tokenInfoStore: { - tokenInfo: stores.tokenInfoStore.tokenInfo, - }, - delegation: { - getDelegationRequests: stores.delegation.getDelegationRequests, - }, - substores: { - ada: { - votingStore: { - catalystRoundInfo: stores.substores.ada.votingStore.catalystRoundInfo, - loadingCatalystRoundInfo: stores.substores.ada.votingStore.loadingCatalystRoundInfo, - } - } - } - }, - VotingRegistrationDialogProps: ({ + VotingPageContentProps: ({ actions, stores, - }: InjectedOrGenerated), + }: InjectedOrGenerated), SidebarContainerProps: ({ actions, stores }: InjectedOrGenerated), NavBarContainerRevampProps: ({ actions, diff --git a/packages/yoroi-extension/app/containers/wallet/voting/VotingPageContent.js b/packages/yoroi-extension/app/containers/wallet/voting/VotingPageContent.js new file mode 100644 index 0000000000..d17445e838 --- /dev/null +++ b/packages/yoroi-extension/app/containers/wallet/voting/VotingPageContent.js @@ -0,0 +1,421 @@ +// @flow +import type { Node } from 'react'; +import { Component } from 'react'; +import { observer } from 'mobx-react'; +import { computed } from 'mobx'; +import { defineMessages, intlShape } from 'react-intl'; +import type { $npm$ReactIntl$IntlFormat } from 'react-intl'; +import type { InjectedOrGenerated } from '../../../types/injectedPropsType'; +import Voting from '../../../components/wallet/voting/Voting'; +import VotingRegistrationDialogContainer from '../dialogs/voting/VotingRegistrationDialogContainer'; +import type { GeneratedData as VotingRegistrationDialogContainerData } from '../dialogs/voting/VotingRegistrationDialogContainer'; +import { handleExternalLinkClick } from '../../../utils/routing'; +import { + WalletTypeOption, +} from '../../../api/ada/lib/storage/models/ConceptualWallet/interfaces'; +import { PublicDeriver } from '../../../api/ada/lib/storage/models/PublicDeriver/index'; +import LoadingSpinner from '../../../components/widgets/LoadingSpinner'; +import VerticallyCenteredLayout from '../../../components/layout/VerticallyCenteredLayout'; +import { CATALYST_MIN_AMOUNT, CATALYST_DISPLAYED_MIN_AMOUNT } from '../../../config/numbersConfig'; +import InsufficientFundsPage from './InsufficientFundsPage'; +import { getTokenName, genLookupOrFail } from '../../../stores/stateless/tokenHelpers'; +import type { TokenInfoMap } from '../../../stores/toplevel/TokenInfoStore'; +import environment from '../../../environment'; +import { MultiToken } from '../../../api/common/lib/MultiToken'; +import RegistrationOver from './RegistrationOver'; +import type { DelegationRequests } from '../../../stores/toplevel/DelegationStore'; +import { + isLedgerNanoWallet, + isTrezorTWallet, +} from '../../../api/ada/lib/storage/models/ConceptualWallet/index'; +import type { CatalystRoundInfoResponse } from '../../../api/ada/lib/state-fetch/types' + +export type GeneratedData = typeof VotingPageContent.prototype.generated; +type Props = InjectedOrGenerated + +const messages: * = defineMessages({ + mainTitle: { + id: 'wallet.registrationOver.mainTitle', + defaultMessage: '!!!Registration is now closed.', + }, + mainSubtitle: { + id: 'wallet.registrationOver.mainSubtitle', + defaultMessage: '!!!The registration period for fund {roundNumber} has ended. For more information, check the Catalyst app.', + }, + unavailableTitle: { + id: 'wallet.registrationOver.unavailableTitle', + defaultMessage: '!!!Catalyst Round information is currently unavailable.', + }, + unavailableSubtitle: { + id: 'wallet.registrationOver.unavailableSubtitle', + defaultMessage: '!!!Please check the Catalyst app for more info', + }, + earlyForRegistrationTitle: { + id: 'wallet.registrationOver.earlyForRegistrationTitle', + defaultMessage: '!!!Registration hasn\'t started yet.' + }, + earlyForRegistrationSubTitle: { + id: 'wallet.registrationOver.earlyForRegistrationSubTitle', + defaultMessage: '!!!Registration for Round {roundNumber} begins at {registrationStart}.' + }, + beforeVotingSubtitle: { + id: 'wallet.registrationOver.beforeVotingSubtitle', + defaultMessage: '!!!Registration has ended. Voting starts at {votingStart}' + }, + betweenVotingSubtitle: { + id: 'wallet.registrationOver.betweenVotingSubtitle', + defaultMessage: '!!!"Registration has ended. Voting ends at {votingEnd}' + }, + nextFundRegistration: { + id: 'wallet.registrationOver.nextFundRegistration', + defaultMessage: 'Round {roundNumber} starts at {registrationStart}' + } +}); + +@observer +class VotingPageContent extends Component { + static contextTypes: {| intl: $npm$ReactIntl$IntlFormat |} = { intl: intlShape.isRequired }; + + onClose: void => void = () => { + this.generated.actions.dialogs.closeActiveDialog.trigger(); + }; + + start: void => void = () => { + this.generated.actions.dialogs.open.trigger({ dialog: VotingRegistrationDialogContainer }); + }; + + get isDelegated(): ?boolean { + const publicDeriver = this.generated.stores.wallets.selected; + const delegationStore = this.generated.stores.delegation; + + if (!publicDeriver) { + throw new Error(`${nameof(this.isDelegated)} no public deriver. Should never happen`); + } + + const delegationRequests = delegationStore.getDelegationRequests(publicDeriver); + if (delegationRequests == null) { + throw new Error(`${nameof(this.isDelegated)} called for non-reward wallet`); + } + const currentDelegation = delegationRequests.getCurrentDelegation; + + if ( + !currentDelegation.wasExecuted || + currentDelegation.isExecuting || + currentDelegation.result == null + ) { + return undefined; + } + if( + !currentDelegation.result.currEpoch || + currentDelegation.result.currEpoch.pools.length === 0 + ) { + return false; + } + return true; + } + + render(): Node { + const { intl } = this.context + const { + uiDialogs, + wallets: { selected }, + } = this.generated.stores; + let activeDialog = null; + + if(selected == null){ + throw new Error(`${nameof(VotingPageContent)} no wallet selected`); + } + + const balance = this.generated.balance; + if (balance == null) { + return ( + + + + ); + } + + // keep enabled on the testnet + const { catalystRoundInfo, loadingCatalystRoundInfo } = + this.generated.stores.substores.ada.votingStore; + + if (loadingCatalystRoundInfo) { + return ( + + + + ); + } + + if (!environment.isTest()) { + if (!catalystRoundInfo || (!catalystRoundInfo.currentFund && !catalystRoundInfo.nextFund)){ + return ( + + ); + } + + const { currentFund, nextFund } = catalystRoundInfo; + const nextFundRegistrationSubtitle = intl.formatMessage(messages.nextFundRegistration, { + roundNumber: nextFund?.id, + registrationStart: nextFund?.registrationStart + }) + + const fund = { + 'id': 8, + 'name': 'Fund9', + 'registrationStart': '2021-01-27T11:00:00Z', + 'registrationEnd': '2023-08-04T11:00:00Z', + 'votingStart': '2021-08-11T11:00:00Z', + 'votingEnd': '2023-08-25T11:00:00Z', + 'votingPowerThreshold': '450' + }; + + if (currentFund) { + const isLate = new Date() >= new Date(Date.parse(fund.registrationEnd)) + const isEarly = new Date() <= new Date(Date.parse(fund.registrationStart)) + const isBeforeVoting = new Date() <= new Date(Date.parse(fund.votingStart)) + const isAfterVoting = new Date() >= new Date(Date.parse(fund.votingEnd)) + const isBetweenVoting = !isBeforeVoting && !isAfterVoting; + + if (isEarly) { + return ( + + ); + } + + // registeration is ended -> check for voting start and end dates + if (isLate) { + if (isBeforeVoting) { + return ( + + ); + } + + if (isBetweenVoting) { + return ( + + ); + } + + if (isAfterVoting) { + /* if we after the voting date (= between funds) and no next funds + will dispaly "round is over" */ + let subtitle = intl.formatMessage(messages.mainSubtitle, { + roundNumber: currentFund.id + }) + + // Check for the next funds if we are after voting + if(nextFund) { + subtitle = nextFundRegistrationSubtitle + } + + return ( + + ); + } + } + } else if (nextFund) { + // No current funds -> check for next funds + return ( + + ); + } + } + + // disable the minimum on E2E tests + if (!environment.isTest() && balance.getDefaultEntry().amount.lt(CATALYST_MIN_AMOUNT)) { + const getTokenInfo = genLookupOrFail(this.generated.stores.tokenInfoStore.tokenInfo); + const tokenInfo = getTokenInfo({ + identifier: selected.getParent().getDefaultToken().defaultIdentifier, + networkId: selected.getParent().getDefaultToken().defaultNetworkId, + }); + return ( + + ) + } + + let walletType; + if ( + selected.getParent().getWalletType() !== WalletTypeOption.HARDWARE_WALLET + ) { + walletType = 'mnemonic'; + } else if (isTrezorTWallet(selected.getParent())) { + walletType = 'trezorT'; + } else if (isLedgerNanoWallet(selected.getParent())) { + walletType = 'ledgerNano'; + } else { + throw new Error(`${nameof(VotingPageContent)} unexpected wallet type`); + } + + if (uiDialogs.isOpen(VotingRegistrationDialogContainer)) { + activeDialog = ( + + ); + } + + /* + At this point we are sure that we have current funds + I added the "5" for two reasons + 1. As a placeholder as the component will not be rendered without it. + 2. this page you can see it in test environment even if you + out of the registration dates. + */ + const round = catalystRoundInfo?.currentFund?.id || catalystRoundInfo?.nextFund?.id || 5 + const fundName = catalystRoundInfo?.currentFund?.name || round.toString(); + return ( +
+ {activeDialog} + +
+ ); + } + + @computed get generated(): {| + VotingRegistrationDialogProps: InjectedOrGenerated, + actions: {| + dialogs: {| + closeActiveDialog: {| + trigger: (params: void) => void, + |}, + open: {| + trigger: (params: {| + dialog: any, + params?: any, + |}) => void, + |}, + |}, + |}, + hasAnyPending: boolean, + balance: ?MultiToken, + stores: {| + uiDialogs: {| + isOpen: any => boolean, + |}, + tokenInfoStore: {| + tokenInfo: TokenInfoMap, + |}, + wallets: {| + selected: null | PublicDeriver<>, + |}, + delegation: {| + getDelegationRequests: (PublicDeriver<>) => void | DelegationRequests, + |}, + substores: {| + ada: {| + votingStore: {| + catalystRoundInfo: ?CatalystRoundInfoResponse, + loadingCatalystRoundInfo: boolean, + |} + |} + |} + |}, + |} { + if (this.props.generated !== undefined) { + return this.props.generated; + } + if (this.props.stores == null || this.props.actions == null) { + throw new Error(`${nameof(VotingPageContent)} no way to generated props`); + } + + const { stores, actions } = this.props; + const txInfo = (() => { + const selected = stores.wallets.selected; + if (selected == null) return { + hasAnyPending: false, + balance: null, + }; + const txRequests = stores.transactions.getTxRequests(selected); + return { + hasAnyPending: (txRequests.requests.pendingRequest.result ?? []).length > 0, + // note: Catalyst balance depends on UTXO balance -- not on rewards + balance: txRequests.requests.getBalanceRequest.result, + }; + })(); + return Object.freeze({ + ...txInfo, + actions: { + dialogs: { + closeActiveDialog: { + trigger: actions.dialogs.closeActiveDialog.trigger, + }, + open: { + trigger: actions.dialogs.open.trigger, + }, + }, + }, + stores: { + uiDialogs: { + isOpen: stores.uiDialogs.isOpen, + }, + wallets: { + selected: stores.wallets.selected, + }, + tokenInfoStore: { + tokenInfo: stores.tokenInfoStore.tokenInfo, + }, + delegation: { + getDelegationRequests: stores.delegation.getDelegationRequests, + }, + substores: { + ada: { + votingStore: { + catalystRoundInfo: stores.substores.ada.votingStore.catalystRoundInfo, + loadingCatalystRoundInfo: stores.substores.ada.votingStore.loadingCatalystRoundInfo, + } + } + } + }, + VotingRegistrationDialogProps: ({ + actions, + stores, + }: InjectedOrGenerated), + }); + }; +}; + + +export default VotingPageContent; \ No newline at end of file