From 46472bb6624df78b95e94972a00841b7607b1751 Mon Sep 17 00:00:00 2001 From: Douglas Daniel Date: Mon, 20 Dec 2021 13:33:25 -0700 Subject: [PATCH] feat(wallet): Transactions in Wallet Panel --- .../browser/brave_wallet_constants.h | 12 + .../assets/svg-icons/lightning-bolt-icon.svg | 3 + .../portfolio-transaction-item/index.tsx | 2 +- .../portfolio-transaction-item/style.ts | 18 -- .../extension/connected-bottom-nav/index.tsx | 14 +- .../extension/connected-bottom-nav/style.ts | 20 +- .../components/extension/index.ts | 8 +- .../extension/panel-tooltip/style.ts | 2 +- .../extension/shared-panel-styles.ts | 25 ++ .../transaction-detail-panel/index.tsx | 228 ++++++++++++++++++ .../transaction-detail-panel/style.ts | 142 +++++++++++ .../extension/transaction-list-item/index.tsx | 207 ++++++++++++++++ .../extension/transaction-list-item/style.ts | 106 ++++++++ .../extension/transactions-panel/index.tsx | 72 ++++++ .../extension/transactions-panel/style.ts | 10 + .../components/shared/style.ts | 27 ++- components/brave_wallet_ui/constants/types.ts | 2 + .../brave_wallet_ui/options/panel-titles.ts | 8 + .../brave_wallet_ui/panel/container.tsx | 77 +++++- components/brave_wallet_ui/stories/locale.ts | 19 +- .../stories/wallet-concept.tsx | 2 +- .../stories/wallet-extension-panels.tsx | 84 ++++++- .../utils/format-prices.test.ts | 5 + .../brave_wallet_ui/utils/format-prices.ts | 5 + .../brave_wallet_ui/utils/tx-utils.test.ts | 30 +++ components/brave_wallet_ui/utils/tx-utils.ts | 20 ++ components/resources/wallet_strings.grdp | 7 + 27 files changed, 1113 insertions(+), 42 deletions(-) create mode 100644 components/brave_wallet_ui/assets/svg-icons/lightning-bolt-icon.svg create mode 100644 components/brave_wallet_ui/components/extension/transaction-detail-panel/index.tsx create mode 100644 components/brave_wallet_ui/components/extension/transaction-detail-panel/style.ts create mode 100644 components/brave_wallet_ui/components/extension/transaction-list-item/index.tsx create mode 100644 components/brave_wallet_ui/components/extension/transaction-list-item/style.ts create mode 100644 components/brave_wallet_ui/components/extension/transactions-panel/index.tsx create mode 100644 components/brave_wallet_ui/components/extension/transactions-panel/style.ts create mode 100644 components/brave_wallet_ui/utils/tx-utils.test.ts diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index b61e67836603..8b9ac9078f53 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -450,6 +450,18 @@ constexpr webui::LocalizedString kLocalizedStrings[] = { IDS_BRAVE_WALLET_TRANSACTION_STATUS_CONFIRMED}, {"braveWalletTransactionStatusError", IDS_BRAVE_WALLET_TRANSACTION_STATUS_ERROR}, + {"braveWalletRecentTransactions", IDS_BRAVE_WALLET_RECENT_TRANSACTIONS}, + {"braveWalletTransactionDetails", IDS_BRAVE_WALLET_TRANSACTION_DETAILS}, + {"braveWalletTransactionDetailDate", + IDS_BRAVE_WALLET_TRANSACTION_DETAIL_DATE}, + {"braveWalletTransactionDetailSpeedUp", + IDS_BRAVE_WALLET_TRANSACTION_DETAIL_SPEEDUP}, + {"braveWalletTransactionDetailHash", + IDS_BRAVE_WALLET_TRANSACTION_DETAIL_HASH}, + {"braveWalletTransactionDetailNetwork", + IDS_BRAVE_WALLET_TRANSACTION_DETAIL_NETWORK}, + {"braveWalletTransactionDetailStatus", + IDS_BRAVE_WALLET_TRANSACTION_DETAIL_STATUS}, {"braveWalletTransactionPlaceholder", IDS_BRAVE_WALLET_TRANSACTION_PLACEHOLDER}, {"braveWalletTransactionApproveUnlimited", diff --git a/components/brave_wallet_ui/assets/svg-icons/lightning-bolt-icon.svg b/components/brave_wallet_ui/assets/svg-icons/lightning-bolt-icon.svg new file mode 100644 index 000000000000..37f42bad06a9 --- /dev/null +++ b/components/brave_wallet_ui/assets/svg-icons/lightning-bolt-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/components/brave_wallet_ui/components/desktop/portfolio-transaction-item/index.tsx b/components/brave_wallet_ui/components/desktop/portfolio-transaction-item/index.tsx index 4ff6d60ff80c..b43056b0e8dd 100644 --- a/components/brave_wallet_ui/components/desktop/portfolio-transaction-item/index.tsx +++ b/components/brave_wallet_ui/components/desktop/portfolio-transaction-item/index.tsx @@ -33,7 +33,6 @@ import { FromCircle, MoreButton, MoreIcon, - StatusBubble, StatusRow, StyledWrapper, ToCircle, @@ -41,6 +40,7 @@ import { TransactionFeeTooltipBody, TransactionFeeTooltipTitle } from './style' +import { StatusBubble } from '../../shared/style' import TransactionFeesTooltip from '../transaction-fees-tooltip' import TransactionPopup, { TransactionPopupItem } from '../transaction-popup' import TransactionTimestampTooltip from '../transaction-timestamp-tooltip' diff --git a/components/brave_wallet_ui/components/desktop/portfolio-transaction-item/style.ts b/components/brave_wallet_ui/components/desktop/portfolio-transaction-item/style.ts index 926a8fbdd14c..2f83690efa6c 100644 --- a/components/brave_wallet_ui/components/desktop/portfolio-transaction-item/style.ts +++ b/components/brave_wallet_ui/components/desktop/portfolio-transaction-item/style.ts @@ -1,12 +1,10 @@ import styled from 'styled-components' -import { BraveWallet } from '../../../constants/types' import { MoreVertRIcon, ArrowRightIcon } from 'brave-ui/components/icons' import CoinsIconSVG from '../../../assets/svg-icons/coins-icon.svg' import { WalletButton } from '../../shared/style' interface StyleProps { orb: string - status: BraveWallet.TransactionStatus } export const StyledWrapper = styled.div` @@ -155,22 +153,6 @@ export const StatusRow = styled.div` flex-direction: row; ` -export const StatusBubble = styled.div>` - display: flex; - align-items: center; - justify-content: center; - width: 10px; - height: 10px; - border-radius: 100%; - opacity: ${(p) => p.status === 3 || p.status === 1 || p.status === 0 ? 0.4 : 1}; - background-color: ${(p) => p.status === 4 || p.status === 1 - ? '#2AC194' - : p.status === 2 || p.status === 5 ? '#EE6374' - : p.status === 0 ? p.theme.color.interactive08 : p.theme.color.warningIcon - }; - margin-right: 6px; -` - export const CoinsButton = styled(WalletButton)` display: flex; align-items: center; diff --git a/components/brave_wallet_ui/components/extension/connected-bottom-nav/index.tsx b/components/brave_wallet_ui/components/extension/connected-bottom-nav/index.tsx index 2adf8ae59075..06522db095c8 100644 --- a/components/brave_wallet_ui/components/extension/connected-bottom-nav/index.tsx +++ b/components/brave_wallet_ui/components/extension/connected-bottom-nav/index.tsx @@ -6,11 +6,12 @@ import { reduceNetworkDisplayName } from '../../../utils/network-utils' // Styled Components import { StyledWrapper, - // AppsIcon, + LightningBoltIcon, NavButton, NavButtonText, NavDivider, - NavOutline + NavOutline, + TransactionsButton } from './style' import { BraveWallet, PanelTypes } from '../../../constants/types' @@ -55,11 +56,10 @@ function ConnectedBottomNav (props: Props) { {getLocale('braveWalletSwap')} - {/* */} - {/* Temp commented out for MVP */} - {/* - - */} + + + + ) diff --git a/components/brave_wallet_ui/components/extension/connected-bottom-nav/style.ts b/components/brave_wallet_ui/components/extension/connected-bottom-nav/style.ts index 8862d6f828e1..55f4a7855883 100644 --- a/components/brave_wallet_ui/components/extension/connected-bottom-nav/style.ts +++ b/components/brave_wallet_ui/components/extension/connected-bottom-nav/style.ts @@ -1,5 +1,5 @@ import styled from 'styled-components' -import UnCheckStar from '../../../assets/svg-icons/star-unchecked.svg' +import LightningBolt from '../../../assets/svg-icons/lightning-bolt-icon.svg' import { WalletButton } from '../../shared/style' interface StyleProps { @@ -58,10 +58,22 @@ export const NavButtonText = styled.span` opacity: ${(p) => p.disabled ? '0.6' : '1'}; ` -export const AppsIcon = styled.div` +export const TransactionsButton = styled(WalletButton) ` + display: flex; + height: 100%; + width: 50px; + align-items: center; + justify-content: center; + cursor: pointer; + outline: none; + border: none; + background: none; +` + +export const LightningBoltIcon = styled.div` width: 18px; height: 18px; background-color: ${(p) => p.theme.palette.white}; - -webkit-mask-image: url(${UnCheckStar}); - mask-image: url(${UnCheckStar}); + -webkit-mask-image: url(${LightningBolt}); + mask-image: url(${LightningBolt}); ` diff --git a/components/brave_wallet_ui/components/extension/index.ts b/components/brave_wallet_ui/components/extension/index.ts index da128c90d888..34548102c389 100644 --- a/components/brave_wallet_ui/components/extension/index.ts +++ b/components/brave_wallet_ui/components/extension/index.ts @@ -19,6 +19,9 @@ import EditGas from './edit-gas' import SitePermissions from './site-permissions-panel' import ConnectedAccountItem from './connected-account-item' import AddSuggestedTokenPanel from './add-suggested-token-panel' +import TransactionsPanel from './transactions-panel' +import TransactionsListItem from './transaction-list-item' +import TransactionDetailPanel from './transaction-detail-panel' import { NavButton } from './buttons' export { @@ -43,5 +46,8 @@ export { EditGas, SitePermissions, ConnectedAccountItem, - AddSuggestedTokenPanel + AddSuggestedTokenPanel, + TransactionsPanel, + TransactionsListItem, + TransactionDetailPanel } diff --git a/components/brave_wallet_ui/components/extension/panel-tooltip/style.ts b/components/brave_wallet_ui/components/extension/panel-tooltip/style.ts index 691eea4d833e..e8813b4c50c1 100644 --- a/components/brave_wallet_ui/components/extension/panel-tooltip/style.ts +++ b/components/brave_wallet_ui/components/extension/panel-tooltip/style.ts @@ -15,7 +15,7 @@ export const Tip = styled.div` position: absolute; border-radius: 4px; left: 50%; - transform: ${(p) => p.position === 'right' ? 'translateX(calc(-50% + 30px))' : 'translateX(calc(-50% - 30px))'} translateY(25%); + transform: ${(p) => p.position === 'right' ? 'translateX(calc(-50% + 40px))' : 'translateX(calc(-50% - 40px))'} translateY(25%); padding: 6px; color: ${(p) => p.theme.palette.white}; background: ${(p) => p.theme.palette.black}; diff --git a/components/brave_wallet_ui/components/extension/shared-panel-styles.ts b/components/brave_wallet_ui/components/extension/shared-panel-styles.ts index 8a5c39b49243..84c7726874d7 100644 --- a/components/brave_wallet_ui/components/extension/shared-panel-styles.ts +++ b/components/brave_wallet_ui/components/extension/shared-panel-styles.ts @@ -95,3 +95,28 @@ export const TabRow = styled.div` width: 255px; margin-bottom: 10px; ` + +export const DetailTextDarkBold = styled.span` + font-family: Poppins; + font-size: 12px; + line-height: 18px; + letter-spacing: 0.01em; + font-weight: 600; + color: ${(p) => p.theme.color.text02}; +` + +export const DetailTextLight = styled.span` + font-family: Poppins; + font-size: 12px; + line-height: 18px; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.text03}; +` + +export const DetailTextDark = styled.span` + font-family: Poppins; + font-size: 12px; + line-height: 18px; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.text02}; +` diff --git a/components/brave_wallet_ui/components/extension/transaction-detail-panel/index.tsx b/components/brave_wallet_ui/components/extension/transaction-detail-panel/index.tsx new file mode 100644 index 000000000000..3256c38f371d --- /dev/null +++ b/components/brave_wallet_ui/components/extension/transaction-detail-panel/index.tsx @@ -0,0 +1,228 @@ +import * as React from 'react' +import * as EthereumBlockies from 'ethereum-blockies' +import { useTransactionParser } from '../../../common/hooks' + +// Utils +import { reduceAddress } from '../../../utils/reduce-address' +import { getTransactionStatusString } from '../../../utils/tx-utils' +import { toProperCase } from '../../../utils/string-utils' +import { mojoTimeDeltaToJSDate } from '../../../utils/datetime-utils' +import { formatFiatAmountWithCommasAndDecimals, formatWithCommasAndDecimals } from '../../../utils/format-prices' + +import { getLocale } from '../../../../common/locale' +import { + BraveWallet, + WalletAccountType, + DefaultCurrencies +} from '../../../constants/types' +import Header from '../../buy-send-swap/select-header' + +// Styled Components +import { + StyledWrapper, + OrbContainer, + FromCircle, + ToCircle, + DetailRow, + DetailTitle, + DetailButton, + StatusRow, + BalanceColumn, + TransactionValue, + PanelDescription, + SpacerText, + FromToRow, + AccountNameText, + ArrowIcon +} from './style' + +import { + DetailTextDarkBold, + DetailTextDark +} from '../shared-panel-styles' + +import { StatusBubble } from '../../shared/style' + +export interface Props { + transaction: BraveWallet.TransactionInfo + selectedNetwork: BraveWallet.EthereumChain + accounts: WalletAccountType[] + visibleTokens: BraveWallet.ERCToken[] + transactionSpotPrices: BraveWallet.AssetPrice[] + defaultCurrencies: DefaultCurrencies + onBack: () => void + onRetryTransaction: (transaction: BraveWallet.TransactionInfo) => void + onSpeedupTransaction: (transaction: BraveWallet.TransactionInfo) => void + onCancelTransaction: (transaction: BraveWallet.TransactionInfo) => void +} + +const TransactionDetailPanel = (props: Props) => { + const { + transaction, + selectedNetwork, + accounts, + visibleTokens, + transactionSpotPrices, + defaultCurrencies, + onBack, + onRetryTransaction, + onSpeedupTransaction, + onCancelTransaction + } = props + + const parseTransaction = useTransactionParser(selectedNetwork, accounts, transactionSpotPrices, visibleTokens) + const transactionDetails = React.useMemo( + () => parseTransaction(transaction), + [transaction] + ) + + const fromOrb = React.useMemo(() => { + return EthereumBlockies.create({ seed: transactionDetails.sender.toLowerCase(), size: 8, scale: 16 }).toDataURL() + }, [transactionDetails.sender]) + + const toOrb = React.useMemo(() => { + return EthereumBlockies.create({ seed: transactionDetails.recipient.toLowerCase(), size: 8, scale: 16 }).toDataURL() + }, [transactionDetails.recipient]) + + const onClickViewOnBlockExplorer = () => { + const explorerURL = selectedNetwork.blockExplorerUrls[0] + if (explorerURL && transaction?.txHash) { + const url = `${explorerURL}/tx/${transaction.txHash}` + window.open(url, '_blank') + } else { + alert(getLocale('braveWalletTransactionExplorerMissing')) + } + } + + const onClickRetryTransaction = () => { + if (transaction) { + onRetryTransaction(transaction) + } + } + + const onClickSpeedupTransaction = () => { + if (transaction) { + onSpeedupTransaction(transaction) + } + } + + const onClickCancelTransaction = () => { + if (transaction) { + onCancelTransaction(transaction) + } + } + + const transactionTitle = React.useMemo((): string => { + if (transactionDetails.isSwap) { + return toProperCase(getLocale('braveWalletSwap')) + } + if (transaction.txType === BraveWallet.TransactionType.ERC20Approve) { + return toProperCase(getLocale('braveWalletApprovalTransactionIntent')) + } + return toProperCase(getLocale('braveWalletTransactionSent')) + }, [transactionDetails, transaction]) + + const transactionValue = React.useMemo((): string => { + if (transaction.txType === BraveWallet.TransactionType.ERC721TransferFrom || + transaction.txType === BraveWallet.TransactionType.ERC721SafeTransferFrom) { + return transactionDetails.erc721ERCToken?.name + ' ' + transactionDetails.erc721TokenId + } + return formatWithCommasAndDecimals(transactionDetails.value) + ' ' + transactionDetails.symbol + }, [transactionDetails, transaction]) + + const transactionFiatValue = React.useMemo((): string => { + if (transaction.txType !== BraveWallet.TransactionType.ERC721TransferFrom && + transaction.txType !== BraveWallet.TransactionType.ERC721SafeTransferFrom && + transaction.txType !== BraveWallet.TransactionType.ERC20Approve) { + return formatFiatAmountWithCommasAndDecimals(transactionDetails.fiatValue, defaultCurrencies.fiat) + } + return '' + }, [transactionDetails, transaction, defaultCurrencies]) + + return ( + +
+ + + + + + {transactionDetails.senderLabel} + + {transactionDetails.recipientLabel} + + {transactionTitle} + {transactionValue} + {transactionFiatValue} + + + {getLocale('braveWalletAllowSpendTransactionFee')} + + + {transactionDetails.gasFee} {selectedNetwork.symbol} + {formatFiatAmountWithCommasAndDecimals(transactionDetails.gasFeeFiat, defaultCurrencies.fiat)} + + + + + {getLocale('braveWalletTransactionDetailDate')} + + + {mojoTimeDeltaToJSDate(transactionDetails.createdTime).toUTCString()} + + + {transactionDetails.status !== BraveWallet.TransactionStatus.Rejected && + + + {getLocale('braveWalletTransactionDetailHash')} + + + {reduceAddress(transaction.txHash)} + + + } + + + {getLocale('braveWalletTransactionDetailNetwork')} + + + {selectedNetwork.chainName} + + + + + {getLocale('braveWalletTransactionDetailStatus')} + + + + + {getTransactionStatusString(transactionDetails.status)} + + + + {(transactionDetails.status === BraveWallet.TransactionStatus.Approved || transactionDetails.status === BraveWallet.TransactionStatus.Submitted) && + + + + {getLocale('braveWalletTransactionDetailSpeedUp')} + | + {getLocale('braveWalletBackupButtonCancel')} + + + } + {transactionDetails.status === BraveWallet.TransactionStatus.Error && + + + + {getLocale('braveWalletTransactionRetry')} + + + } + + ) +} + +export default TransactionDetailPanel diff --git a/components/brave_wallet_ui/components/extension/transaction-detail-panel/style.ts b/components/brave_wallet_ui/components/extension/transaction-detail-panel/style.ts new file mode 100644 index 000000000000..5673583c7463 --- /dev/null +++ b/components/brave_wallet_ui/components/extension/transaction-detail-panel/style.ts @@ -0,0 +1,142 @@ +import styled from 'styled-components' +import { ArrowRightIcon } from 'brave-ui/components/icons' +import { WalletButton } from '../../shared/style' + +interface StyleProps { + orb: string +} + +export const StyledWrapper = styled.div` + display: flex; + width: 100%; + flex-direction: column; + align-items: center; + justify-content: flex-start; +` + +export const TransactionValue = styled.span` + font-family: Poppins; + font-size: 18px; + line-height: 22px; + letter-spacing: 0.02em; + color: ${(p) => p.theme.color.text01}; + font-weight: 600; +` + +export const PanelDescription = styled.span` + font-family: Poppins; + font-size: 12px; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.text02}; + height: 18px; +` + +export const FromCircle = styled.div>` + width: 40px; + height: 40px; + border-radius: 100%; + background-image: url(${(p) => p.orb}); + background-size: cover; +` + +export const ToCircle = styled.div>` + width: 24px; + height: 24px; + border-radius: 100%; + background-image: url(${(p) => p.orb}); + background-size: cover; + position: absolute; + left: 24px; +` + +export const OrbContainer = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + position: relative; + padding-right: 12px; + margin-bottom: 10px; +` + +export const DetailRow = styled.div` + display: flex; + width: 100%; + flex-direction: row; + align-items: flex-start; + justify-content: space-between; + padding: 6px; +` + +export const DetailTitle = styled.span` + font-family: Poppins; + font-size: 12px; + line-height: 18px; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.text01}; + font-weight: 600; +` + +export const SpacerText = styled.span` + font-family: Poppins; + font-size: 12px; + line-height: 18px; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.text02}; + font-weight: 600; + margin: 0px 6px; +` + +export const DetailButton = styled(WalletButton)` + font-family: Poppins; + font-size: 12px; + line-height: 18px; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.interactive05}; + background: none; + cursor: pointer; + outline: none; + border: none; + margin: 0px; + padding: 0px; +` + +export const StatusRow = styled.div` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +` + +export const BalanceColumn = styled.div` + display: flex; + flex-direction: column; + align-items: flex-end; + justify-content: center; +` + +export const FromToRow = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + width: 100%; + margin-bottom: 8px; +` + +export const ArrowIcon = styled(ArrowRightIcon)` + width: auto; + height: 16px; + margin-right: 6px; + margin-left: 6px; + color: ${(p) => p.theme.color.text03}; +` + +export const AccountNameText = styled.span` + font-family: Poppins; + font-size: 13px; + line-height: 20px; + font-weight: 600; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.text02}; +` diff --git a/components/brave_wallet_ui/components/extension/transaction-list-item/index.tsx b/components/brave_wallet_ui/components/extension/transaction-list-item/index.tsx new file mode 100644 index 000000000000..d905ab75e3ec --- /dev/null +++ b/components/brave_wallet_ui/components/extension/transaction-list-item/index.tsx @@ -0,0 +1,207 @@ +import * as React from 'react' +import * as EthereumBlockies from 'ethereum-blockies' + +import { getLocale } from '../../../../common/locale' +import { + BraveWallet, + WalletAccountType, + DefaultCurrencies +} from '../../../constants/types' + +// Utils +import { toProperCase } from '../../../utils/string-utils' +import { getTransactionStatusString } from '../../../utils/tx-utils' +import { mojoTimeDeltaToJSDate, formatDateAsRelative } from '../../../utils/datetime-utils' +import { formatFiatAmountWithCommasAndDecimals } from '../../../utils/format-prices' + +// Hooks +import { useTransactionParser } from '../../../common/hooks' +import { SwapExchangeProxy } from '../../../common/hooks/address-labels' + +// Styled Components +import { + DetailTextDarkBold, + DetailTextDark, + DetailTextLight +} from '../shared-panel-styles' + +import { StatusBubble } from '../../shared/style' + +import { + ArrowIcon, + BalanceColumn, + DetailColumn, + DetailRow, + FromCircle, + StatusRow, + StyledWrapper, + ToCircle, + TransactionDetailRow +} from './style' + +export interface Props { + selectedNetwork: BraveWallet.EthereumChain + transaction: BraveWallet.TransactionInfo + account?: WalletAccountType + accounts: WalletAccountType[] + visibleTokens: BraveWallet.ERCToken[] + transactionSpotPrices: BraveWallet.AssetPrice[] + defaultCurrencies: DefaultCurrencies + onSelectTransaction: (transaction: BraveWallet.TransactionInfo) => void +} + +const TransactionsListItem = (props: Props) => { + const { + transaction, + selectedNetwork, + visibleTokens, + transactionSpotPrices, + accounts, + defaultCurrencies, + onSelectTransaction + } = props + + const parseTransaction = useTransactionParser(selectedNetwork, accounts, transactionSpotPrices, visibleTokens) + const transactionDetails = React.useMemo( + () => parseTransaction(transaction), + [transaction] + ) + + const fromOrb = React.useMemo(() => { + return EthereumBlockies.create({ seed: transactionDetails.sender.toLowerCase(), size: 8, scale: 16 }).toDataURL() + }, [transactionDetails.sender]) + + const toOrb = React.useMemo(() => { + return EthereumBlockies.create({ seed: transactionDetails.recipient.toLowerCase(), size: 8, scale: 16 }).toDataURL() + }, [transactionDetails.recipient]) + + const onClickTransaction = () => { + onSelectTransaction(transaction) + } + + const transactionIntentLocale = React.useMemo((): string => { + switch (true) { + case transaction.txType === BraveWallet.TransactionType.ERC20Approve: { + const text = getLocale('braveWalletApprovalTransactionIntent') + return `${toProperCase(text)} ${transactionDetails.symbol}` + } + + // Detect sending to 0x Exchange Proxy + case transactionDetails.isSwap: { + return getLocale('braveWalletSwap') + } + + case transaction.txType === BraveWallet.TransactionType.ETHSend: + case transaction.txType === BraveWallet.TransactionType.ERC20Transfer: + case transaction.txType === BraveWallet.TransactionType.ERC721TransferFrom: + case transaction.txType === BraveWallet.TransactionType.ERC721SafeTransferFrom: + default: { + const text = getLocale('braveWalletTransactionSent') + const erc721ID = transaction.txType === BraveWallet.TransactionType.ERC721TransferFrom || + transaction.txType === BraveWallet.TransactionType.ERC721SafeTransferFrom + ? ' ' + transactionDetails.erc721TokenId + : '' + return `${toProperCase(text)} ${transactionDetails.symbol}${erc721ID}` + } + } + }, [transaction]) + + const transactionIntentDescription = React.useMemo(() => { + switch (true) { + case transaction.txType === BraveWallet.TransactionType.ERC20Approve: { + return ( + <> + + + {transactionDetails.value}{' '} + {transactionDetails.symbol} + + + + + + {transactionDetails.approvalTargetLabel} + + + + + ) + } + + // FIXME: Add as new TransactionType on the controller side. + case transaction.txData.baseData.to.toLowerCase() === SwapExchangeProxy: { + return ( + <> + + + {transactionDetails.value}{' '} + {transactionDetails.symbol} + + + + + + {transactionDetails.recipientLabel} + + + + ) + } + + case transaction.txType === BraveWallet.TransactionType.ETHSend: + case transaction.txType === BraveWallet.TransactionType.ERC20Transfer: + case transaction.txType === BraveWallet.TransactionType.ERC721TransferFrom: + case transaction.txType === BraveWallet.TransactionType.ERC721SafeTransferFrom: + default: { + return ( + <> + + + {transactionDetails.senderLabel}{' '} + + + + + + {transactionDetails.recipientLabel} + + + + ) + } + } + }, [transactionDetails]) + + return ( + + + + + + + + {transactionIntentLocale} +   + -  + {formatDateAsRelative(mojoTimeDeltaToJSDate(transactionDetails.createdTime))} + + {transactionIntentDescription} + + + + + {formatFiatAmountWithCommasAndDecimals(transactionDetails.fiatValue, defaultCurrencies.fiat)} + {transactionDetails.nativeCurrencyTotal} {selectedNetwork.symbol} + + + + {getTransactionStatusString(transactionDetails.status)} + + + + + + ) +} + +export default TransactionsListItem diff --git a/components/brave_wallet_ui/components/extension/transaction-list-item/style.ts b/components/brave_wallet_ui/components/extension/transaction-list-item/style.ts new file mode 100644 index 000000000000..3248e57a09d4 --- /dev/null +++ b/components/brave_wallet_ui/components/extension/transaction-list-item/style.ts @@ -0,0 +1,106 @@ +import styled from 'styled-components' +import { ArrowRightIcon } from 'brave-ui/components/icons' +import { WalletButton } from '../../shared/style' + +interface StyleProps { + orb: string +} + +export const StyledWrapper = styled(WalletButton)` + display: flex; + align-items: center; + justify-content: space-between; + flex-direction: row; + width: 100%; + margin: 8px 0px; + cursor: pointer; + outline: none; + background: none; + border: none; + border-radius: 8px; + padding: 5px; + &:hover { + background-color: ${(p) => p.theme.color.background02}; + } +` + +export const DetailRow = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; +` + +export const AddressText = styled.span` + font-family: Poppins; + font-size: 13px; + line-height: 20px; + letter-spacing: 0.01em; + font-weight: 600; + color: ${(p) => p.theme.color.text01}; + margin: 0px 5px; +` + +export const DetailText = styled.span` + font-family: Poppins; + font-size: 13px; + line-height: 20px; + letter-spacing: 0.01em; + font-weight: 400; + color: ${(p) => p.theme.color.text02}; +` + +export const FromCircle = styled.div>` + width: 40px; + height: 40px; + border-radius: 100%; + background-image: url(${(p) => p.orb}); + background-size: cover; + margin-right: 20px; +` + +export const ToCircle = styled.div>` + width: 24px; + height: 24px; + border-radius: 100%; + background-image: url(${(p) => p.orb}); + background-size: cover; + position: absolute; + left: 42px; +` + +export const DetailColumn = styled.div` + display: flex; + align-items: flex-start; + justify-content: center; + flex-direction: column; +` + +export const BalanceColumn = styled.div` + display: flex; + align-items: flex-end; + justify-content: center; + flex-direction: column; +` + +export const ArrowIcon = styled(ArrowRightIcon)` + width: auto; + height: 16px; + margin-right: 6px; + color: ${(p) => p.theme.color.text03}; +` + +export const TransactionDetailRow = styled.div` + display: flex; + align-items: center; + justify-content: flex-start; + flex-direction: row; +` + +export const StatusRow = styled.div` + flex: 1; + display: flex; + align-items: center; + justify-content: flex-start; + flex-direction: row; +` diff --git a/components/brave_wallet_ui/components/extension/transactions-panel/index.tsx b/components/brave_wallet_ui/components/extension/transactions-panel/index.tsx new file mode 100644 index 000000000000..342883d7534c --- /dev/null +++ b/components/brave_wallet_ui/components/extension/transactions-panel/index.tsx @@ -0,0 +1,72 @@ +import * as React from 'react' + +import { + BraveWallet, + WalletAccountType, + DefaultCurrencies, + AccountTransactions +} from '../../../constants/types' + +// Styled Components +import { + StyledWrapper +} from './style' + +import { TransactionsListItem } from '../' +import { sortTransactionByDate } from '../../../utils/tx-utils' + +export interface Props { + selectedNetwork: BraveWallet.EthereumChain + selectedAccount: WalletAccountType + transactions: AccountTransactions + accounts: WalletAccountType[] + visibleTokens: BraveWallet.ERCToken[] + transactionSpotPrices: BraveWallet.AssetPrice[] + defaultCurrencies: DefaultCurrencies + onSelectTransaction: (transaction: BraveWallet.TransactionInfo) => void +} + +const TransactionsPanel = (props: Props) => { + const { + transactions, + selectedNetwork, + visibleTokens, + transactionSpotPrices, + accounts, + defaultCurrencies, + selectedAccount, + onSelectTransaction + } = props + + const findAccount = (address: string): WalletAccountType | undefined => { + return accounts.find((account) => address === account.address) + } + + const transactionList = React.useMemo(() => { + if (selectedAccount?.address && transactions[selectedAccount.address]) { + return sortTransactionByDate(transactions[selectedAccount.address], 'descending') + } else { + return [] + } + }, [selectedAccount, transactions]) + + return ( + + {transactionList.map((transaction: BraveWallet.TransactionInfo) => + + )} + + ) +} + +export default TransactionsPanel diff --git a/components/brave_wallet_ui/components/extension/transactions-panel/style.ts b/components/brave_wallet_ui/components/extension/transactions-panel/style.ts new file mode 100644 index 000000000000..5ff4d0d241d4 --- /dev/null +++ b/components/brave_wallet_ui/components/extension/transactions-panel/style.ts @@ -0,0 +1,10 @@ +import styled from 'styled-components' + +export const StyledWrapper = styled.div` + display: flex; + height: 100%; + width: 100%; + flex-direction: column; + align-items: center; + justify-content: flex-start; +` diff --git a/components/brave_wallet_ui/components/shared/style.ts b/components/brave_wallet_ui/components/shared/style.ts index 6e2b62fab328..2f9eaeb86c0d 100644 --- a/components/brave_wallet_ui/components/shared/style.ts +++ b/components/brave_wallet_ui/components/shared/style.ts @@ -1,5 +1,5 @@ import styled from 'styled-components' - +import { BraveWallet } from '../../constants/types' import transparent40x40Image from '../../assets/png-icons/transparent40x40.png' import { stripERC20TokenImageURL } from '../../utils/string-utils' @@ -29,3 +29,28 @@ export const WalletButton = styled.button` outline-width: 2px; } ` + +interface StyleProps { + status: BraveWallet.TransactionStatus +} + +export const StatusBubble = styled.div>` + display: flex; + align-items: center; + justify-content: center; + width: 10px; + height: 10px; + border-radius: 100%; + opacity: ${(p) => p.status === BraveWallet.TransactionStatus.Submitted || + p.status === BraveWallet.TransactionStatus.Approved || + p.status === BraveWallet.TransactionStatus.Unapproved + ? 0.4 + : 1 + }; + background-color: ${(p) => p.status === BraveWallet.TransactionStatus.Confirmed || p.status === BraveWallet.TransactionStatus.Approved + ? p.theme.color.successBorder + : p.status === BraveWallet.TransactionStatus.Rejected || p.status === BraveWallet.TransactionStatus.Error ? p.theme.color.errorBorder + : p.status === BraveWallet.TransactionStatus.Unapproved ? p.theme.color.interactive08 : p.theme.color.warningIcon + }; + margin-right: 6px; +` diff --git a/components/brave_wallet_ui/constants/types.ts b/components/brave_wallet_ui/constants/types.ts index acc021ba590e..5522a3b0134c 100644 --- a/components/brave_wallet_ui/constants/types.ts +++ b/components/brave_wallet_ui/constants/types.ts @@ -102,6 +102,8 @@ export type PanelTypes = | 'showUnlock' | 'sitePermissions' | 'addSuggestedToken' + | 'transactions' + | 'transactionDetails' export type NavTypes = | 'crypto' diff --git a/components/brave_wallet_ui/options/panel-titles.ts b/components/brave_wallet_ui/options/panel-titles.ts index f1875b6d5c7b..bc9ca2462483 100644 --- a/components/brave_wallet_ui/options/panel-titles.ts +++ b/components/brave_wallet_ui/options/panel-titles.ts @@ -21,5 +21,13 @@ export const PanelTitles = (): PanelTitleObjectType[] => [ { id: 'sitePermissions', title: getLocale('braveWalletSitePermissionsTitle') + }, + { + id: 'transactions', + title: getLocale('braveWalletRecentTransactions') + }, + { + id: 'transactionDetails', + title: getLocale('braveWalletTransactionDetails') } ] diff --git a/components/brave_wallet_ui/panel/container.tsx b/components/brave_wallet_ui/panel/container.tsx index beb88b612100..702ffdb84071 100644 --- a/components/brave_wallet_ui/panel/container.tsx +++ b/components/brave_wallet_ui/panel/container.tsx @@ -16,7 +16,9 @@ import { ConfirmTransactionPanel, ConnectHardwareWalletPanel, SitePermissions, - AddSuggestedTokenPanel + AddSuggestedTokenPanel, + TransactionsPanel, + TransactionDetailPanel } from '../components/extension' import { Send, @@ -104,7 +106,8 @@ function Container (props: Props) { connectedAccounts, activeOrigin, pendingTransactions, - defaultCurrencies + defaultCurrencies, + transactions } = props.wallet const { @@ -126,6 +129,7 @@ function Container (props: Props) { const [selectedAccounts, setSelectedAccounts] = React.useState([]) const [filteredAppsList, setFilteredAppsList] = React.useState(AppsList) const [selectedWyreAsset, setSelectedWyreAsset] = React.useState(WyreAccountAssetOptions[0]) + const [selectedTransaction, setSelectedTransaction] = React.useState() const [showSelectAsset, setShowSelectAsset] = React.useState(false) const [buyAmount, setBuyAmount] = React.useState('') @@ -503,6 +507,27 @@ function Container (props: Props) { }) } + const onSelectTransaction = (transaction: BraveWallet.TransactionInfo) => { + setSelectedTransaction(transaction) + props.walletPanelActions.navigateTo('transactionDetails') + } + + const onRetryTransaction = (transaction: BraveWallet.TransactionInfo) => { + props.walletActions.retryTransaction(transaction) + } + + const onSpeedupTransaction = (transaction: BraveWallet.TransactionInfo) => { + props.walletActions.speedupTransaction(transaction) + } + + const onCancelTransaction = (transaction: BraveWallet.TransactionInfo) => { + props.walletActions.cancelTransaction(transaction) + } + + const onGoBackToTransactions = () => { + props.walletPanelActions.navigateTo('transactions') + } + const isConnectedToSite = React.useMemo((): boolean => { if (activeOrigin === WalletOrigin) { return true @@ -861,6 +886,54 @@ function Container (props: Props) { ) } + if (selectedPanel === 'transactionDetails' && selectedTransaction) { + return ( + + + + + + ) + } + + if (selectedPanel === 'transactions') { + return ( + + + + + + + + + + ) + } + if (selectedPanel === 'sitePermissions') { return ( diff --git a/components/brave_wallet_ui/stories/locale.ts b/components/brave_wallet_ui/stories/locale.ts index 89db69160694..7f738fd65793 100644 --- a/components/brave_wallet_ui/stories/locale.ts +++ b/components/brave_wallet_ui/stories/locale.ts @@ -403,5 +403,22 @@ provideStrings({ // Add Suggested Token Panel braveWalletAddSuggestedTokenTitle: 'Add suggested token', - braveWalletAddSuggestedTokenDescription: 'Would you like to import this token?' + braveWalletAddSuggestedTokenDescription: 'Would you like to import this token?', + + // Transaction Detail Panel + braveWalletRecentTransactions: 'Recent transactions', + braveWalletTransactionDetails: 'Transaction details', + braveWalletTransactionDetailDate: 'Date', + braveWalletTransactionDetailSpeedUp: 'Speedup', + braveWalletTransactionDetailHash: 'Transaction hash', + braveWalletTransactionDetailNetwork: 'Network', + braveWalletTransactionDetailStatus: 'Status', + + // Transactions Status + braveWalletTransactionStatusUnapproved: 'Unapproved', + braveWalletTransactionStatusApproved: 'Approved', + braveWalletTransactionStatusRejected: 'Rejected', + braveWalletTransactionStatusSubmitted: 'Submitted', + braveWalletTransactionStatusConfirmed: 'Confirmed', + braveWalletTransactionStatusError: 'Error' }) diff --git a/components/brave_wallet_ui/stories/wallet-concept.tsx b/components/brave_wallet_ui/stories/wallet-concept.tsx index 9d39b4a5a997..e8d67feee521 100644 --- a/components/brave_wallet_ui/stories/wallet-concept.tsx +++ b/components/brave_wallet_ui/stories/wallet-concept.tsx @@ -55,7 +55,7 @@ export default { } } -const transactionDummyData: AccountTransactions = { +export const transactionDummyData: AccountTransactions = { [mockUserAccounts[0].id]: [ { fromAddress: '0x7d66c9ddAED3115d93Bd1790332f3Cd06Cf52B14', diff --git a/components/brave_wallet_ui/stories/wallet-extension-panels.tsx b/components/brave_wallet_ui/stories/wallet-extension-panels.tsx index ddc760962a75..ec2e2ed8e1ae 100644 --- a/components/brave_wallet_ui/stories/wallet-extension-panels.tsx +++ b/components/brave_wallet_ui/stories/wallet-extension-panels.tsx @@ -11,7 +11,9 @@ import { ConfirmTransactionPanel, ConnectHardwareWalletPanel, SitePermissions, - AddSuggestedTokenPanel + AddSuggestedTokenPanel, + TransactionsPanel, + TransactionDetailPanel } from '../components/extension' import { AppList } from '../components/shared' import { @@ -48,6 +50,7 @@ import { mockNetworks } from './mock-data/mock-networks' import { AccountAssetOptions, NewAssetOptions } from '../options/asset-options' import { PanelTitles } from '../options/panel-titles' import './locale' +import { transactionDummyData } from './wallet-concept' export default { title: 'Wallet/Extension/Panels', parameters: { @@ -319,6 +322,21 @@ _ConnectWithSite.story = { } export const _ConnectedPanel = (args: { locked: boolean }) => { + const transactionDummyAccounts: WalletAccountType[] = [ + { + id: '1', + name: 'Account 1', + address: '1', + balance: '0.31178', + asset: 'eth', + fiatBalance: '0', + accountType: 'Primary', + tokens: [] + } + ] + const transactionList = { + [transactionDummyAccounts[0].address]: [...transactionDummyData[1]].concat(...transactionDummyData[2]) + } const { locked } = args const [inputValue, setInputValue] = React.useState('') const [walletLocked, setWalletLocked] = React.useState(locked) @@ -339,6 +357,7 @@ export const _ConnectedPanel = (args: { locked: boolean }) => { const [toAddress, setToAddress] = React.useState('') const [fromAmount, setFromAmount] = React.useState('') const [buyAmount, setBuyAmount] = React.useState('') + const [selectedTransaction, setSelectedTransaction] = React.useState(transactionList[1][0]) const onSetBuyAmount = (value: string) => { setBuyAmount(value) @@ -361,6 +380,10 @@ export const _ConnectedPanel = (args: { locked: boolean }) => { setSelectedPanel('main') } + const onBackToTransactions = () => { + navigateTo('transactions') + } + const onSelectNetwork = (network: BraveWallet.EthereumChain) => () => { setSelectedNetwork(network) setSelectedPanel('main') @@ -479,8 +502,29 @@ export const _ConnectedPanel = (args: { locked: boolean }) => { alert('Will redirect to brave://wallet/crypto/portfolio/add-asset') } + const onClickRetryTransaction = () => { + // Does nothing in storybook + alert('Will retry transaction') + } + + const onClickCancelTransaction = () => { + // Does nothing in storybook + alert('Will cancel transaction') + } + + const onClickSpeedupTransaction = () => { + // Does nothing in storybook + alert('Will speedup transaction') + } + const connectedAccounts = accounts.slice(0, 2) + const onSelectTransaction = (transaction: BraveWallet.TransactionInfo) => { + navigateTo('transactionDetails') + setSelectedTransaction(transaction) + console.log(selectedTransaction) + } + return ( {walletLocked ? ( @@ -539,8 +583,24 @@ export const _ConnectedPanel = (args: { locked: boolean }) => { /> } - {!showSelectAsset && selectedPanel !== 'networks' && selectedPanel !== 'accounts' && - + + + } + {!showSelectAsset && selectedPanel !== 'networks' && selectedPanel !== 'accounts' && selectedPanel !== 'transactionDetails' && + < Panel navAction={navigateTo} title={panelTitle} useSearch={selectedPanel === 'apps'} @@ -595,14 +655,28 @@ export const _ConnectedPanel = (args: { locked: boolean }) => { onAddAccount={onAddAccount} /> } + {selectedPanel === 'transactions' && + + } } )} - )} - + ) + } + ) } diff --git a/components/brave_wallet_ui/utils/format-prices.test.ts b/components/brave_wallet_ui/utils/format-prices.test.ts index 0ac80659b657..5d571cc5ab55 100644 --- a/components/brave_wallet_ui/utils/format-prices.test.ts +++ b/components/brave_wallet_ui/utils/format-prices.test.ts @@ -14,6 +14,11 @@ describe('Check Formating with Commas and Decimals', () => { const value = '0' expect(formatWithCommasAndDecimals(value)).toEqual('0.00') }) + + test('Value is Unlimited should return Unlimited', () => { + const value = 'Unlimited' + expect(formatWithCommasAndDecimals(value)).toEqual('Unlimited') + }) }) describe('Check Formating with Commas and Decimals for Fiat', () => { diff --git a/components/brave_wallet_ui/utils/format-prices.ts b/components/brave_wallet_ui/utils/format-prices.ts index 0b2f7c5cf15c..2d85a6f6f6b4 100644 --- a/components/brave_wallet_ui/utils/format-prices.ts +++ b/components/brave_wallet_ui/utils/format-prices.ts @@ -13,6 +13,11 @@ export const formatWithCommasAndDecimals = (value: string) => { return '' } + // We some times return Unlimited as a value + if (isNaN(Number(value))) { + return value + } + const valueToNumber = Number(value) if (valueToNumber === 0) { diff --git a/components/brave_wallet_ui/utils/tx-utils.test.ts b/components/brave_wallet_ui/utils/tx-utils.test.ts new file mode 100644 index 000000000000..14c72b9d46ad --- /dev/null +++ b/components/brave_wallet_ui/utils/tx-utils.test.ts @@ -0,0 +1,30 @@ +import { getTransactionStatusString } from './tx-utils' + +describe('Check Transaction Status Strings Value', () => { + test('Transaction ID 0 should return Unapproved', () => { + expect(getTransactionStatusString(0)).toEqual('braveWalletTransactionStatusUnapproved') + }) + test('Transaction ID 1 should return Approved', () => { + expect(getTransactionStatusString(1)).toEqual('braveWalletTransactionStatusApproved') + }) + + test('Transaction ID 2 should return Rejected', () => { + expect(getTransactionStatusString(2)).toEqual('braveWalletTransactionStatusRejected') + }) + + test('Transaction ID 3 should return Submitted', () => { + expect(getTransactionStatusString(3)).toEqual('braveWalletTransactionStatusSubmitted') + }) + + test('Transaction ID 4 should return Confirmed', () => { + expect(getTransactionStatusString(4)).toEqual('braveWalletTransactionStatusConfirmed') + }) + + test('Transaction ID 5 should return Error', () => { + expect(getTransactionStatusString(5)).toEqual('braveWalletTransactionStatusError') + }) + + test('Transaction ID 6 should return an empty string', () => { + expect(getTransactionStatusString(6)).toEqual('') + }) +}) diff --git a/components/brave_wallet_ui/utils/tx-utils.ts b/components/brave_wallet_ui/utils/tx-utils.ts index 701db9e60120..4852677f8e5f 100644 --- a/components/brave_wallet_ui/utils/tx-utils.ts +++ b/components/brave_wallet_ui/utils/tx-utils.ts @@ -1,4 +1,5 @@ import { BraveWallet } from '../constants/types' +import { getLocale } from '../../common/locale' type Order = 'ascending' | 'descending' @@ -9,3 +10,22 @@ export const sortTransactionByDate = (transactions: BraveWallet.TransactionInfo[ : Number(y.createdTime.microseconds) - Number(x.createdTime.microseconds) }) } + +export const getTransactionStatusString = (statusId: number) => { + switch (statusId) { + case BraveWallet.TransactionStatus.Unapproved: + return getLocale('braveWalletTransactionStatusUnapproved') + case BraveWallet.TransactionStatus.Approved: + return getLocale('braveWalletTransactionStatusApproved') + case BraveWallet.TransactionStatus.Rejected: + return getLocale('braveWalletTransactionStatusRejected') + case BraveWallet.TransactionStatus.Submitted: + return getLocale('braveWalletTransactionStatusSubmitted') + case BraveWallet.TransactionStatus.Confirmed: + return getLocale('braveWalletTransactionStatusConfirmed') + case BraveWallet.TransactionStatus.Error: + return getLocale('braveWalletTransactionStatusError') + default: + return '' + } +} diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index 0ec5c0a6c5bf..0fc2caa5f68a 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -299,6 +299,13 @@ Submitted Confirmed Error + Recent transactions + Transaction details + Date + Speedup + Transaction hash + Network + Status Max priority fee Edit gas While not a guarantee, miners will likely prioritize your transaction if you pay a higher fee.