From 2d62774d59ef79e728d849073214a5671958c2d5 Mon Sep 17 00:00:00 2001 From: vetalcore Date: Mon, 2 Dec 2024 12:07:17 +0200 Subject: [PATCH 1/5] fix: [lw-11876]: show upgrade to lace in send flow in locked rewards available --- apps/browser-extension-wallet/src/popup.tsx | 6 +- .../browser-view/features/staking/hooks.ts | 62 +++++++++- .../src/views/nami-mode/NamiView.tsx | 14 ++- .../outside-handles-provider/types.ts | 2 + .../ui/app/components/upgradeToLaceBanner.tsx | 86 ++++++++++++++ packages/nami/src/ui/app/pages/send.tsx | 108 ++++++++++++------ packages/nami/src/ui/app/pages/settings.tsx | 1 + packages/nami/src/ui/app/pages/wallet.tsx | 1 + packages/nami/src/ui/indexMain.tsx | 2 +- 9 files changed, 237 insertions(+), 45 deletions(-) create mode 100644 packages/nami/src/ui/app/components/upgradeToLaceBanner.tsx diff --git a/apps/browser-extension-wallet/src/popup.tsx b/apps/browser-extension-wallet/src/popup.tsx index a47e54f452..6f9ca0fac8 100644 --- a/apps/browser-extension-wallet/src/popup.tsx +++ b/apps/browser-extension-wallet/src/popup.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import * as ReactDOM from 'react-dom'; import { HashRouter } from 'react-router-dom'; -import { PopupView } from '@routes'; +import { PopupView, walletRoutePaths } from '@routes'; import { StoreProvider } from '@stores'; import { CurrencyStoreProvider } from '@providers/currency'; import { AppSettingsProvider, DatabaseProvider, ThemeProvider, AnalyticsProvider } from '@providers'; @@ -34,8 +34,8 @@ const App = (): React.ReactElement => { const newModeValue = changes.BACKGROUND_STORAGE?.newValue?.namiMigration; if (oldModeValue?.mode !== newModeValue?.mode) { setMode(newModeValue); - // Force back to original routing - window.location.hash = '#'; + // Force back to original routing unless it is staking route (see LW-11876) + if (window.location.hash.split('#')[1] !== walletRoutePaths.earn) window.location.hash = '#'; } }); diff --git a/apps/browser-extension-wallet/src/views/browser-view/features/staking/hooks.ts b/apps/browser-extension-wallet/src/views/browser-view/features/staking/hooks.ts index fc86c1b7cc..8348862eb0 100644 --- a/apps/browser-extension-wallet/src/views/browser-view/features/staking/hooks.ts +++ b/apps/browser-extension-wallet/src/views/browser-view/features/staking/hooks.ts @@ -1,8 +1,17 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { useDelegationStore } from '@src/features/delegation/stores'; import { useWalletStore } from '@stores'; import { withSignTxConfirmation } from '@lib/wallet-api-ui'; import { useSecrets } from '@lace/core'; +import { useObservable } from '@lace/common'; +import { Wallet } from '@lace/cardano'; + +interface UseRewardAccountsDataType { + accountsWithRegisteredStakeCredsWithoutVotingDelegation: Wallet.Cardano.RewardAccountInfo[]; + accountsWithRegisteredStakeCreds: Wallet.Cardano.RewardAccountInfo[]; + poolIdToRewardAccountMap: Map; + lockedStakeRewards: bigint; +} export const useDelegationTransaction = (): { signAndSubmitTransaction: () => Promise } => { const { password, clearSecrets } = useSecrets(); @@ -17,3 +26,54 @@ export const useDelegationTransaction = (): { signAndSubmitTransaction: () => Pr return { signAndSubmitTransaction }; }; + +export const getPoolIdToRewardAccountMap = ( + rewardAccounts: Wallet.Cardano.RewardAccountInfo[] +): UseRewardAccountsDataType['poolIdToRewardAccountMap'] => + new Map( + rewardAccounts + ?.map((rewardAccount): [string, Wallet.Cardano.RewardAccountInfo] => { + const { delegatee } = rewardAccount; + const delagationInfo = delegatee?.nextNextEpoch || delegatee?.nextEpoch || delegatee?.currentEpoch; + + return [delagationInfo?.id.toString(), rewardAccount]; + }) + .filter(([poolId]) => !!poolId) + ); + +export const useRewardAccountsData = (): UseRewardAccountsDataType => { + const { inMemoryWallet } = useWalletStore(); + const rewardAccounts = useObservable(inMemoryWallet.delegation.rewardAccounts$); + const accountsWithRegisteredStakeCreds = rewardAccounts?.filter( + ({ credentialStatus }) => Wallet.Cardano.StakeCredentialStatus.Registered === credentialStatus + ); + + const accountsWithRegisteredStakeCredsWithoutVotingDelegation = useMemo( + () => accountsWithRegisteredStakeCreds?.filter(({ dRepDelegatee }) => !dRepDelegatee), + [accountsWithRegisteredStakeCreds] + ); + + const lockedStakeRewards = useMemo( + () => + BigInt( + accountsWithRegisteredStakeCredsWithoutVotingDelegation + ? Wallet.BigIntMath.sum( + accountsWithRegisteredStakeCredsWithoutVotingDelegation.map(({ rewardBalance }) => rewardBalance) + ) + : 0 + ), + [accountsWithRegisteredStakeCredsWithoutVotingDelegation] + ); + + const poolIdToRewardAccountMap = useMemo( + () => getPoolIdToRewardAccountMap(accountsWithRegisteredStakeCreds), + [accountsWithRegisteredStakeCreds] + ); + + return { + accountsWithRegisteredStakeCreds, + accountsWithRegisteredStakeCredsWithoutVotingDelegation, + lockedStakeRewards, + poolIdToRewardAccountMap + }; +}; diff --git a/apps/browser-extension-wallet/src/views/nami-mode/NamiView.tsx b/apps/browser-extension-wallet/src/views/nami-mode/NamiView.tsx index 2e0e32d693..a029ff2254 100644 --- a/apps/browser-extension-wallet/src/views/nami-mode/NamiView.tsx +++ b/apps/browser-extension-wallet/src/views/nami-mode/NamiView.tsx @@ -18,7 +18,8 @@ import { useWalletManager, useBuildDelegation, useBalances, - useHandleResolver + useHandleResolver, + useRedirection } from '@hooks'; import { walletManager, withSignTxConfirmation } from '@lib/wallet-api-ui'; import { useAnalytics } from './hooks'; @@ -26,7 +27,7 @@ import { useDappContext, withDappContext } from '@src/features/dapp/context'; import { localDappService } from '../browser-view/features/dapp/components/DappList/localDappService'; import { isValidURL } from '@src/utils/is-valid-url'; import { CARDANO_COIN_SYMBOL } from './constants'; -import { useDelegationTransaction } from '../browser-view/features/staking/hooks'; +import { useDelegationTransaction, useRewardAccountsData } from '../browser-view/features/staking/hooks'; import { useSecrets } from '@lace/core'; import { useDelegationStore } from '@src/features/delegation/stores'; import { useStakePoolDetails } from '@src/features/stake-pool-details/store'; @@ -43,6 +44,7 @@ import { BackgroundStorage } from '@lib/scripts/types'; import { getWalletAccountsQtyString } from '@src/utils/get-wallet-count-string'; import { useNetworkError } from '@hooks/useNetworkError'; import { createHistoricalOwnInputResolver } from '@src/utils/own-input-resolver'; +import { walletRoutePaths } from '@routes'; const { AVAILABLE_CHAINS, DEFAULT_SUBMIT_API } = config(); @@ -179,6 +181,10 @@ export const NamiView = withDappContext((): React.ReactElement => { setDeletingWallet(false); }, [analytics, deleteWallet, setDeletingWallet, walletRepository]); + const { lockedStakeRewards } = useRewardAccountsData(); + + const redirectToStaking = useRedirection(walletRoutePaths.earn); + return ( { chainHistoryProvider, protocolParameters: walletState?.protocolParameters, assetInfo: walletState?.assetInfo, - createHistoricalOwnInputResolver + createHistoricalOwnInputResolver, + lockedStakeRewards, + redirectToStaking }} > , ) => Wallet.Cardano.InputResolver; + lockedStakeRewards: bigint; + redirectToStaking: () => void; } diff --git a/packages/nami/src/ui/app/components/upgradeToLaceBanner.tsx b/packages/nami/src/ui/app/components/upgradeToLaceBanner.tsx new file mode 100644 index 0000000000..77ea83cdbd --- /dev/null +++ b/packages/nami/src/ui/app/components/upgradeToLaceBanner.tsx @@ -0,0 +1,86 @@ +import React from 'react'; + +import { Box, Button, Text, Link, useColorModeValue } from '@chakra-ui/react'; +import { AnimatePresence, motion } from 'framer-motion'; + +import { useOutsideHandles } from '../../../features/outside-handles-provider'; + +export const UpgradeToLaceBanner = ({ + showSwitchToLaceBanner, +}: Readonly<{ showSwitchToLaceBanner: boolean }>) => { + const warningBackground = useColorModeValue('#fcf5e3', '#fcf5e3'); + const { openExternalLink, switchWalletMode, redirectToStaking } = + useOutsideHandles(); + + return ( + + {showSwitchToLaceBanner && ( + + + + Your ADA balance includes Locked Stake Rewards that can only be + withdrawn or transacted after registering your voting power. + Upgrade to Lace to continue. For more information, visit our{' '} + { + openExternalLink('https://www.lace.io/faq'); + }} + > + FAQs. + + + + + + )} + + ); +}; diff --git a/packages/nami/src/ui/app/pages/send.tsx b/packages/nami/src/ui/app/pages/send.tsx index abb4fb344e..1c897ebaae 100644 --- a/packages/nami/src/ui/app/pages/send.tsx +++ b/packages/nami/src/ui/app/pages/send.tsx @@ -7,7 +7,7 @@ /* eslint-disable unicorn/no-null */ /* eslint-disable @typescript-eslint/naming-convention */ import type { RefObject } from 'react'; -import React, { useCallback, useMemo } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; import { Cardano, Serialization, ProviderUtil } from '@cardano-sdk/core'; import { @@ -41,7 +41,7 @@ import { } from '@chakra-ui/react'; import { useObservable } from '@lace/common'; import debouncePromise from 'debounce-promise'; -import { debounce } from 'lodash'; +import debounce from 'lodash/debounce'; import latest from 'promise-latest'; import { MdModeEdit } from 'react-icons/md'; import { Planet } from 'react-kawaii'; @@ -68,6 +68,10 @@ import { NetworkConnectionStates, useCommonOutsideHandles, } from '../../../features/common-outside-handles-provider'; +import { + useOutsideHandles, + type OutsideHandlesContextValue, +} from '../../../features/outside-handles-provider'; import { useStoreActions, useStoreState } from '../../store'; import Account from '../components/account'; import AssetBadge from '../components/assetBadge'; @@ -77,6 +81,7 @@ import ConfirmModal from '../components/confirmModal'; import Copy from '../components/copy'; import { Scrollbars } from '../components/scrollbar'; import UnitDisplay from '../components/unitDisplay'; +import { UpgradeToLaceBanner } from '../components/upgradeToLaceBanner'; import type { UseAccount } from '../../../adapters/account'; import { @@ -132,6 +137,8 @@ const Send = ({ const { secretsUtil } = useOutsideHandles(); + const { lockedStakeRewards } = useOutsideHandles(); + const [showSwitchToLaceBanner, setShowSwitchToLaceBanner] = useState(false); const [address, setAddress] = [ useStoreState(state => state.globalModel.sendStore.address), useStoreActions(actions => actions.globalModel.sendStore.setAddress), @@ -233,7 +240,7 @@ const Send = ({ setFee({ fee: '' }); setTx(null); - await new Promise((res, rej) => + await new Promise(res => setTimeout(() => { res(null); }), @@ -442,15 +449,35 @@ const Send = ({ const setMessageDebounced = useMemo(() => debounce(setMessage, 300), []); const isOffline = networkConnection === NetworkConnectionStates.OFFLINE; + const setShowSwitchToLaceBannerDebounced = useMemo( + () => debounce(setShowSwitchToLaceBanner, 300), + [], + ); + + const reachedMaxAdaAmount = useMemo( + () => + BigInt(toUnit(value.ada)) > + BigInt(BigInt(utxoTotal?.coins || 0) + BigInt(rewards || 0) || '0') - + BigInt(lockedStakeRewards.toString()), + [value.ada, utxoTotal?.coins, rewards, lockedStakeRewards], + ); + + useEffect(() => { + if (!lockedStakeRewards) return; + + setShowSwitchToLaceBannerDebounced(reachedMaxAdaAmount); + }, [lockedStakeRewards, reachedMaxAdaAmount]); + return ( <> {protocolParameters && isLoading ? ( + {!!lockedStakeRewards && ( + + )} - BigInt( - BigInt(utxoTotal?.coins || 0) + - BigInt(rewards || 0) || '0', - )) + reachedMaxAdaAmount) } onFocus={() => (focus.current = true)} placeholder="0.000000" @@ -605,38 +634,43 @@ const Send = ({ - - 0 && ( + - {value.assets.map(asset => ( - - - - ))} - - + + {value.assets.map(asset => ( + + + + ))} + + + )}