Skip to content

Commit

Permalink
feat(wallet): Recently Interacted Address History
Browse files Browse the repository at this point in the history
  • Loading branch information
Douglashdaniel committed Jan 3, 2025
1 parent 457e9b3 commit 63c730a
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 11 deletions.
1 change: 1 addition & 0 deletions components/brave_wallet/browser/brave_wallet_constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ inline constexpr webui::LocalizedString kLocalizedStrings[] = {
IDS_BRAVE_WALLET_USER_UNDERSTANDS_LABEL},
{"braveWalletChooseRecipient", IDS_BRAVE_WALLET_CHOOSE_RECIPIENT},
{"braveWalletMyAddresses", IDS_BRAVE_WALLET_MY_ADDRESSES},
{"braveWalletRecentAddresses", IDS_BRAVE_WALLET_RECENT_ADDRESSES},
{"braveWalletAddressOrDomainPlaceholder",
IDS_BRAVE_WALLET_ADDRESS_OR_DOMAIN_PLACEHOLDER},
{"braveWalletSendTo", IDS_BRAVE_WALLET_SEND_TO},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export const LOCAL_STORAGE_KEYS = {
NFT_COLLECTION_NAMES_REGISTRY: 'NFT_COLLECTION_NAMES_REGISTRY2',
FILTERED_OUT_DAPP_NETWORK_KEYS: 'BRAVE_WALLET_FILTERED_OUT_DAPP_NETWORK_KEYS',
FILTERED_OUT_DAPP_CATEGORIES: 'BRAVE_WALLET_FILTERED_OUT_DAPP_CATEGORIES',
HAS_ACCEPTED_PARTNER_TERMS: 'BRAVE_WALLET_HAS_ACCEPTED_PARTNER_TERMS'
HAS_ACCEPTED_PARTNER_TERMS: 'BRAVE_WALLET_HAS_ACCEPTED_PARTNER_TERMS',
RECENTLY_INTERACTED_WITH_ADDRESS_HISTORY:
'BRAVE_WALLET_RECENTLY_INTERACTED_WITH_ADDRESS_HISTORY'
} as const

const LOCAL_STORAGE_KEYS_DEPRECATED = {
Expand Down
2 changes: 2 additions & 0 deletions components/brave_wallet_ui/constants/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1121,3 +1121,5 @@ export type zcashAddressOptionType = {
addressType: zcashAddressTypes
label: string
}

export type AddressHistory = Record<string, string[]>
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ export const WalletIcon = styled(Icon).attrs({
margin-right: 8px;
`

export const TrashIcon = styled(Icon).attrs({
name: 'trash'
})`
--leo-icon-size: 20px;
color: ${leo.color.icon.default};
`

export const DomainLoadIcon = styled(LoaderIcon)<{ position: number }>`
color: ${leo.color.icon.default};
height: 20px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ import * as React from 'react'
import { skipToken } from '@reduxjs/toolkit/query/react'
import Icon from '@brave/leo/react/icon'
import ProgressRing from '@brave/leo/react/progressRing'
import Button from '@brave/leo/react/button'

// Constants
import {
LOCAL_STORAGE_KEYS //
} from '../../../../../common/constants/local-storage-keys'

// Types
import {
BraveWallet,
AddressMessageInfo,
AddressMessageInfoIds,
CoinTypesMap
CoinTypesMap,
AddressHistory
} from '../../../../../constants/types'

// Queries
Expand All @@ -32,12 +39,20 @@ import {
useValidateUnifiedAddressQuery
} from '../../../../../common/slices/api.slice'

// Hooks
import {
useLocalStorage //
} from '../../../../../common/hooks/use_local_storage'

// Utils
import {
isValidBtcAddress,
isValidEVMAddress,
isValidFilAddress
} from '../../../../../utils/address-utils'
import {
getAddressHistoryIdentifier //
} from '../../../../../utils/local-storage-utils'

// Messages
import {
Expand Down Expand Up @@ -91,7 +106,8 @@ import {
WalletIcon,
AddressButtonText,
DomainLoadIcon,
SearchBoxContainer
SearchBoxContainer,
TrashIcon
} from './select_address_modal.style'

interface Props {
Expand All @@ -116,6 +132,12 @@ export const SelectAddressModal = React.forwardRef<HTMLDivElement, Props>(
setResolvedDomainAddress
} = props

// Local Storage
const [addressHistory, setAddressHistory] = useLocalStorage<AddressHistory>(
LOCAL_STORAGE_KEYS.RECENTLY_INTERACTED_WITH_ADDRESS_HISTORY,
{}
)

// Mutations
const [enableEnsOffchainLookup] = useEnableEnsOffchainLookupMutation()
const [generateReceiveAddress, { isLoading: isGeneratingAddress }] =
Expand Down Expand Up @@ -212,6 +234,22 @@ export const SelectAddressModal = React.forwardRef<HTMLDivElement, Props>(
)
}, [accountsByNetwork, searchValue])

const addressHistoryIdentifier =
getAddressHistoryIdentifier(selectedNetwork)

const addressHistoryByNetworkOrCoin = React.useMemo(() => {
if (!addressHistoryIdentifier) {
return []
}
return addressHistory[addressHistoryIdentifier] ?? []
}, [addressHistoryIdentifier, addressHistory])

const filteredAddressHistory = React.useMemo(() => {
return addressHistoryByNetworkOrCoin.filter((address) =>
address.toLocaleLowerCase().startsWith(searchValue.toLocaleLowerCase())
)
}, [addressHistoryByNetworkOrCoin, searchValue])

const evmAddressesforFVMTranslation = React.useMemo(
() =>
accountsByNetwork
Expand Down Expand Up @@ -323,7 +361,8 @@ export const SelectAddressModal = React.forwardRef<HTMLDivElement, Props>(
filteredAccounts.length !== 0) ||
(!isSearchingForDomain &&
addressMessageInformation &&
filteredAccounts.length === 0)
filteredAccounts.length === 0 &&
filteredAddressHistory.length === 0)

// Methods
const onSelectAccount = React.useCallback(
Expand Down Expand Up @@ -369,6 +408,26 @@ export const SelectAddressModal = React.forwardRef<HTMLDivElement, Props>(
dismissOffchainEnsWarning(true)
}, [enableEnsOffchainLookup])

const onClickDeleteAddress = React.useCallback(
(address: string) => {
if (addressHistoryIdentifier) {
const newAddressHistoryForNetwork =
addressHistoryByNetworkOrCoin.filter(
(addr: string) => addr !== address
)
let history = addressHistory
history[addressHistoryIdentifier] = newAddressHistoryForNetwork
setAddressHistory(history)
}
},
[
addressHistory,
addressHistoryByNetworkOrCoin,
addressHistoryIdentifier,
setAddressHistory
]
)

if (showChecksumInfo) {
return (
<PopupModal
Expand Down Expand Up @@ -439,13 +498,61 @@ export const SelectAddressModal = React.forwardRef<HTMLDivElement, Props>(
fullWidth={true}
justifyContent='flex-start'
>
{filteredAddressHistory.length !== 0 && (
<>
<Row
width='100%'
justifyContent='flex-start'
padding='0px 8px'
margin='4px 0px'
>
<LabelText
textSize='12px'
isBold={true}
>
{getLocale('braveWalletRecentAddresses')}
</LabelText>
</Row>
{filteredAddressHistory.map((address) => (
<Row
key={address}
gap='12px'
>
<AddressButton onClick={() => onSelectAddress(address)}>
<WalletIcon />
<Column alignItems='flext-start'>
<AddressButtonText
textSize='14px'
isBold={true}
textColor='primary'
textAlign='left'
>
{address}
</AddressButtonText>
</Column>
</AddressButton>
<Button
kind='plain-faint'
size='small'
onClick={() => onClickDeleteAddress(address)}
>
<TrashIcon />
</Button>
</Row>
))}
</>
)}
{filteredAccounts.length !== 0 && (
<>
<Row
width='100%'
justifyContent='flex-start'
padding='0px 8px'
marginBottom={4}
margin={
filteredAddressHistory.length !== 0
? '8px 0px 4px 0px'
: '0px 0px 4px 0px'
}
>
<LabelText
textSize='12px'
Expand Down Expand Up @@ -482,6 +589,7 @@ export const SelectAddressModal = React.forwardRef<HTMLDivElement, Props>(
</Row>
)}
{filteredAccounts.length === 0 &&
filteredAddressHistory.length === 0 &&
!isSearchingForDomain &&
!showEnsOffchainWarning &&
addressMessageInformation?.type !== 'error' && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ import {
CoinTypesMap,
BraveWallet,
BaseTransactionParams,
AmountValidationErrorType
AmountValidationErrorType,
AddressHistory
} from '../../../../constants/types'

// Constants
import { MAX_ZCASH_MEMO_LENGTH } from '../constants/magics'
import {
LOCAL_STORAGE_KEYS //
} from '../../../../common/constants/local-storage-keys'

// Utils
import { getLocale } from '../../../../../common/locale'
Expand All @@ -40,6 +44,9 @@ import {
import {
getDominantColorFromImageURL //
} from '../../../../utils/style.utils'
import {
getAddressHistoryIdentifier //
} from '../../../../utils/local-storage-utils'

// Hooks
import {
Expand All @@ -64,6 +71,9 @@ import {
import {
useAccountFromAddressQuery //
} from '../../../../common/slices/api.slice.extra'
import {
useLocalStorage //
} from '../../../../common/hooks/use_local_storage'

// Styled Components
import { InputRow, ToText, ToRow } from './send.style'
Expand Down Expand Up @@ -102,6 +112,12 @@ interface Props {
export const SendScreen = React.memo((props: Props) => {
const { isAndroid = false } = props

// Local Storage
const [addressHistory, setAddressHistory] = useLocalStorage<AddressHistory>(
LOCAL_STORAGE_KEYS.RECENTLY_INTERACTED_WITH_ADDRESS_HISTORY,
{}
)

// routing
const query = useQuery()
const history = useHistory()
Expand Down Expand Up @@ -258,6 +274,24 @@ export const SendScreen = React.memo((props: Props) => {
return getDominantColorFromImageURL(tokenFromParams?.logo ?? '')
}, [tokenFromParams?.logo])

const addressHistoryIdentifier =
getAddressHistoryIdentifier(networkFromParams)

const toAddress =
resolvedDomainAddress !== '' ? resolvedDomainAddress : toAddressOrUrl

// Gets the list of recently used addresses for the selected network or coin.
const addressHistoryByNetworkOrCoin = React.useMemo(() => {
if (!addressHistoryIdentifier) {
return []
}
return addressHistory[addressHistoryIdentifier] ?? []
}, [addressHistoryIdentifier, addressHistory])

const { account: toAccount } = useAccountFromAddressQuery(
toAddress ?? undefined
)

// Methods
const selectSendAsset = React.useCallback(
(asset: BraveWallet.BlockchainToken, account?: BraveWallet.AccountInfo) => {
Expand Down Expand Up @@ -311,8 +345,25 @@ export const SendScreen = React.memo((props: Props) => {
hardware: accountFromParams.hardware
}

const toAddress =
resolvedDomainAddress !== '' ? resolvedDomainAddress : toAddressOrUrl
// Checks to see if the toAddress is not already in history and
// that the toAddress is not one of the users own accounts.
if (
!addressHistoryByNetworkOrCoin.includes(toAddress) &&
addressHistoryIdentifier &&
!toAccount
) {
// Ensures that we only keep history of up to 5 addresses
// for a network at a time.
const addressesToKeep =
addressHistoryByNetworkOrCoin.length > 4
? addressHistoryByNetworkOrCoin.slice(0, 4)
: addressHistoryByNetworkOrCoin

// Updates address history.
let history = addressHistory
history[addressHistoryIdentifier] = [toAddress, ...addressesToKeep]
setAddressHistory(history)
}

switch (fromAccount.accountId.coin) {
case BraveWallet.CoinType.BTC: {
Expand Down Expand Up @@ -461,12 +512,15 @@ export const SendScreen = React.memo((props: Props) => {
tokenFromParams,
accountFromParams,
networkFromParams,
toAddressOrUrl,
sendBtcTransaction,
sendingMaxAmount,
sendAmount,
resolvedDomainAddress,
memoText,
addressHistory,
addressHistoryByNetworkOrCoin,
addressHistoryIdentifier,
toAddress,
toAccount,
resetSendFields,
sendEvmTransaction,
sendERC20Transfer,
Expand All @@ -475,7 +529,8 @@ export const SendScreen = React.memo((props: Props) => {
sendFilTransaction,
sendSolTransaction,
sendSPLTransfer,
sendZecTransaction
sendZecTransaction,
setAddressHistory
])

const handleFromAssetValueChange = React.useCallback(
Expand Down
1 change: 1 addition & 0 deletions components/brave_wallet_ui/stories/locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ provideStrings({
braveWalletUserUnderstandsLabel: 'I understand',
braveWalletChooseRecipient: 'Choose recipient',
braveWalletMyAddresses: 'My addresses',
braveWalletRecentAddresses: 'Recent addresses',
braveWalletAddressOrDomainPlaceholder: 'Enter public address or domain.',
braveWalletSendTo: 'Send to',
braveWalletInvalidDomainExtension: 'Invalid domain extension.',
Expand Down
14 changes: 14 additions & 0 deletions components/brave_wallet_ui/utils/local-storage-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,17 @@ export const setPersistedNftCollectionNamesRegistry = (
console.error(error)
}
}

export const getAddressHistoryIdentifier = (
network?: BraveWallet.NetworkInfo
) => {
if (!network) {
return undefined
}
// Since ETH and SOL use the address between networks
// we use the CoinType as the identifier.
return network.coin === BraveWallet.CoinType.ETH ||
network.coin === BraveWallet.CoinType.SOL
? network.coin.toString()
: network.chainId
}
Loading

0 comments on commit 63c730a

Please sign in to comment.