From 134873df771018f4e687d4270e10d987793abeb2 Mon Sep 17 00:00:00 2001 From: "Eugene P." Date: Mon, 26 Aug 2024 12:18:59 +0300 Subject: [PATCH] feat(wallet): rebrand balance section (#1863) * feat(wallet): Home page. Update balance section. Signed-off-by: Eugene Panteleymonchuk * feat(wallet): Home page. Update balance section. Signed-off-by: Eugene Panteleymonchuk * feat(wallet): Redirect to send coins page after unlock. Signed-off-by: Eugene Panteleymonchuk * feat(wallet): Home balance section. Revert changing hook, update business-logic. Signed-off-by: Eugene Panteleymonchuk * feat(wallet): Home balance section. Disable when locked. Signed-off-by: Eugene Panteleymonchuk * feat(wallet): rebrand home. Updates after PR comments. Signed-off-by: Eugene Panteleymonchuk * feat(wallet): remove console.log Signed-off-by: Eugene Panteleymonchuk --------- Signed-off-by: Eugene Panteleymonchuk Co-authored-by: evavirseda --- apps/ui-icons/src/Send.tsx | 23 ++++ apps/ui-icons/src/index.ts | 3 +- apps/ui-icons/svgs/send.svg | 5 + .../components/molecules/account/Account.tsx | 2 +- .../components/molecules/address/Address.tsx | 56 ++++++-- .../accounts/UnlockAccountContext.tsx | 22 ++- apps/wallet/src/ui/app/hooks/index.ts | 1 + .../app/pages/home/tokens/TokensDetails.tsx | 127 ++++++++++++++++-- .../pages/home/tokens/coin-balance/index.tsx | 33 +---- 9 files changed, 221 insertions(+), 51 deletions(-) create mode 100644 apps/ui-icons/src/Send.tsx create mode 100644 apps/ui-icons/svgs/send.svg diff --git a/apps/ui-icons/src/Send.tsx b/apps/ui-icons/src/Send.tsx new file mode 100644 index 00000000000..f3cb0a6197b --- /dev/null +++ b/apps/ui-icons/src/Send.tsx @@ -0,0 +1,23 @@ +// Copyright (c) 2024 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +import { SVGProps } from 'react'; +export default function SvgSend(props: SVGProps) { + return ( + + + + ); +} diff --git a/apps/ui-icons/src/index.ts b/apps/ui-icons/src/index.ts index a19892633dd..14d5ea0af18 100644 --- a/apps/ui-icons/src/index.ts +++ b/apps/ui-icons/src/index.ts @@ -36,9 +36,9 @@ export { default as Handler } from './Handler'; export { default as Home } from './Home'; export { default as ImportPass } from './ImportPass'; export { default as Info } from './Info'; +export { default as IotaLogoWeb } from './IotaLogoWeb'; export { default as IotaLogoMark } from './IotaLogoMark'; export { default as IotaLogoSmall } from './IotaLogoSmall'; -export { default as IotaLogoWeb } from './IotaLogoWeb'; export { default as Key } from './Key'; export { default as Ledger } from './Ledger'; export { default as ListViewLarge } from './ListViewLarge'; @@ -61,6 +61,7 @@ export { default as RadioOn } from './RadioOn'; export { default as Save } from './Save'; export { default as Search } from './Search'; export { default as Seed } from './Seed'; +export { default as Send } from './Send'; export { default as Settings } from './Settings'; export { default as SortByDefault } from './SortByDefault'; export { default as SortByDown } from './SortByDown'; diff --git a/apps/ui-icons/svgs/send.svg b/apps/ui-icons/svgs/send.svg new file mode 100644 index 00000000000..230eae62ceb --- /dev/null +++ b/apps/ui-icons/svgs/send.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/ui-kit/src/lib/components/molecules/account/Account.tsx b/apps/ui-kit/src/lib/components/molecules/account/Account.tsx index aaf3716f92a..4e98222beb6 100644 --- a/apps/ui-kit/src/lib/components/molecules/account/Account.tsx +++ b/apps/ui-kit/src/lib/components/molecules/account/Account.tsx @@ -93,7 +93,7 @@ export function Account({
) => void; + onCopySuccess?: (e: React.MouseEvent, text: string) => void; + /** + * The onCopyError event of the Address (optional). + */ + onCopyError?: (e: unknown, text: string) => void; /** * The onOpen event of the Address (optional). */ @@ -33,24 +47,50 @@ export function Address({ text, isCopyable, isExternal, - onCopy, + externalLink, + copyText = text, + onCopySuccess, + onCopyError, onOpen, }: AddressProps): React.JSX.Element { + async function handleCopyClick(event: React.MouseEvent) { + if (!navigator.clipboard) { + return; + } + + try { + await navigator.clipboard.writeText(copyText); + onCopySuccess?.(event, copyText); + } catch (error) { + console.error('Failed to copy:', error); + onCopyError?.(error, copyText); + } + } + + function handleOpenClick(event: React.MouseEvent) { + if (externalLink) { + const newWindow = window.open(externalLink, '_blank', 'noopener,noreferrer'); + if (newWindow) newWindow.opener = null; + } else { + onOpen?.(event); + } + } + return ( -
+
{text} {isCopyable && ( )} {isExternal && ( diff --git a/apps/wallet/src/ui/app/components/accounts/UnlockAccountContext.tsx b/apps/wallet/src/ui/app/components/accounts/UnlockAccountContext.tsx index 58d6b818229..71e7758d0ae 100644 --- a/apps/wallet/src/ui/app/components/accounts/UnlockAccountContext.tsx +++ b/apps/wallet/src/ui/app/components/accounts/UnlockAccountContext.tsx @@ -3,17 +3,25 @@ // SPDX-License-Identifier: Apache-2.0 import { type SerializedUIAccount } from '_src/background/accounts/Account'; -import React, { createContext, useCallback, useContext, useState, type ReactNode } from 'react'; +import React, { + createContext, + useCallback, + useContext, + useState, + type ReactNode, + useRef, +} from 'react'; import { toast } from 'react-hot-toast'; - import { useBackgroundClient } from '../../hooks/useBackgroundClient'; import { useUnlockMutation } from '../../hooks/useUnlockMutation'; import { UnlockAccountModal } from './UnlockAccountModal'; +type OnSuccessCallback = () => void | Promise; + interface UnlockAccountContextType { isUnlockModalOpen: boolean; accountToUnlock: SerializedUIAccount | null; - unlockAccount: (account: SerializedUIAccount) => void; + unlockAccount: (account: SerializedUIAccount, onSuccessCallback?: OnSuccessCallback) => void; lockAccount: (account: SerializedUIAccount) => void; isPending: boolean; hideUnlockModal: () => void; @@ -28,20 +36,26 @@ interface UnlockAccountProviderProps { export function UnlockAccountProvider({ children }: UnlockAccountProviderProps) { const [isUnlockModalOpen, setIsUnlockModalOpen] = useState(false); const [accountToUnlock, setAccountToUnlock] = useState(null); + const onSuccessCallbackRef = useRef(); const unlockAccountMutation = useUnlockMutation(); const backgroundClient = useBackgroundClient(); const hideUnlockModal = useCallback(() => { setIsUnlockModalOpen(false); setAccountToUnlock(null); + onSuccessCallbackRef.current && onSuccessCallbackRef.current(); }, []); const unlockAccount = useCallback( - async (account: SerializedUIAccount) => { + async (account: SerializedUIAccount, onSuccessCallback?: OnSuccessCallback) => { if (account) { if (account.isPasswordUnlockable) { // for password-unlockable accounts, show the unlock modal setIsUnlockModalOpen(true); setAccountToUnlock(account); + + if (onSuccessCallback) { + onSuccessCallbackRef.current = onSuccessCallback; + } } else { try { // for non-password-unlockable accounts, unlock directly diff --git a/apps/wallet/src/ui/app/hooks/index.ts b/apps/wallet/src/ui/app/hooks/index.ts index 530810aaa4d..c5f56ef733d 100644 --- a/apps/wallet/src/ui/app/hooks/index.ts +++ b/apps/wallet/src/ui/app/hooks/index.ts @@ -15,6 +15,7 @@ export { useTransactionDryRun } from './useTransactionDryRun'; export { useGetTxnRecipientAddress } from './useGetTxnRecipientAddress'; export { useGetTransferAmount } from './useGetTransferAmount'; export { useOwnedNFT } from './useOwnedNFT'; +export { useCopyToClipboard } from './useCopyToClipboard'; export * from './useTransactionData'; export * from './useActiveAddress'; diff --git a/apps/wallet/src/ui/app/pages/home/tokens/TokensDetails.tsx b/apps/wallet/src/ui/app/pages/home/tokens/TokensDetails.tsx index 62833d493da..abbdc9e83da 100644 --- a/apps/wallet/src/ui/app/pages/home/tokens/TokensDetails.tsx +++ b/apps/wallet/src/ui/app/pages/home/tokens/TokensDetails.tsx @@ -6,8 +6,15 @@ import { useIsWalletDefiEnabled } from '_app/hooks/useIsWalletDefiEnabled'; import { LargeButton } from '_app/shared/LargeButton'; import { Text } from '_app/shared/text'; import { ButtonOrLink } from '_app/shared/utils/ButtonOrLink'; -import { AccountsList, Alert, CoinIcon, Loading, UnlockAccountButton } from '_components'; -import { useAppSelector, useCoinsReFetchingConfig } from '_hooks'; +import { + AccountsList, + Alert, + CoinIcon, + ExplorerLinkType, + Loading, + UnlockAccountButton, +} from '_components'; +import { useAppSelector, useCoinsReFetchingConfig, useCopyToClipboard } from '_hooks'; import { ampli } from '_src/shared/analytics/ampli'; import { Feature } from '_src/shared/experimentation/features'; import { useActiveAccount } from '_src/ui/app/hooks/useActiveAccount'; @@ -15,6 +22,7 @@ import { usePinnedCoinTypes } from '_src/ui/app/hooks/usePinnedCoinTypes'; import FaucetRequestButton from '_src/ui/app/shared/faucet/FaucetRequestButton'; import PageTitle from '_src/ui/app/shared/PageTitle'; import { useFeature } from '@growthbook/growthbook-react'; +import { toast } from 'react-hot-toast'; import { DELEGATED_STAKES_QUERY_REFETCH_INTERVAL, DELEGATED_STAKES_QUERY_STALE_TIME, @@ -28,6 +36,22 @@ import { useResolveIotaNSName, useSortedCoinsByCategories, } from '@iota/core'; +import { + Button, + ButtonSize, + ButtonType, + Address, + Dialog, + DialogContent, + DialogBody, + Header, + ButtonUnstyled, + Chip, + SegmentedButton, + SegmentedButtonType, + Title, + TitleSize, +} from '@iota/apps-ui-kit'; import { useIotaClientQuery } from '@iota/dapp-kit'; import { Info12 } from '@iota/icons'; import { type CoinBalance as CoinBalanceType, Network } from '@iota/iota-sdk/client'; @@ -35,20 +59,14 @@ import { formatAddress, IOTA_TYPE_ARG, parseStructTag } from '@iota/iota-sdk/uti import { useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; import { type ReactNode, useEffect, useState } from 'react'; -import { Pined, Unpined } from '@iota/ui-icons'; +import { ArrowBottomLeft, Send, Pined, Unpined } from '@iota/ui-icons'; import Interstitial, { type InterstitialConfig } from '../interstitial'; import { CoinBalance } from './coin-balance'; import { PortfolioName } from './PortfolioName'; import { TokenStakingOverview } from './TokenStakingOverview'; import { TokenLink } from './TokenLink'; -import { - ButtonUnstyled, - Chip, - SegmentedButton, - SegmentedButtonType, - Title, - TitleSize, -} from '@iota/apps-ui-kit'; +import { useNavigate } from 'react-router-dom'; +import { useExplorerLink } from '_app/hooks/useExplorerLink'; interface TokenDetailsProps { coinType?: string; @@ -320,6 +338,8 @@ function getFallbackSymbol(coinType: string) { } function TokenDetails({ coinType }: TokenDetailsProps) { + const [dialogReceiveOpen, setDialogReceiveOpen] = useState(false); + const navigate = useNavigate(); const isDefiWalletEnabled = useIsWalletDefiEnabled(); const [interstitialDismissed, setInterstitialDismissed] = useState(false); const activeCoinType = coinType || IOTA_TYPE_ARG; @@ -347,6 +367,10 @@ function TokenDetails({ coinType }: TokenDetailsProps) { retry: false, enabled: isMainnet, }); + const explorerHref = useExplorerLink({ + type: ExplorerLinkType.Address, + address: activeAccountAddress, + }); const { data: coinBalances, @@ -381,6 +405,16 @@ function TokenDetails({ coinType }: TokenDetailsProps) { // Avoid perpetual loading state when fetching and retry keeps failing add isFetched check const isFirstTimeLoading = isPending && !isFetched; + const onSendClick = () => { + if (!activeAccount?.isLocked) { + const destination = coinBalance?.coinType + ? `/send?${new URLSearchParams({ type: coinBalance?.coinType }).toString()}` + : '/send'; + + navigate(destination); + } + }; + useEffect(() => { const dismissed = walletInterstitialConfig?.dismissKey && @@ -428,6 +462,40 @@ function TokenDetails({ coinType }: TokenDetailsProps) { className="flex h-full flex-1 flex-grow flex-col items-center gap-8" data-testid="coin-page" > +
+
+
+
toast.success('Address copied')} + /> +
+ +
+
+
+
)}
+ setDialogReceiveOpen(isOpen)} + /> ); } export default TokenDetails; + +function DialogReceiveTokens({ + address, + open, + setOpen, +}: { + address: string; + open: boolean; + setOpen: (isOpen: boolean) => void; +}) { + const onCopy = useCopyToClipboard(address, { + copySuccessMessage: 'Address copied', + }); + + return ( +
+ + +
setOpen(false)} /> + +
+
+
+
+
+
+ +
+
+ ); +} diff --git a/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx b/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx index c2192b32e78..8aeb3bb4c88 100644 --- a/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx +++ b/apps/wallet/src/ui/app/pages/home/tokens/coin-balance/index.tsx @@ -1,10 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // Modifications Copyright (c) 2024 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -import { useIsWalletDefiEnabled } from '_app/hooks/useIsWalletDefiEnabled'; import { useAppSelector } from '_hooks'; -import { Heading } from '_src/ui/app/shared/heading'; -import { Text } from '_src/ui/app/shared/text'; import { useBalanceInUSD, useFormatCoin } from '@iota/core'; import { Network } from '@iota/iota-sdk/client'; import { IOTA_TYPE_ARG } from '@iota/iota-sdk/utils'; @@ -20,7 +17,6 @@ interface WalletBalanceUsdProps { } function WalletBalanceUsd({ amount: walletBalance }: WalletBalanceUsdProps) { - const isDefiWalletEnabled = useIsWalletDefiEnabled(); const formattedWalletBalance = useBalanceInUSD(IOTA_TYPE_ARG, walletBalance); const walletBalanceInUsd = useMemo(() => { @@ -36,15 +32,7 @@ function WalletBalanceUsd({ amount: walletBalance }: WalletBalanceUsdProps) { return null; } - return ( - - {walletBalanceInUsd} - - ); + return
{walletBalanceInUsd}
; } export function CoinBalance({ amount: walletBalance, type }: CoinProps) { @@ -52,19 +40,12 @@ export function CoinBalance({ amount: walletBalance, type }: CoinProps) { const [formatted, symbol] = useFormatCoin(walletBalance, type); return ( -
-
- - {formatted} - - - - {symbol} - -
-
- {network === Network.Mainnet ? : null} + <> +
+
{formatted}
+
{symbol}
-
+ {network === Network.Mainnet ? : null} + ); }