From a2f49f2196db5872d1eb5cdae20a9441e469b92f Mon Sep 17 00:00:00 2001 From: Douglas Daniel Date: Mon, 28 Feb 2022 15:43:20 -0700 Subject: [PATCH] feat(wallet): Backed Up Transactions Warning --- .../browser/brave_wallet_constants.h | 10 ++ .../brave_wallet_ui/common/async/handlers.ts | 1 + .../confirm-transaction-panel/index.tsx | 74 +++++++++++++-- .../components/extension/index.ts | 4 +- .../transactions-backed-up-warning/index.tsx | 93 +++++++++++++++++++ .../transactions-backed-up-warning/style.ts | 89 ++++++++++++++++++ .../brave_wallet_ui/panel/container.tsx | 2 + components/brave_wallet_ui/stories/locale.ts | 7 ++ .../stories/wallet-extension-panels.tsx | 32 ++++--- .../brave_wallet_ui/utils/datetime-utils.ts | 6 +- components/resources/wallet_strings.grdp | 5 + 11 files changed, 301 insertions(+), 22 deletions(-) create mode 100644 components/brave_wallet_ui/components/extension/transactions-backed-up-warning/index.tsx create mode 100644 components/brave_wallet_ui/components/extension/transactions-backed-up-warning/style.ts diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index cf59a3589e19..882a702ae416 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -415,6 +415,16 @@ constexpr webui::LocalizedString kLocalizedStrings[] = { {"braveWalletConfirmTransactionFrist", IDS_BRAVE_WALLET_CONFIRM_TRANSACTION_FRIST}, {"braveWalletConfirmTransactions", IDS_BRAVE_WALLET_CONFIRM_TRANSACTIONS}, + {"braveWalletBackedUpTransactionsTitle", + IDS_BRAVE_WALLET_BACKED_UP_TRANSACTIONS_TITLE}, + {"braveWalletBackedUpTransactionsDescription", + IDS_BRAVE_WALLET_BACKED_UP_TRANSACTIONS_DESCRIPTION}, + {"braveWalletBackedUpTransactionsCheckbox", + IDS_BRAVE_WALLET_BACKED_UP_TRANSACTIONS_CHECKBOX}, + {"braveWalletBackedUpTransactionsClearButton", + IDS_BRAVE_WALLET_BACKED_UP_TRANSACTIONS_CLEAR_BUTTON}, + {"braveWalletBackedUpTransactionsClearingButton", + IDS_BRAVE_WALLET_BACKED_UP_TRANSACTIONS_CLEARING_BUTTON}, {"braveWalletPanelTitle", IDS_BRAVE_WALLET_PANEL_TITLE}, {"braveWalletPanelConnected", IDS_BRAVE_WALLET_PANEL_CONNECTED}, {"braveWalletSitePermissionsAccounts", diff --git a/components/brave_wallet_ui/common/async/handlers.ts b/components/brave_wallet_ui/common/async/handlers.ts index a803e7501c85..5584ed3f139c 100644 --- a/components/brave_wallet_ui/common/async/handlers.ts +++ b/components/brave_wallet_ui/common/async/handlers.ts @@ -627,6 +627,7 @@ handler.on(WalletActions.addSitePermission.getType(), async (store: Store, paylo handler.on(WalletActions.transactionStatusChanged.getType(), async (store: Store, payload: TransactionStatusChanged) => { const status = payload.txInfo.txStatus + await store.dispatch(refreshTransactionHistory(payload.txInfo.fromAddress)) if (status === BraveWallet.TransactionStatus.Confirmed || status === BraveWallet.TransactionStatus.Error) { await refreshBalancesPricesAndHistory(store) } diff --git a/components/brave_wallet_ui/components/extension/confirm-transaction-panel/index.tsx b/components/brave_wallet_ui/components/extension/confirm-transaction-panel/index.tsx index 9eebbc03560c..0d4b30394846 100644 --- a/components/brave_wallet_ui/components/extension/confirm-transaction-panel/index.tsx +++ b/components/brave_wallet_ui/components/extension/confirm-transaction-panel/index.tsx @@ -4,7 +4,8 @@ import { create } from 'ethereum-blockies' import { BraveWallet, WalletAccountType, - DefaultCurrencies + DefaultCurrencies, + AccountTransactions } from '../../../constants/types' import { UpdateUnapprovedTransactionGasFieldsType, @@ -16,15 +17,23 @@ import { import { reduceAddress } from '../../../utils/reduce-address' import { reduceNetworkDisplayName } from '../../../utils/network-utils' import { reduceAccountDisplayName } from '../../../utils/reduce-account-name' +import { sortTransactionByDate } from '../../../utils/tx-utils' +import { mojoTimeDeltaToJSDate, calculatedTimeDiffInMilliseconds } from '../../../utils/datetime-utils' import Amount from '../../../utils/amount' // Hooks import { usePricing, useTransactionParser, useTokenInfo } from '../../../common/hooks' import { getLocale } from '../../../../common/locale' -import { withPlaceholderIcon } from '../../shared' -import { NavButton, PanelTab, TransactionDetailBox } from '../' +// Components +import { withPlaceholderIcon } from '../../shared' +import { + NavButton, + PanelTab, + TransactionDetailBox, + TransactionsBackedUpWarning +} from '../' import EditGas, { MaxPriorityPanels } from '../edit-gas' import EditAllowance from '../edit-allowance' @@ -94,6 +103,8 @@ export interface Props { transactionsQueueLength: number transactionQueueNumber: number defaultCurrencies: DefaultCurrencies + transactions: AccountTransactions + selectedAccount: WalletAccountType onQueueNextTransaction: () => void onConfirm: () => void onReject: () => void @@ -118,6 +129,8 @@ function ConfirmTransactionPanel (props: Props) { transactionQueueNumber, fullTokenList, defaultCurrencies, + transactions, + selectedAccount, onQueueNextTransaction, onConfirm, onReject, @@ -144,11 +157,39 @@ function ConfirmTransactionPanel (props: Props) { const [currentTokenAllowance, setCurrentTokenAllowance] = React.useState('') const [isEditingAllowance, setIsEditingAllowance] = React.useState(false) const [showAdvancedTransactionSettings, setShowAdvancedTransactionSettings] = React.useState(false) + const [showTransactionsBackedUpWarning, setShowTransactionsBackedUpWarning] = React.useState(false) const { findAssetPrice } = usePricing(transactionSpotPrices) const parseTransaction = useTransactionParser(selectedNetwork, accounts, transactionSpotPrices, visibleTokens, fullTokenList) const transactionDetails = parseTransaction(transactionInfo) + const unconfirmedSubmittedTransactions = React.useMemo(() => { + // Gets a the list of transcations for the selected account + const transactionsList = + selectedAccount?.address && transactions[selectedAccount.address] + ? sortTransactionByDate(transactions[selectedAccount.address], 'descending') + : [] + + // Gets a list of all transactions with the status of Submitted + const submittedTransactions = transactionsList.filter((transaction) => transaction.txStatus === BraveWallet.TransactionStatus.Submitted) + + // Returns a list of all unconfirmed submited transactions that are over 24 hours old + return submittedTransactions.filter((transaction) => + Math.floor( + calculatedTimeDiffInMilliseconds( + mojoTimeDeltaToJSDate(transaction.submittedTime) + ) / (1000 * 60 * 60)) > 24) + }, [selectedAccount, transactions]) + + React.useEffect(() => { + if ( + unconfirmedSubmittedTransactions.length !== 0 && + transactionInfo.txDataUnion.ethTxData1559?.baseData.nonce === '' + ) { + setShowTransactionsBackedUpWarning(true) + } + }, [unconfirmedSubmittedTransactions]) + const { onFindTokenInfoByContractAddress, foundTokenInfoByContractAddress @@ -261,6 +302,27 @@ function ConfirmTransactionPanel (props: Props) { ) }, [transactionDetails]) + const onContinueTransaction = (clearTransactions: boolean) => { + if (clearTransactions) { + // Finds the nonce of the oldest unconfirmed transaction and + // sets the nonce for the new unapproved transaction. + const newNonce = + unconfirmedSubmittedTransactions[unconfirmedSubmittedTransactions.length - 1] + .txDataUnion.ethTxData1559?.baseData.nonce ?? '' + + updateUnapprovedTransactionNonce({ txMetaId: transactionInfo.id, nonce: newNonce }) + } + setShowTransactionsBackedUpWarning(false) + } + + if (showTransactionsBackedUpWarning) { + return ( + + ) + } + if (isEditing) { return ( - + {getLocale('braveWalletAllowSpendUnlimitedWarningTitle')} @@ -394,7 +456,7 @@ function ConfirmTransactionPanel (props: Props) { /> @@ -439,7 +501,7 @@ function ConfirmTransactionPanel (props: Props) { transactionDetails.isApprovalUnlimited ? getLocale('braveWalletTransactionApproveUnlimited') : new Amount(transactionDetails.valueExact) - .formatAsAsset(undefined, transactionDetails.symbol) + .formatAsAsset(undefined, transactionDetails.symbol) } diff --git a/components/brave_wallet_ui/components/extension/index.ts b/components/brave_wallet_ui/components/extension/index.ts index 1d979302af9b..5b5aeed3f249 100644 --- a/components/brave_wallet_ui/components/extension/index.ts +++ b/components/brave_wallet_ui/components/extension/index.ts @@ -24,6 +24,7 @@ import TransactionsListItem from './transaction-list-item' import TransactionDetailPanel from './transaction-detail-panel' import AssetsPanel from './assets-panel' import EncryptionKeyPanel from './encryption-key-panel' +import TransactionsBackedUpWarning from './transactions-backed-up-warning' import { NavButton } from './buttons' export { @@ -53,5 +54,6 @@ export { TransactionsListItem, TransactionDetailPanel, AssetsPanel, - EncryptionKeyPanel + EncryptionKeyPanel, + TransactionsBackedUpWarning } diff --git a/components/brave_wallet_ui/components/extension/transactions-backed-up-warning/index.tsx b/components/brave_wallet_ui/components/extension/transactions-backed-up-warning/index.tsx new file mode 100644 index 000000000000..51f1b00632f0 --- /dev/null +++ b/components/brave_wallet_ui/components/extension/transactions-backed-up-warning/index.tsx @@ -0,0 +1,93 @@ +import * as React from 'react' + +// Components +import { NavButton } from '../' +import { Checkbox } from 'brave-ui' +import { getLocale } from '../../../../common/locale' + +// Styled Components +import { + StyledWrapper, + Title, + Description, + LearnMoreButton, + CheckboxRow, + ClearButton, + ClearButtonText, + LoadIcon, + CheckboxText +} from './style' + +export interface Props { + onContinue: (clearTransactions: boolean) => void +} + +function TransactionsBackedUpWarning (props: Props) { + const { onContinue } = props + const [clearTransactions, setClearTransactions] = React.useState(false) + const [isClearing, setIsClearing] = React.useState(false) + + const onCheckClearTransactions = (key: string, selected: boolean) => { + if (key === 'clearTransactions') { + setClearTransactions(selected) + } + } + + const onClickContinue = () => { + if (clearTransactions) { + setIsClearing(true) + } + onContinue(clearTransactions) + } + + const onClickLearnMore = () => { + chrome.tabs.create({ url: 'https://support.brave.com/hc/en-us/articles/4537540021389' }, () => { + if (chrome.runtime.lastError) { + console.error('tabs.create failed: ' + chrome.runtime.lastError.message) + } + }) + } + + return ( + + {getLocale('braveWalletBackedUpTransactionsTitle')} + {getLocale('braveWalletBackedUpTransactionsDescription')} + {getLocale('braveWalletWelcomePanelButton')} + + {getLocale('braveWalletBackedUpTransactionsCheckbox')}} + size='small' + value={{ clearTransactions: clearTransactions }} + onChange={onCheckClearTransactions} + /> + + { + isClearing ? ( + + + {getLocale('braveWalletBackedUpTransactionsClearingButton')} + + + + ) : ( + + ) + } + + ) +} + +export default TransactionsBackedUpWarning diff --git a/components/brave_wallet_ui/components/extension/transactions-backed-up-warning/style.ts b/components/brave_wallet_ui/components/extension/transactions-backed-up-warning/style.ts new file mode 100644 index 000000000000..a33cc6df71f0 --- /dev/null +++ b/components/brave_wallet_ui/components/extension/transactions-backed-up-warning/style.ts @@ -0,0 +1,89 @@ +import styled from 'styled-components' +import { LoaderIcon } from 'brave-ui/components/icons' +import { WalletButton } from '../../shared/style' + +export const StyledWrapper = styled.div` + display: flex; + width: 100%; + height: 100%; + flex-direction: column; + align-items: center; + justify-content: center; +` + +export const Title = styled.span` + font-family: Poppins; + font-size: 15px; + line-height: 20px; + letter-spacing: 0.01em; + font-weight: 600; + color: ${(p) => p.theme.color.text01}; + margin-bottom: 10px; +` + +export const Description = styled.span` + font-family: Poppins; + font-size: 14px; + line-height: 18px; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.text02}; + width: 80%; + text-align: center; + margin-bottom: 6px; +` + +export const LearnMoreButton = styled(WalletButton)` + cursor: pointer; + outline: none; + background: none; + border: none; + font-family: Poppins; + font-size: 14px; + line-height: 18px; + letter-spacing: 0.01em; + color: ${(p) => p.theme.color.interactive05}; + margin: 0px; + padding: 0px; +` + +export const CheckboxRow = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: row; + margin: 20px 0px; + width: 70%; +` + +export const CheckboxText = styled.span` + font-size: 8px; + line-height: 20px; + color: ${(p) => p.theme.color.text02}; +` + +export const ClearButton = styled(WalletButton)` + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 7px 22px; + border: none; + box-sizing: border-box; + border-radius: 40px; + background-color: ${(p) => p.theme.color.errorBorder}; + opacity: .4; +` + +export const ClearButtonText = styled.span` + font-weight: 600; + font-size: 13px; + line-height: 20px; + color: ${(p) => p.theme.palette.white}; + margin-right: 4px; +` + +export const LoadIcon = styled(LoaderIcon)` + color: ${p => p.theme.palette.white}; + height: 25px; + width: 25px; +` diff --git a/components/brave_wallet_ui/panel/container.tsx b/components/brave_wallet_ui/panel/container.tsx index 64cc1362c7c8..bbdf115e3365 100644 --- a/components/brave_wallet_ui/panel/container.tsx +++ b/components/brave_wallet_ui/panel/container.tsx @@ -661,6 +661,8 @@ function Container (props: Props) { updateUnapprovedTransactionNonce={props.walletActions.updateUnapprovedTransactionNonce} gasEstimates={gasEstimates} fullTokenList={props.wallet.fullTokenList} + transactions={transactions} + selectedAccount={selectedAccount} /> diff --git a/components/brave_wallet_ui/stories/locale.ts b/components/brave_wallet_ui/stories/locale.ts index 3b7e23982378..6b8af85d2465 100644 --- a/components/brave_wallet_ui/stories/locale.ts +++ b/components/brave_wallet_ui/stories/locale.ts @@ -329,6 +329,13 @@ provideStrings({ braveWalletConfirmTransactionFrist: 'first', braveWalletConfirmTransactions: 'transactions', + // Backed up Transactions Warning + braveWalletBackedUpTransactionsTitle: 'Transaction backlog', + braveWalletBackedUpTransactionsDescription: 'Youre attempting to start a new transaction while you have previously submitted transactions that have not been confirmed. This will block any new ones from being submitted.', + braveWalletBackedUpTransactionsCheckbox: 'Clear & replace the incomplete transaction(s)', + braveWalletBackedUpTransactionsClearButton: 'Clear and continue', + braveWalletBackedUpTransactionsClearingButton: 'Clearing transactions', + // Wallet Main Panel braveWalletPanelTitle: 'Brave Wallet', braveWalletPanelConnected: 'Connected', diff --git a/components/brave_wallet_ui/stories/wallet-extension-panels.tsx b/components/brave_wallet_ui/stories/wallet-extension-panels.tsx index 1ad012eb9e1c..e10423ca3648 100644 --- a/components/brave_wallet_ui/stories/wallet-extension-panels.tsx +++ b/components/brave_wallet_ui/stories/wallet-extension-panels.tsx @@ -97,6 +97,22 @@ const mockDefaultCurrencies = { crypto: 'BTC' } +const transactionDummyAccounts: WalletAccountType[] = [ + { + id: '1', + name: 'Account 1', + address: '1', + balance: '0.31178', + accountType: 'Primary', + tokenBalanceRegistry: {}, + coin: BraveWallet.CoinType.ETH + } +] + +const transactionList = { + [transactionDummyAccounts[0].address]: [...transactionDummyData[1]].concat(...transactionDummyData[2]) +} + export const _ConfirmTransaction = () => { const transactionInfo: BraveWallet.TransactionInfo = { fromAddress: '0x7d66c9ddAED3115d93Bd1790332f3Cd06Cf52B14', @@ -203,6 +219,8 @@ export const _ConfirmTransaction = () => { getERC20Allowance={getERC20Allowance} fullTokenList={NewAssetOptions} gasEstimates={undefined} + selectedAccount={accounts[0]} + transactions={transactionList} /> ) @@ -391,20 +409,6 @@ _ConnectWithSite.story = { } export const _ConnectedPanel = (args: { locked: boolean }) => { - const transactionDummyAccounts: WalletAccountType[] = [ - { - id: '1', - name: 'Account 1', - address: '1', - balance: '0.31178', - accountType: 'Primary', - tokenBalanceRegistry: {}, - coin: BraveWallet.CoinType.ETH - } - ] - const transactionList = { - [transactionDummyAccounts[0].address]: [...transactionDummyData[1]].concat(...transactionDummyData[2]) - } const { locked } = args const [inputValue, setInputValue] = React.useState('') const [walletLocked, setWalletLocked] = React.useState(locked) diff --git a/components/brave_wallet_ui/utils/datetime-utils.ts b/components/brave_wallet_ui/utils/datetime-utils.ts index 7ef03669eeb8..034ff7dc0604 100644 --- a/components/brave_wallet_ui/utils/datetime-utils.ts +++ b/components/brave_wallet_ui/utils/datetime-utils.ts @@ -16,9 +16,13 @@ export function mojoTimeDeltaToJSDate (mojoTime: TimeDelta) { const monthMap = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] +export function calculatedTimeDiffInMilliseconds (date: Date, now: Date = new Date()) { + return now.getTime() - date.getTime() +} + export function formatDateAsRelative (date: Date, now: Date = new Date()) { // the difference in milliseconds - const diff = now.getTime() - date.getTime() + const diff = calculatedTimeDiffInMilliseconds(date, now) // convert diff to seconds const sec = Math.floor(diff / 1000) diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index b013a12a94eb..4558d6b1b1ae 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -279,6 +279,11 @@ next first transactions + Transaction backlog + You're attempting to start a new transaction while you have previously submitted transactions that have not been confirmed. This will block any new ones from being submitted. + Clear & replace the incomplete transaction(s) + Clear and continue + Clearing transactions Brave Wallet Connected Site permissions