From e0aa2ab56f1730d7528845b84f9f34f96098fef2 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Fri, 3 Oct 2025 18:01:31 +0200
Subject: [PATCH 01/36] feat: new mobile send flow
---
src/assets/translations/en/main.json | 9 +-
src/components/Modals/Send/Form.tsx | 9 +-
src/components/Modals/Send/SendCommon.tsx | 1 +
.../Send/SendMaxButton/SendMaxButton.tsx | 9 +-
src/components/Modals/Send/views/Address.tsx | 92 +++++--
src/components/Modals/Send/views/Amount.tsx | 252 ++++++++++++++++++
6 files changed, 344 insertions(+), 28 deletions(-)
create mode 100644 src/components/Modals/Send/views/Amount.tsx
diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json
index f3aeecc3acd..3b1f179c799 100644
--- a/src/assets/translations/en/main.json
+++ b/src/assets/translations/en/main.json
@@ -77,6 +77,7 @@
"rename": "Rename",
"forget": "Forget?",
"pending": "Pending",
+ "preview": "Preview",
"incomplete": "Incomplete",
"available": "Available",
"or": "or",
@@ -1472,7 +1473,13 @@
"slow": "Slow",
"average": "Average",
"fast": "Fast",
- "enterAmount": "Enter Amount"
+ "enterAmount": "Enter Amount",
+ "scanQrCode": "Scan QR Code",
+ "scanQrCodeDescription": "Tap to scan an address",
+ "yourAddresses": "Your Addresses",
+ "to": "To",
+ "from": "From",
+ "availableBalance": "%{balance} available"
},
"status": {
"pendingBody": "Sending %{amount} %{symbol}"
diff --git a/src/components/Modals/Send/Form.tsx b/src/components/Modals/Send/Form.tsx
index f5c70bc3c74..60c40a5e4a8 100644
--- a/src/components/Modals/Send/Form.tsx
+++ b/src/components/Modals/Send/Form.tsx
@@ -12,6 +12,7 @@ import { useFormSend } from './hooks/useFormSend/useFormSend'
import { SendFormFields, SendRoutes } from './SendCommon'
import { maybeFetchChangeAddress } from './utils'
import { Address } from './views/Address'
+import { Amount } from './views/Amount'
import { Confirm } from './views/Confirm'
import { Details } from './views/Details'
import { Status } from './views/Status'
@@ -31,9 +32,9 @@ import { getMixPanel } from '@/lib/mixpanel/mixPanelSingleton'
import { MixPanelEvent } from '@/lib/mixpanel/types'
import { actionSlice } from '@/state/slices/actionSlice/actionSlice'
import {
- ActionStatus,
- ActionType,
- GenericTransactionDisplayType,
+ ActionStatus,
+ ActionType,
+ GenericTransactionDisplayType,
} from '@/state/slices/actionSlice/types'
import { preferences } from '@/state/slices/preferencesSlice/preferencesSlice'
import { selectMarketDataByAssetIdUserCurrency } from '@/state/slices/selectors'
@@ -42,6 +43,7 @@ import { store, useAppDispatch, useAppSelector } from '@/state/store'
const status =
const confirm =
const details =
+const amount =
const address =
export type SendInput = {
@@ -307,6 +309,7 @@ export const Form: React.FC = ({ initialAssetId, input = '', acco
{selectAssetRouter}
{address}
+ {amount}
{details}
{qrCodeScanner}
{confirm}
diff --git a/src/components/Modals/Send/SendCommon.tsx b/src/components/Modals/Send/SendCommon.tsx
index 7ad9649e9a9..fb70fc08bb0 100644
--- a/src/components/Modals/Send/SendCommon.tsx
+++ b/src/components/Modals/Send/SendCommon.tsx
@@ -1,4 +1,5 @@
export enum SendRoutes {
+ Amount = '/send/amount',
Details = '/send/details',
Confirm = '/send/confirm',
Status = '/send/status',
diff --git a/src/components/Modals/Send/SendMaxButton/SendMaxButton.tsx b/src/components/Modals/Send/SendMaxButton/SendMaxButton.tsx
index 2be37e6473f..57165445e46 100644
--- a/src/components/Modals/Send/SendMaxButton/SendMaxButton.tsx
+++ b/src/components/Modals/Send/SendMaxButton/SendMaxButton.tsx
@@ -8,14 +8,7 @@ type SendMaxButtonProps = {
export const SendMaxButton = ({ onClick }: SendMaxButtonProps) => {
return (
-
+
+
+
{
[asset?.symbol, translate, handleMemoChange],
)
+ const handleSelectAddressBookEntry = useCallback(
+ (entryAddress: string) => {
+ setValue(SendFormFields.Input, entryAddress, { shouldValidate: true })
+ },
+ [setValue],
+ )
+
if (!asset) return null
return (
@@ -379,6 +386,9 @@ export const SendAmount = () => {
supportsENS={supportsENS}
translate={translate}
onScanQRCode={handleScanQrCode}
+ onSelectEntry={handleSelectAddressBookEntry}
+ chainId={asset.chainId}
+ resolvedAddress={to}
/>
diff --git a/src/context/ModalProvider/ModalContainer.tsx b/src/context/ModalProvider/ModalContainer.tsx
index a6ff548572e..1c1934325d8 100644
--- a/src/context/ModalProvider/ModalContainer.tsx
+++ b/src/context/ModalProvider/ModalContainer.tsx
@@ -221,6 +221,14 @@ const WalletDrawer = makeSuspenseful(
),
)
+const AddAddressModal = makeSuspenseful(
+ lazy(() =>
+ import('@/components/Modals/Send/AddAddressModal').then(({ AddAddressModal }) => ({
+ default: AddAddressModal,
+ })),
+ ),
+)
+
export const MODALS: Modals = {
receive: ReceiveModal,
qrCode: QrCodeModal,
@@ -248,6 +256,7 @@ export const MODALS: Modals = {
assetActionsDrawer: AssetActionsDrawer,
rating: RatingModal,
walletDrawer: WalletDrawer,
+ addAddress: AddAddressModal,
} as const
export const modalReducer = (state: ModalState, action: ModalActions): ModalState => {
diff --git a/src/context/ModalProvider/types.ts b/src/context/ModalProvider/types.ts
index 90c794e096b..e1d28209cf3 100644
--- a/src/context/ModalProvider/types.ts
+++ b/src/context/ModalProvider/types.ts
@@ -13,6 +13,7 @@ import type { PopupWindowModalProps } from '@/components/Modals/PopupWindowModal
import type { QrCodeModalProps } from '@/components/Modals/QrCode/QrCode'
import type { RateChangedModalProps } from '@/components/Modals/RateChanged/RateChanged'
import type { ReceivePropsType } from '@/components/Modals/Receive/Receive'
+import type { AddAddressModalProps } from '@/components/Modals/Send/AddAddressModal'
import type { SendModalProps } from '@/components/Modals/Send/Send'
import type { SnapsModalProps } from '@/components/Modals/Snaps/Snaps'
import type { TradeAssetSearchModalProps } from '@/components/Modals/TradeAssetSearch/TradeAssetSearchModal'
@@ -43,6 +44,7 @@ export type Modals = {
assetActionsDrawer: FC
rating: FC
walletDrawer: FC
+ addAddress: FC
}
export type ModalActions = OpenModalType | CloseModalType
diff --git a/src/state/reducer.ts b/src/state/reducer.ts
index e13a491269d..d1c7fe6840c 100644
--- a/src/state/reducer.ts
+++ b/src/state/reducer.ts
@@ -23,6 +23,8 @@ import {
} from './migrations'
import { actionSlice } from './slices/actionSlice/actionSlice'
import type { ActionState } from './slices/actionSlice/types'
+import type { AddressBookState } from './slices/addressBookSlice/addressBookSlice'
+import { addressBookSlice } from './slices/addressBookSlice/addressBookSlice'
import type { AssetsState } from './slices/assetsSlice/assetsSlice'
import { assetApi, assets } from './slices/assetsSlice/assetsSlice'
import { limitOrderInput } from './slices/limitOrderInputSlice/limitOrderInputSlice'
@@ -61,6 +63,7 @@ export const slices = {
limitOrder: limitOrderSlice,
snapshot,
localWallet: localWalletSlice,
+ addressBook: addressBookSlice,
}
const preferencesPersistConfig = {
@@ -137,6 +140,12 @@ const swapPersistConfig = {
version: Math.max(...Object.keys(clearSwapsMigrations).map(Number)),
}
+const addressBookPersistConfig = {
+ key: 'addressBook',
+ storage: localforage,
+ version: 0,
+}
+
export const sliceReducers = {
assets: persistReducer(assetsPersistConfig, assets.reducer),
marketData: persistReducer(marketDataPersistConfig, marketData.reducer),
@@ -159,6 +168,7 @@ export const sliceReducers = {
),
action: persistReducer(actionPersistConfig, actionSlice.reducer),
swap: persistReducer(swapPersistConfig, swapSlice.reducer),
+ addressBook: persistReducer(addressBookPersistConfig, addressBookSlice.reducer),
}
export const apiSlices = {
diff --git a/src/state/slices/addressBookSlice/addressBookSlice.ts b/src/state/slices/addressBookSlice/addressBookSlice.ts
new file mode 100644
index 00000000000..6106051b410
--- /dev/null
+++ b/src/state/slices/addressBookSlice/addressBookSlice.ts
@@ -0,0 +1,57 @@
+import type { PayloadAction } from '@reduxjs/toolkit'
+import { createSlice } from '@reduxjs/toolkit'
+import type { ChainId } from '@shapeshiftoss/caip'
+
+export type AddressBookEntry = {
+ id: string
+ name: string
+ address: string
+ chainId: ChainId
+ createdAt: number
+}
+
+export type AddressBookState = {
+ byId: Record
+ ids: string[]
+}
+
+const initialState: AddressBookState = {
+ byId: {},
+ ids: [],
+}
+
+export const addressBookSlice = createSlice({
+ name: 'addressBook',
+ initialState,
+ reducers: create => ({
+ addAddress: create.reducer(
+ (state, action: PayloadAction>) => {
+ const id = `${action.payload.chainId}_${action.payload.address}_${Date.now()}`
+ const entry: AddressBookEntry = {
+ ...action.payload,
+ id,
+ createdAt: Date.now(),
+ }
+ state.byId[id] = entry
+ state.ids.push(id)
+ },
+ ),
+ updateAddress: create.reducer((state, action: PayloadAction<{ id: string; name: string }>) => {
+ const entry = state.byId[action.payload.id]
+ if (entry) {
+ entry.name = action.payload.name
+ }
+ }),
+ deleteAddress: create.reducer((state, action: PayloadAction) => {
+ delete state.byId[action.payload]
+ state.ids = state.ids.filter(id => id !== action.payload)
+ }),
+ clear: create.reducer(() => initialState),
+ }),
+ selectors: {
+ selectAddressBookEntries: state => state.ids.map(id => state.byId[id]),
+ selectAddressBookEntryById: (state, entryId: string) => state.byId[entryId],
+ selectAddressBookEntriesByChainId: (state, chainId: ChainId) =>
+ state.ids.map(id => state.byId[id]).filter(entry => entry.chainId === chainId),
+ },
+})
diff --git a/src/state/slices/addressBookSlice/selectors.ts b/src/state/slices/addressBookSlice/selectors.ts
new file mode 100644
index 00000000000..a4b4f94e3b1
--- /dev/null
+++ b/src/state/slices/addressBookSlice/selectors.ts
@@ -0,0 +1,18 @@
+import { createSelector } from '@reduxjs/toolkit'
+import type { ChainId } from '@shapeshiftoss/caip'
+import createCachedSelector from 're-reselect'
+
+import { addressBookSlice } from './addressBookSlice'
+
+import type { ReduxState } from '@/state/reducer'
+
+export const selectAddressBookEntries = createSelector(
+ addressBookSlice.selectors.selectAddressBookEntries,
+ entries => entries,
+)
+
+export const selectAddressBookEntriesByChainId = createCachedSelector(
+ addressBookSlice.selectors.selectAddressBookEntries,
+ (_state: ReduxState, chainId: ChainId) => chainId,
+ (entries, chainId) => entries.filter(entry => entry.chainId === chainId),
+)((_state: ReduxState, chainId: ChainId): ChainId => chainId ?? 'undefined')
diff --git a/src/state/store.ts b/src/state/store.ts
index d7a87cae774..36cf8125b9d 100644
--- a/src/state/store.ts
+++ b/src/state/store.ts
@@ -54,6 +54,7 @@ export const clearState = () => {
store.dispatch(slices.localWallet.actions.clear())
store.dispatch(slices.limitOrderInput.actions.clear())
store.dispatch(slices.limitOrder.actions.clear())
+ store.dispatch(slices.addressBook.actions.clear())
store.dispatch(apiSlices.assetApi.util.resetApiState())
store.dispatch(apiSlices.marketApi.util.resetApiState())
diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts
index 5fca5b185e0..373ee369cef 100644
--- a/src/test/mocks/store.ts
+++ b/src/test/mocks/store.ts
@@ -358,4 +358,12 @@ export const mockStore: ReduxState = {
nativeWalletName: null,
rdns: null,
},
+ addressBook: {
+ _persist: {
+ version: 0,
+ rehydrated: false,
+ },
+ byId: {},
+ ids: [],
+ },
}
From 54a7b3e95d3d0a566e812fe97f29364e96e10e81 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Mon, 13 Oct 2025 23:06:15 +0200
Subject: [PATCH 08/36] fix: qr code and default asset route
---
src/components/Modals/Send/Form.tsx | 8 ++++++--
src/components/Modals/Send/Send.tsx | 15 +++++++++++----
src/components/Modals/Send/SendCommon.tsx | 18 ++++++++++++++++++
src/components/Modals/Send/views/Address.tsx | 4 ++--
.../Modals/Send/views/SendAmount.tsx | 8 ++------
5 files changed, 39 insertions(+), 14 deletions(-)
diff --git a/src/components/Modals/Send/Form.tsx b/src/components/Modals/Send/Form.tsx
index d83b0047c47..5e79ab61036 100644
--- a/src/components/Modals/Send/Form.tsx
+++ b/src/components/Modals/Send/Form.tsx
@@ -276,13 +276,17 @@ export const Form: React.FC = ({ initialAssetId, input = '', acco
.toString(),
)
}
+ if (isUnderMd) {
+ navigate(SendRoutes.Address)
+ return
+ }
- navigate(SendRoutes.Address)
+ navigate(SendRoutes.Amount)
} catch (e: any) {
setAddressError(e.message)
}
},
- [navigate, methods],
+ [navigate, methods, isUnderMd],
)
const qrCodeScanner = useMemo(
diff --git a/src/components/Modals/Send/Send.tsx b/src/components/Modals/Send/Send.tsx
index 98efeb13322..a9fcb87864d 100644
--- a/src/components/Modals/Send/Send.tsx
+++ b/src/components/Modals/Send/Send.tsx
@@ -1,13 +1,14 @@
+import { useMediaQuery } from '@chakra-ui/react'
import type { AccountId, AssetId } from '@shapeshiftoss/caip'
+import { useMemo } from 'react'
import { MemoryRouter } from 'react-router-dom'
import { Form } from './Form'
-import { SendRoutes } from './SendCommon'
+import { desktopSendRoutes, mobileSendRoutes } from './SendCommon'
import { Dialog } from '@/components/Modal/components/Dialog'
import { useModal } from '@/hooks/useModal/useModal'
-
-export const entries = Object.values(SendRoutes)
+import { breakpoints } from '@/theme/theme'
export type SendModalProps = {
assetId?: AssetId
@@ -18,9 +19,15 @@ export type SendModalProps = {
export const SendModal = ({ assetId, accountId, input }: SendModalProps) => {
const { close, isOpen } = useModal('send')
+ const [isUnderMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false })
+
+ const initialEntries = useMemo(() => {
+ return isUnderMd ? mobileSendRoutes : desktopSendRoutes
+ }, [isUnderMd])
+
return (
diff --git a/src/components/Modals/Send/SendCommon.tsx b/src/components/Modals/Send/SendCommon.tsx
index 85b84da4029..96b9ebbe61b 100644
--- a/src/components/Modals/Send/SendCommon.tsx
+++ b/src/components/Modals/Send/SendCommon.tsx
@@ -7,6 +7,24 @@ export enum SendRoutes {
Address = '/send/address',
}
+export const mobileSendRoutes = [
+ SendRoutes.Confirm,
+ SendRoutes.Status,
+ SendRoutes.Scan,
+ SendRoutes.Select,
+ SendRoutes.Amount,
+ SendRoutes.Address,
+]
+
+export const desktopSendRoutes = [
+ SendRoutes.Confirm,
+ SendRoutes.Status,
+ SendRoutes.Scan,
+ SendRoutes.Select,
+ SendRoutes.Address,
+ SendRoutes.Amount,
+]
+
export enum SendFormFields {
Input = 'input', // the raw field input on the address input
Memo = 'memo', // an optional memo, used only on chains supporting it e.g Cosmos SDK chains
diff --git a/src/components/Modals/Send/views/Address.tsx b/src/components/Modals/Send/views/Address.tsx
index aca12023a52..3b2de161e2d 100644
--- a/src/components/Modals/Send/views/Address.tsx
+++ b/src/components/Modals/Send/views/Address.tsx
@@ -139,8 +139,8 @@ export const Address = () => {
}, [send, qrCode])
const handleScanQrCode = useCallback(() => {
- qrCode.open({ assetId })
- }, [qrCode, assetId])
+ navigate(SendRoutes.Scan)
+ }, [navigate])
if (!asset) return null
diff --git a/src/components/Modals/Send/views/SendAmount.tsx b/src/components/Modals/Send/views/SendAmount.tsx
index 71e6d415278..99890427493 100644
--- a/src/components/Modals/Send/views/SendAmount.tsx
+++ b/src/components/Modals/Send/views/SendAmount.tsx
@@ -47,7 +47,6 @@ import { SelectAssetRoutes } from '@/components/SelectAssets/SelectAssetCommon'
import { SlideTransition } from '@/components/SlideTransition'
import { Text } from '@/components/Text/Text'
import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
-import { useModal } from '@/hooks/useModal/useModal'
import { parseAddressInputWithChainId } from '@/lib/address/address'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { allowedDecimalSeparators } from '@/state/slices/preferencesSlice/preferencesSlice'
@@ -98,8 +97,6 @@ export const SendAmount = () => {
} = useLocaleFormatter()
const [isValidating, setIsValidating] = useState(false)
- const qrCode = useModal('qrCode')
- const sendModal = useModal('send')
const [isUnderMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false })
const { accountId, assetId, to, amountCryptoPrecision, fiatAmount, memo, input } = useWatch({
@@ -142,9 +139,8 @@ export const SendAmount = () => {
)
const handleScanQrCode = useCallback(() => {
- qrCode.open({ assetId })
- sendModal.close()
- }, [qrCode, assetId, sendModal])
+ navigate(SendRoutes.Scan)
+ }, [navigate])
const currentValue = fieldName === SendFormFields.FiatAmount ? fiatAmount : amountCryptoPrecision
const isFiat = fieldName === SendFormFields.FiatAmount
From 957ff34fc338e9d145571312280462ea2a5bb5e5 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Wed, 15 Oct 2025 09:41:34 +0200
Subject: [PATCH 09/36] fix: continue
---
src/assets/translations/en/main.json | 5 +
.../Modals/Send/AddressBook/AddressBook.tsx | 118 ++++++++++++++----
.../Modals/Send/AddressBook/ConfirmDelete.tsx | 51 ++++++++
.../Modals/Send/AddressInput/AddressInput.tsx | 52 ++++----
.../components/AddressInputWithDropdown.tsx | 43 ++-----
src/components/Modals/Send/views/Address.tsx | 19 +--
.../Modals/Send/views/SendAmount.tsx | 25 +++-
.../slices/addressBookSlice/selectors.ts | 22 +++-
8 files changed, 238 insertions(+), 97 deletions(-)
create mode 100644 src/components/Modals/Send/AddressBook/ConfirmDelete.tsx
diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json
index 0a09b6d76de..c5c70847cd7 100644
--- a/src/assets/translations/en/main.json
+++ b/src/assets/translations/en/main.json
@@ -28,6 +28,7 @@
"volumeShort": "VOL",
"save": "Save",
"saveChanges": "Save Changes",
+ "delete": "Delete",
"new": "New",
"continue": "Continue",
"reward": "Reward",
@@ -1450,6 +1451,10 @@
"namePlaceholder": "Name",
"title": "Add Contact"
},
+ "confirmDelete": {
+ "title": "Delete Address",
+ "message": "Are you sure you want to delete %{name} from your address book?"
+ },
"permissionsButton": "Try again",
"transactionSent": "Transaction sent",
"sent": "%{asset} sent",
diff --git a/src/components/Modals/Send/AddressBook/AddressBook.tsx b/src/components/Modals/Send/AddressBook/AddressBook.tsx
index 2fd6627f29b..77708d562e6 100644
--- a/src/components/Modals/Send/AddressBook/AddressBook.tsx
+++ b/src/components/Modals/Send/AddressBook/AddressBook.tsx
@@ -1,21 +1,24 @@
import {
- Avatar,
- Box,
- Text as CText,
- HStack,
- Icon,
- useColorModeValue,
- VStack,
+ Avatar,
+ Box,
+ Button,
+ HStack,
+ Icon,
+ Text as CText,
+ useDisclosure,
+ VStack,
} from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
-import { useCallback, useMemo } from 'react'
-import { FaRegAddressBook } from 'react-icons/fa'
+import { useCallback, useMemo, useState } from 'react'
+import { FaRegAddressBook, FaTrash } from 'react-icons/fa'
import { useTranslate } from 'react-polyglot'
+import { ConfirmDelete } from '@/components/Modals/Send/AddressBook/ConfirmDelete'
import { Text } from '@/components/Text'
import { makeBlockiesUrl } from '@/lib/blockies/makeBlockiesUrl'
-import { selectAddressBookEntriesByChainId } from '@/state/slices/addressBookSlice/selectors'
-import { useAppSelector } from '@/state/store'
+import { addressBookSlice } from '@/state/slices/addressBookSlice/addressBookSlice'
+import { selectAddressBookEntriesByChainNamespace } from '@/state/slices/addressBookSlice/selectors'
+import { useAppDispatch, useAppSelector } from '@/state/store'
export type AddressBookEntry = {
id: string
@@ -26,31 +29,56 @@ export type AddressBookEntry = {
type AddressBookEntryButtonProps = {
entry: AddressBookEntry
onSelect: (address: string) => void
+ onDelete: (id: string) => void
}
-const AddressBookEntryButton = ({ entry, onSelect }: AddressBookEntryButtonProps) => {
+const addressSx = {
+ _hover: {
+ background: 'background.surface.raised.base',
+ },
+}
+
+const deleteButtonSx = {
+ svg: {
+ width: '12px',
+ height: '12px',
+ },
+}
+
+const AddressBookEntryButton = ({ entry, onSelect, onDelete }: AddressBookEntryButtonProps) => {
const avatarUrl = useMemo(() => makeBlockiesUrl(entry.address), [entry.address])
const handleClick = useCallback(() => onSelect(entry.address), [onSelect, entry.address])
const shortAddress = useMemo(
() => `${entry.address.slice(0, 6)}...${entry.address.slice(-4)}`,
[entry.address],
)
- const borderColor = useColorModeValue('gray.200', 'gray.700')
+
+ const handleDelete = useCallback(
+ (id: string) => (e: React.MouseEvent) => {
+ e.stopPropagation()
+ e.preventDefault()
+ onDelete(id)
+ },
+ [onDelete],
+ )
return (
-
-
-
+
+
+
-
+
{entry.name}
@@ -58,6 +86,9 @@ const AddressBookEntryButton = ({ entry, onSelect }: AddressBookEntryButtonProps
+
+
+
)
}
@@ -74,9 +105,29 @@ export const AddressBook = ({
emptyMessage = 'modals.send.noEntries',
}: AddressBookProps) => {
const translate = useTranslate()
+ const dispatch = useAppDispatch()
+ const { isOpen, onClose, onOpen } = useDisclosure({
+ defaultIsOpen: false,
+ })
+ const [selectedDeleteEntry, setSelectedDeleteEntry] = useState(null)
const addressBookEntries = useAppSelector(state =>
- chainId ? selectAddressBookEntriesByChainId(state, chainId) : [],
+ chainId ? selectAddressBookEntriesByChainNamespace(state, chainId) : [],
+ )
+
+ const handleDelete = useCallback(
+ (id: string) => () => {
+ dispatch(addressBookSlice.actions.deleteAddress(id))
+ },
+ [dispatch],
+ )
+
+ const handleDeleteConfirm = useCallback(
+ (selectedEntry: AddressBookEntry) => () => {
+ setSelectedDeleteEntry(selectedEntry)
+ onOpen()
+ },
+ [onOpen],
)
return (
@@ -93,10 +144,23 @@ export const AddressBook = ({
) : (
addressBookEntries.map(entry => (
-
+
))
)}
+ {selectedDeleteEntry && (
+
+ )}
)
}
diff --git a/src/components/Modals/Send/AddressBook/ConfirmDelete.tsx b/src/components/Modals/Send/AddressBook/ConfirmDelete.tsx
new file mode 100644
index 00000000000..3f6504b2def
--- /dev/null
+++ b/src/components/Modals/Send/AddressBook/ConfirmDelete.tsx
@@ -0,0 +1,51 @@
+import { Button, Stack, Text as CText } from '@chakra-ui/react'
+import { useCallback } from 'react'
+import { useTranslate } from 'react-polyglot'
+
+import { Dialog } from '@/components/Modal/components/Dialog'
+import { DialogBody } from '@/components/Modal/components/DialogBody'
+import { DialogFooter } from '@/components/Modal/components/DialogFooter'
+import { DialogHeader, DialogHeaderMiddle } from '@/components/Modal/components/DialogHeader'
+import { DialogTitle } from '@/components/Modal/components/DialogTitle'
+import { Text } from '@/components/Text'
+
+type ConfirmDeleteProps = {
+ entryName: string
+ onDelete: () => void
+ onClose: () => void
+ isOpen: boolean
+}
+
+export const ConfirmDelete = ({ entryName, onDelete, onClose, isOpen }: ConfirmDeleteProps) => {
+ const translate = useTranslate()
+
+ const handleConfirm = useCallback(() => {
+ onDelete()
+ onClose()
+ }, [onDelete, onClose])
+
+ return (
+
+ )
+}
diff --git a/src/components/Modals/Send/AddressInput/AddressInput.tsx b/src/components/Modals/Send/AddressInput/AddressInput.tsx
index 7d67ff46573..f47b849a77c 100644
--- a/src/components/Modals/Send/AddressInput/AddressInput.tsx
+++ b/src/components/Modals/Send/AddressInput/AddressInput.tsx
@@ -3,7 +3,6 @@ import {
Avatar,
Box,
Button,
- Text as CText,
Flex,
HStack,
IconButton,
@@ -11,12 +10,13 @@ import {
InputGroup,
InputLeftElement,
InputRightElement,
+ Text as CText,
Text,
useMediaQuery,
VStack,
} from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
-import { useCallback, useMemo, useState } from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ControllerProps, ControllerRenderProps, FieldValues } from 'react-hook-form'
import { Controller, useFormContext, useWatch } from 'react-hook-form'
import { useTranslate } from 'react-polyglot'
@@ -29,7 +29,7 @@ import { QRCodeIcon } from '@/components/Icons/QRCode'
import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis'
import type { SendInput } from '@/components/Modals/Send/Form'
import { makeBlockiesUrl } from '@/lib/blockies/makeBlockiesUrl'
-import { selectAddressBookEntriesByChainId } from '@/state/slices/addressBookSlice/selectors'
+import { selectAddressBookEntriesByChainNamespace } from '@/state/slices/addressBookSlice/selectors'
import { useAppSelector } from '@/state/store'
import { breakpoints } from '@/theme/theme'
@@ -40,7 +40,7 @@ type AddressInputProps = {
pe?: SpaceProps['pe']
resolvedAddress?: string
chainId?: ChainId
- onSaveContact?: () => void
+ onSaveContact?: (e: React.MouseEvent) => void
onEmptied?: () => void
} & InputProps
@@ -71,9 +71,10 @@ export const AddressInput = ({
const isValidating = useFormContext().formState.isValidating
const value = useWatch({ name: SendFormFields.Input })
const [isUnderMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false })
+ const inputRef = useRef(null)
const addressBookEntries = useAppSelector(state =>
- chainId ? selectAddressBookEntriesByChainId(state, chainId) : [],
+ chainId ? selectAddressBookEntriesByChainNamespace(state, chainId) : [],
)
const isInvalid = useMemo(() => {
@@ -95,9 +96,25 @@ export const AddressInput = ({
[resolvedAddress],
)
- const handleFocus = useCallback(() => {
- setIsFocused(true)
- }, [])
+ useEffect(() => {
+ if (addressBookEntry) {
+ setIsFocused(false)
+ }
+ }, [addressBookEntry])
+
+ useEffect(() => {
+ if (isFocused) {
+ inputRef.current?.focus()
+ }
+ }, [isFocused])
+
+ const handleFocus = useCallback(
+ (e: React.FocusEvent) => {
+ props.onFocus?.(e)
+ setIsFocused(true)
+ },
+ [props],
+ )
const handleBlur = useCallback(
(e: React.FocusEvent) => {
@@ -138,6 +155,7 @@ export const AddressInput = ({
- {onSaveContact && (
-
- {translate('common.save')}
-
- )}
)
}
@@ -209,19 +222,14 @@ export const AddressInput = ({
w='full'
px={4}
sx={addressInputSx}
+ onClick={handleDisplayClick}
+ cursor='pointer'
>
{translate('modals.send.sendForm.to')}
-
+
diff --git a/src/components/Modals/Send/components/AddressInputWithDropdown.tsx b/src/components/Modals/Send/components/AddressInputWithDropdown.tsx
index 264d9ef21aa..3266d8ea90c 100644
--- a/src/components/Modals/Send/components/AddressInputWithDropdown.tsx
+++ b/src/components/Modals/Send/components/AddressInputWithDropdown.tsx
@@ -1,17 +1,16 @@
import {
Box,
Button,
- Text as ChakraText,
FormControl,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
- useDisclosure,
+ Text as ChakraText,
VStack,
} from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
-import { useCallback, useRef } from 'react'
+import { useCallback } from 'react'
import { AddressInput } from '../AddressInput/AddressInput'
import { QRCodeIcon } from './QRCodeIcon'
@@ -26,8 +25,7 @@ interface AddressInputWithDropdownProps {
chainId?: ChainId
resolvedAddress?: string
onSelectEntry: (address: string) => void
- showSaveButton?: boolean
- onSaveContact?: () => void
+ onSaveContact: (e: React.MouseEvent) => void
onEmptied?: () => void
}
@@ -40,59 +38,34 @@ export const AddressInputWithDropdown = ({
onScanQRCode,
resolvedAddress,
onSelectEntry,
- showSaveButton,
onSaveContact,
onEmptied,
chainId,
}: AddressInputWithDropdownProps) => {
- const { isOpen, onOpen, onClose } = useDisclosure()
- const triggerRef = useRef(null)
-
- const handleFocus = useCallback(() => {
- if (!isOpen) {
- onOpen()
- }
- }, [onOpen, isOpen])
-
- const handleBlur = useCallback(() => {
- onClose()
- }, [onClose])
-
const handleQRClick = useCallback(() => {
onScanQRCode()
- onClose()
- }, [onScanQRCode, onClose])
+ }, [onScanQRCode])
const handleSelectEntry = useCallback(
(address: string) => {
onSelectEntry(address)
- onClose()
},
- [onSelectEntry, onClose],
+ [onSelectEntry],
)
return (
-
+
-
+
diff --git a/src/components/Modals/Send/views/Address.tsx b/src/components/Modals/Send/views/Address.tsx
index 1415494a992..1af261dd2c4 100644
--- a/src/components/Modals/Send/views/Address.tsx
+++ b/src/components/Modals/Send/views/Address.tsx
@@ -31,7 +31,7 @@ import { SlideTransition } from '@/components/SlideTransition'
import { Text } from '@/components/Text'
import { useModal } from '@/hooks/useModal/useModal'
import { parseAddressInputWithChainId } from '@/lib/address/address'
-import { selectAddressBookEntriesByChainId } from '@/state/slices/addressBookSlice/selectors'
+import { selectAddressBookEntriesByChainNamespace } from '@/state/slices/addressBookSlice/selectors'
import { selectAssetById } from '@/state/slices/selectors'
import { useAppSelector } from '@/state/store'
@@ -79,7 +79,7 @@ export const Address = () => {
const asset = useAppSelector(state => selectAssetById(state, assetId))
const addressBookEntries = useAppSelector(state =>
- asset?.chainId ? selectAddressBookEntriesByChainId(state, asset.chainId) : [],
+ selectAddressBookEntriesByChainNamespace(state, asset?.chainId ?? ''),
)
const supportsENS = asset?.chainId === ethChainId // We only support ENS resolution on ETH mainnet
@@ -166,11 +166,16 @@ export const Address = () => {
[setValue],
)
- const handleSaveContact = useCallback(() => {
- if (address && asset?.chainId) {
- addAddress.open({ address, chainId: asset.chainId })
- }
- }, [address, asset?.chainId, addAddress])
+ const handleSaveContact = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+
+ if (address && asset?.chainId) {
+ addAddress.open({ address, chainId: asset.chainId })
+ }
+ },
+ [address, asset?.chainId, addAddress],
+ )
const handleEmptyChange = useCallback(() => {
setValue(SendFormFields.Input, '', { shouldValidate: true })
diff --git a/src/components/Modals/Send/views/SendAmount.tsx b/src/components/Modals/Send/views/SendAmount.tsx
index 6e161396dcd..653a43e4af5 100644
--- a/src/components/Modals/Send/views/SendAmount.tsx
+++ b/src/components/Modals/Send/views/SendAmount.tsx
@@ -1,8 +1,6 @@
import {
Box,
Button,
- Text as ChakraText,
- Text as CText,
Flex,
FormControl,
FormHelperText,
@@ -12,6 +10,8 @@ import {
Input,
Skeleton,
Stack,
+ Text as ChakraText,
+ Text as CText,
Tooltip,
useMediaQuery,
VStack,
@@ -47,6 +47,7 @@ import { SelectAssetRoutes } from '@/components/SelectAssets/SelectAssetCommon'
import { SlideTransition } from '@/components/SlideTransition'
import { Text } from '@/components/Text/Text'
import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
+import { useModal } from '@/hooks/useModal/useModal'
import { parseAddressInputWithChainId } from '@/lib/address/address'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { allowedDecimalSeparators } from '@/state/slices/preferencesSlice/preferencesSlice'
@@ -98,6 +99,7 @@ export const SendAmount = () => {
const [isValidating, setIsValidating] = useState(false)
const [isUnderMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false })
+ const addAddress = useModal('addAddress')
const { accountId, assetId, to, amountCryptoPrecision, fiatAmount, memo, input } = useWatch({
control,
@@ -264,6 +266,23 @@ export const SendAmount = () => {
[setValue],
)
+ const handleEmptyChange = useCallback(() => {
+ setValue(SendFormFields.Input, '', { shouldValidate: true })
+ setValue(SendFormFields.To, '')
+ setValue(SendFormFields.VanityAddress, '')
+ }, [setValue])
+
+ const handleSaveContact = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+
+ if (to && asset?.chainId) {
+ addAddress.open({ address: to, chainId: asset.chainId })
+ }
+ },
+ [to, asset?.chainId, addAddress],
+ )
+
if (!asset) return null
return (
@@ -383,6 +402,8 @@ export const SendAmount = () => {
translate={translate}
onScanQRCode={handleScanQrCode}
onSelectEntry={handleSelectAddressBookEntry}
+ onEmptied={handleEmptyChange}
+ onSaveContact={handleSaveContact}
chainId={asset.chainId}
resolvedAddress={to}
/>
diff --git a/src/state/slices/addressBookSlice/selectors.ts b/src/state/slices/addressBookSlice/selectors.ts
index a4b4f94e3b1..5ceed733430 100644
--- a/src/state/slices/addressBookSlice/selectors.ts
+++ b/src/state/slices/addressBookSlice/selectors.ts
@@ -1,5 +1,6 @@
import { createSelector } from '@reduxjs/toolkit'
-import type { ChainId } from '@shapeshiftoss/caip'
+import type { ChainId, ChainNamespace } from '@shapeshiftoss/caip'
+import { fromChainId } from '@shapeshiftoss/caip'
import createCachedSelector from 're-reselect'
import { addressBookSlice } from './addressBookSlice'
@@ -11,8 +12,21 @@ export const selectAddressBookEntries = createSelector(
entries => entries,
)
-export const selectAddressBookEntriesByChainId = createCachedSelector(
+export const selectAddressBookEntriesByChainNamespace = createCachedSelector(
addressBookSlice.selectors.selectAddressBookEntries,
(_state: ReduxState, chainId: ChainId) => chainId,
- (entries, chainId) => entries.filter(entry => entry.chainId === chainId),
-)((_state: ReduxState, chainId: ChainId): ChainId => chainId ?? 'undefined')
+ (entries, chainId) => {
+ const { chainNamespace } = fromChainId(chainId)
+
+ // Filter entries that match the same chain namespace
+ // This allows addresses to work across all chains of the same type
+ // (e.g., an Ethereum address works on all EVM chains)
+ return entries.filter(entry => {
+ const entryNamespace = fromChainId(entry.chainId).chainNamespace
+ return entryNamespace === chainNamespace
+ })
+ },
+)((_state: ReduxState, chainId: ChainId): ChainNamespace => {
+ const { chainNamespace } = fromChainId(chainId)
+ return chainNamespace
+})
From e514817997a48dd338a11fccdc657b3f16b45cba Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Mon, 20 Oct 2025 15:37:19 +0200
Subject: [PATCH 10/36] fix: review feedbacks
---
.../Modals/Send/AddressBook/AddressBook.tsx | 22 ++++---
.../components/AddressInputWithDropdown.tsx | 59 ++++++++++---------
2 files changed, 47 insertions(+), 34 deletions(-)
diff --git a/src/components/Modals/Send/AddressBook/AddressBook.tsx b/src/components/Modals/Send/AddressBook/AddressBook.tsx
index 77708d562e6..29726fa7dc2 100644
--- a/src/components/Modals/Send/AddressBook/AddressBook.tsx
+++ b/src/components/Modals/Send/AddressBook/AddressBook.tsx
@@ -2,9 +2,9 @@ import {
Avatar,
Box,
Button,
+ Text as CText,
HStack,
Icon,
- Text as CText,
useDisclosure,
VStack,
} from '@chakra-ui/react'
@@ -63,21 +63,29 @@ const AddressBookEntryButton = ({ entry, onSelect, onDelete }: AddressBookEntryB
)
return (
-
+
-
-
+
+
{entry.name}
@@ -86,7 +94,7 @@ const AddressBookEntryButton = ({ entry, onSelect, onDelete }: AddressBookEntryB
-
+
@@ -112,7 +120,7 @@ export const AddressBook = ({
const [selectedDeleteEntry, setSelectedDeleteEntry] = useState(null)
const addressBookEntries = useAppSelector(state =>
- chainId ? selectAddressBookEntriesByChainNamespace(state, chainId) : [],
+ selectAddressBookEntriesByChainNamespace(state, chainId ?? ''),
)
const handleDelete = useCallback(
diff --git a/src/components/Modals/Send/components/AddressInputWithDropdown.tsx b/src/components/Modals/Send/components/AddressInputWithDropdown.tsx
index 3266d8ea90c..0a3920faee6 100644
--- a/src/components/Modals/Send/components/AddressInputWithDropdown.tsx
+++ b/src/components/Modals/Send/components/AddressInputWithDropdown.tsx
@@ -1,12 +1,13 @@
import {
Box,
Button,
+ Text as ChakraText,
FormControl,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
- Text as ChakraText,
+ Portal,
VStack,
} from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
@@ -16,6 +17,7 @@ import { AddressInput } from '../AddressInput/AddressInput'
import { QRCodeIcon } from './QRCodeIcon'
import { AddressBook } from '@/components/Modals/Send/AddressBook/AddressBook'
+import { useModalChildZIndex } from '@/context/ModalStackProvider'
interface AddressInputWithDropdownProps {
addressInputRules: any
@@ -42,6 +44,7 @@ export const AddressInputWithDropdown = ({
onEmptied,
chainId,
}: AddressInputWithDropdownProps) => {
+ const modalChildZIndex = useModalChildZIndex()
const handleQRClick = useCallback(() => {
onScanQRCode()
}, [onScanQRCode])
@@ -70,33 +73,35 @@ export const AddressInputWithDropdown = ({
/>
-
-
-
-
-
-
- {translate('modals.send.scanQrCode')}
-
-
- {translate('modals.send.sendForm.scanQrCodeDescription')}
-
-
-
+
+
+
+
+
+
+
+ {translate('modals.send.scanQrCode')}
+
+
+ {translate('modals.send.sendForm.scanQrCodeDescription')}
+
+
+
-
-
-
-
+
+
+
+
+
)
From 92c20c1b0d5e417c187f6a1c0b73bd9195955c80 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Mon, 20 Oct 2025 21:02:38 +0200
Subject: [PATCH 11/36] fix: review feedbacks
---
src/components/Modals/QrCode/QrCode.tsx | 12 +-
.../Modals/Send/AddressBook/AddressBook.tsx | 2 +-
.../Modals/Send/AddressInput/AddressInput.tsx | 26 +-
src/components/Modals/Send/Form.tsx | 22 +-
src/components/Modals/Send/Send.tsx | 12 +-
src/components/Modals/Send/SendCommon.tsx | 11 +-
.../components/AddressInputWithDropdown.tsx | 112 ----
src/components/Modals/Send/views/Address.tsx | 1 +
src/components/Modals/Send/views/Confirm.tsx | 30 +-
.../Modals/Send/views/SendAmount.tsx | 538 ------------------
.../Modals/Send/views/SendAmountDetails.tsx | 90 ++-
11 files changed, 93 insertions(+), 763 deletions(-)
delete mode 100644 src/components/Modals/Send/components/AddressInputWithDropdown.tsx
delete mode 100644 src/components/Modals/Send/views/SendAmount.tsx
diff --git a/src/components/Modals/QrCode/QrCode.tsx b/src/components/Modals/QrCode/QrCode.tsx
index f92df597dd3..f6caaf3293b 100644
--- a/src/components/Modals/QrCode/QrCode.tsx
+++ b/src/components/Modals/QrCode/QrCode.tsx
@@ -1,14 +1,11 @@
-import { useMediaQuery } from '@chakra-ui/react'
import type { AccountId, AssetId } from '@shapeshiftoss/caip'
-import { useMemo } from 'react'
import { MemoryRouter } from 'react-router-dom'
-import { desktopSendRoutes, mobileSendRoutes } from '../Send/SendCommon'
+import { sendRoutes } from '../Send/SendCommon'
import { Form } from './Form'
import { Dialog } from '@/components/Modal/components/Dialog'
import { useModal } from '@/hooks/useModal/useModal'
-import { breakpoints } from '@/theme/theme'
export type QrCodeModalProps = {
assetId?: AssetId
@@ -17,15 +14,10 @@ export type QrCodeModalProps = {
export const QrCodeModal = ({ assetId, accountId }: QrCodeModalProps) => {
const { close, isOpen } = useModal('qrCode')
- const [isSmallerThanMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false })
-
- const initialEntries = useMemo(() => {
- return isSmallerThanMd ? mobileSendRoutes : desktopSendRoutes
- }, [isSmallerThanMd])
return (
diff --git a/src/components/Modals/Send/AddressBook/AddressBook.tsx b/src/components/Modals/Send/AddressBook/AddressBook.tsx
index 517859b441a..9c1a8709f6a 100644
--- a/src/components/Modals/Send/AddressBook/AddressBook.tsx
+++ b/src/components/Modals/Send/AddressBook/AddressBook.tsx
@@ -147,7 +147,7 @@ export const AddressBook = ({
-
+
{addressBookEntries.length === 0 ? (
) : (
diff --git a/src/components/Modals/Send/AddressInput/AddressInput.tsx b/src/components/Modals/Send/AddressInput/AddressInput.tsx
index 219dd9607cc..64c329febe5 100644
--- a/src/components/Modals/Send/AddressInput/AddressInput.tsx
+++ b/src/components/Modals/Send/AddressInput/AddressInput.tsx
@@ -37,9 +37,9 @@ type AddressInputProps = {
placeholder?: string
pe?: SpaceProps['pe']
resolvedAddress?: string
+ isReadOnly?: boolean
chainId?: ChainId
onSaveContact?: (e: React.MouseEvent) => void
- onEmptied?: () => void
} & Omit
const addressInputSx = {
@@ -54,9 +54,9 @@ export const AddressInput = ({
rules,
placeholder,
enableQr = false,
+ isReadOnly = false,
resolvedAddress,
chainId,
- onEmptied,
onSaveContact,
onFocus,
onBlur,
@@ -137,15 +137,16 @@ export const AddressInput = ({
}: {
field: ControllerRenderProps
}) => {
- if (isFocused || !resolvedAddress || isInvalid) {
+ if ((isFocused || !resolvedAddress || isInvalid) && !isReadOnly) {
return (
- {translate('modals.send.sendForm.to')}
+ {translate('trade.to')}
const sendAmount =
@@ -88,7 +86,6 @@ export const Form: React.FC = ({ initialAssetId, input = '', acco
const {
state: { wallet },
} = useWallet()
- const [isSmallerThanMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false })
const [addressError, setAddressError] = useState(null)
@@ -210,16 +207,10 @@ export const Form: React.FC = ({ initialAssetId, input = '', acco
// Use requestAnimationFrame to ensure navigation happens after state updates
requestAnimationFrame(() => {
- if (isSmallerThanMd) {
- navigate(SendRoutes.Address, { replace: true })
- return
- }
- // On desktop, go directly to AmountDetails
- // On mobile, go to Address first
- navigate(SendRoutes.AmountDetails, { replace: true })
+ navigate(SendRoutes.Address, { replace: true })
})
},
- [navigate, methods, selectedCurrency, isSmallerThanMd],
+ [navigate, methods, selectedCurrency],
)
const handleBack = useCallback(() => {
@@ -276,17 +267,12 @@ export const Form: React.FC = ({ initialAssetId, input = '', acco
.toString(),
)
}
- if (isSmallerThanMd) {
- navigate(SendRoutes.Address)
- return
- }
-
- navigate(SendRoutes.AmountDetails)
+ navigate(SendRoutes.Address)
} catch (e: any) {
setAddressError(e.message)
}
},
- [navigate, methods, isSmallerThanMd],
+ [navigate, methods],
)
const qrCodeScanner = useMemo(
diff --git a/src/components/Modals/Send/Send.tsx b/src/components/Modals/Send/Send.tsx
index eb41260a489..c11cf246bdf 100644
--- a/src/components/Modals/Send/Send.tsx
+++ b/src/components/Modals/Send/Send.tsx
@@ -1,14 +1,11 @@
-import { useMediaQuery } from '@chakra-ui/react'
import type { AccountId, AssetId } from '@shapeshiftoss/caip'
-import { useMemo } from 'react'
import { MemoryRouter } from 'react-router-dom'
import { Form } from './Form'
-import { desktopSendRoutes, mobileSendRoutes } from './SendCommon'
import { Dialog } from '@/components/Modal/components/Dialog'
+import { sendRoutes } from '@/components/Modals/Send/SendCommon'
import { useModal } from '@/hooks/useModal/useModal'
-import { breakpoints } from '@/theme/theme'
export type SendModalProps = {
assetId?: AssetId
@@ -18,15 +15,10 @@ export type SendModalProps = {
export const SendModal = ({ assetId, accountId, input }: SendModalProps) => {
const { close, isOpen } = useModal('send')
- const [isSmallerThanMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false })
-
- const initialEntries = useMemo(() => {
- return isSmallerThanMd ? mobileSendRoutes : desktopSendRoutes
- }, [isSmallerThanMd])
return (
diff --git a/src/components/Modals/Send/SendCommon.tsx b/src/components/Modals/Send/SendCommon.tsx
index 23f1fd79255..ceef86ca5d9 100644
--- a/src/components/Modals/Send/SendCommon.tsx
+++ b/src/components/Modals/Send/SendCommon.tsx
@@ -8,7 +8,7 @@ export enum SendRoutes {
Amount = '/send/amount',
}
-export const mobileSendRoutes = [
+export const sendRoutes = [
SendRoutes.Confirm,
SendRoutes.Status,
SendRoutes.Scan,
@@ -17,15 +17,6 @@ export const mobileSendRoutes = [
SendRoutes.Address,
]
-export const desktopSendRoutes = [
- SendRoutes.Confirm,
- SendRoutes.Status,
- SendRoutes.Scan,
- SendRoutes.Select,
- SendRoutes.Address,
- SendRoutes.AmountDetails,
-]
-
export enum SendFormFields {
Input = 'input', // the raw field input on the address input
Memo = 'memo', // an optional memo, used only on chains supporting it e.g Cosmos SDK chains
diff --git a/src/components/Modals/Send/components/AddressInputWithDropdown.tsx b/src/components/Modals/Send/components/AddressInputWithDropdown.tsx
deleted file mode 100644
index b2a894f51d3..00000000000
--- a/src/components/Modals/Send/components/AddressInputWithDropdown.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import {
- Box,
- Button,
- FormControl,
- Popover,
- PopoverBody,
- PopoverContent,
- PopoverTrigger,
- Portal,
- Text as ChakraText,
- VStack,
-} from '@chakra-ui/react'
-import { fromAssetId } from '@shapeshiftoss/caip/dist/cjs'
-import { useCallback, useMemo } from 'react'
-import type { ControllerProps } from 'react-hook-form'
-import { useFormContext, useWatch } from 'react-hook-form'
-
-import { AddressInput } from '../AddressInput/AddressInput'
-
-import { QRCodeIcon } from '@/components/Icons/QRCode'
-import { AddressBook } from '@/components/Modals/Send/AddressBook/AddressBook'
-import type { SendInput } from '@/components/Modals/Send/Form'
-import { useModalChildZIndex } from '@/context/ModalStackProvider'
-
-interface AddressInputWithDropdownProps {
- addressInputRules: ControllerProps['rules']
- supportsENS: boolean
- translate: (key: string) => string
- onScanQRCode: () => void
- onSelectEntry: (address: string) => void
- onSaveContact: (e: React.MouseEvent) => void
- onEmptied?: () => void
-}
-
-const qrCodeIcon =
-
-export const AddressInputWithDropdown = ({
- addressInputRules,
- supportsENS,
- translate,
- onScanQRCode,
- onSelectEntry,
- onSaveContact,
- onEmptied,
-}: AddressInputWithDropdownProps) => {
- const { control } = useFormContext()
- const { assetId } = useWatch({
- control,
- }) as Partial
-
- const chainId = useMemo(() => fromAssetId(assetId ?? '').chainId, [assetId])
-
- const modalChildZIndex = useModalChildZIndex()
- const handleQRClick = useCallback(() => {
- onScanQRCode()
- }, [onScanQRCode])
-
- const handleSelectEntry = useCallback(
- (address: string) => {
- onSelectEntry(address)
- },
- [onSelectEntry],
- )
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {translate('modals.send.scanQrCode')}
-
-
- {translate('modals.send.sendForm.scanQrCodeDescription')}
-
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/src/components/Modals/Send/views/Address.tsx b/src/components/Modals/Send/views/Address.tsx
index 4b320996276..8065fcdfa60 100644
--- a/src/components/Modals/Send/views/Address.tsx
+++ b/src/components/Modals/Send/views/Address.tsx
@@ -224,6 +224,7 @@ export const Address = () => {
height='auto'
background='transparent'
m={-2}
+ my={0}
p={2}
>
diff --git a/src/components/Modals/Send/views/Confirm.tsx b/src/components/Modals/Send/views/Confirm.tsx
index 6f4c49bccfa..51fa2297dd2 100644
--- a/src/components/Modals/Send/views/Confirm.tsx
+++ b/src/components/Modals/Send/views/Confirm.tsx
@@ -1,5 +1,6 @@
import { ExternalLinkIcon } from '@chakra-ui/icons'
import {
+ Avatar,
Box,
Button,
Divider,
@@ -54,11 +55,12 @@ import { getConfig } from '@/config'
import { defaultLongPressConfig } from '@/constants/longPress'
import { getChainAdapterManager } from '@/context/PluginProvider/chainAdapterSingleton'
import { useWallet } from '@/hooks/useWallet/useWallet'
+import { makeBlockiesUrl } from '@/lib/blockies/makeBlockiesUrl'
import { isMobile } from '@/lib/globals'
import { middleEllipsis } from '@/lib/utils'
import { isUtxoAccountId } from '@/lib/utils/utxo'
import { vibrate } from '@/lib/vibrate'
-import { ProfileAvatar } from '@/pages/Dashboard/components/ProfileAvatar/ProfileAvatar'
+import { selectAddressBookEntriesByChainNamespace } from '@/state/slices/addressBookSlice/selectors'
import { selectAssetById, selectFeeAssetById } from '@/state/slices/selectors'
import { useAppSelector } from '@/state/store'
@@ -111,6 +113,14 @@ export const Confirm = ({ handleSubmit }: ConfirmProps) => {
state: { wallet },
} = useWallet()
+ const addressBookEntries = useAppSelector(state =>
+ selectAddressBookEntriesByChainNamespace(state, asset?.chainId ?? ''),
+ )
+
+ const selectedAddressBookEntry = useMemo(() => {
+ return addressBookEntries.find(entry => entry.address === to)
+ }, [addressBookEntries, to])
+
const showMemoRow = useMemo(
() => Boolean(assetId && fromAssetId(assetId).chainNamespace === CHAIN_NAMESPACE.CosmosSdk),
[assetId],
@@ -127,6 +137,14 @@ export const Confirm = ({ handleSubmit }: ConfirmProps) => {
[assetId, wallet],
)
+ const avatarUrl = useMemo(
+ () =>
+ selectedAddressBookEntry
+ ? makeBlockiesUrl(selectedAddressBookEntry.address)
+ : makeBlockiesUrl(to ?? ''),
+ [selectedAddressBookEntry, to],
+ )
+
const borderColor = useColorModeValue('gray.100', 'gray.750')
const handleNonceChange = useCallback(
@@ -252,11 +270,16 @@ export const Confirm = ({ handleSubmit }: ConfirmProps) => {
-
+
{vanityAddress ? vanityAddress : }
+ {selectedAddressBookEntry && (
+
+ {selectedAddressBookEntry.name}
+
+ )}
{
>
- {/* @TODO: Use custom receive address avatar */}
-
+
diff --git a/src/components/Modals/Send/views/SendAmount.tsx b/src/components/Modals/Send/views/SendAmount.tsx
deleted file mode 100644
index 653a43e4af5..00000000000
--- a/src/components/Modals/Send/views/SendAmount.tsx
+++ /dev/null
@@ -1,538 +0,0 @@
-import {
- Box,
- Button,
- Flex,
- FormControl,
- FormHelperText,
- FormLabel,
- HStack,
- Icon,
- Input,
- Skeleton,
- Stack,
- Text as ChakraText,
- Text as CText,
- Tooltip,
- useMediaQuery,
- VStack,
-} from '@chakra-ui/react'
-import { CHAIN_NAMESPACE, ethChainId, fromAssetId } from '@shapeshiftoss/caip'
-import get from 'lodash/get'
-import { useCallback, useEffect, useMemo, useState } from 'react'
-import type { ControllerRenderProps, FieldValues } from 'react-hook-form'
-import { Controller, useFormContext, useWatch } from 'react-hook-form'
-import { FaInfoCircle } from 'react-icons/fa'
-import { TbSwitchVertical } from 'react-icons/tb'
-import type { NumberFormatValues } from 'react-number-format'
-import NumberFormat from 'react-number-format'
-import { useTranslate } from 'react-polyglot'
-import { useNavigate } from 'react-router-dom'
-
-import { AddressInputWithDropdown } from '../components/AddressInputWithDropdown'
-import type { SendInput } from '../Form'
-import { useSendDetails } from '../hooks/useSendDetails/useSendDetails'
-import { SendFormFields, SendRoutes } from '../SendCommon'
-
-import { AccountSelector } from '@/components/AccountSelector/AccountSelector'
-import { Amount } from '@/components/Amount/Amount'
-import { Display } from '@/components/Display'
-import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis'
-import { DialogBackButton } from '@/components/Modal/components/DialogBackButton'
-import { DialogBody } from '@/components/Modal/components/DialogBody'
-import { DialogFooter } from '@/components/Modal/components/DialogFooter'
-import { DialogHeader } from '@/components/Modal/components/DialogHeader'
-import { DialogTitle } from '@/components/Modal/components/DialogTitle'
-import { SendMaxButton } from '@/components/Modals/Send/SendMaxButton/SendMaxButton'
-import { SelectAssetRoutes } from '@/components/SelectAssets/SelectAssetCommon'
-import { SlideTransition } from '@/components/SlideTransition'
-import { Text } from '@/components/Text/Text'
-import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
-import { useModal } from '@/hooks/useModal/useModal'
-import { parseAddressInputWithChainId } from '@/lib/address/address'
-import { bnOrZero } from '@/lib/bignumber/bignumber'
-import { allowedDecimalSeparators } from '@/state/slices/preferencesSlice/preferencesSlice'
-import { selectAssetById } from '@/state/slices/selectors'
-import { useAppSelector } from '@/state/store'
-import { breakpoints } from '@/theme/theme'
-
-const accountDropdownBoxProps = { px: 0, my: 0 }
-const accountDropdownButtonProps = { px: 2 }
-
-const MAX_COSMOS_SDK_MEMO_LENGTH = 256
-
-type RenderController = ({
- field,
-}: {
- field: ControllerRenderProps
-}) => React.ReactElement
-
-const AmountInput = (props: any) => {
- return (
-
- )
-}
-
-export const SendAmount = () => {
- const {
- control,
- setValue,
- trigger,
- formState: { errors },
- } = useFormContext()
- const navigate = useNavigate()
- const translate = useTranslate()
- const {
- number: { localeParts },
- } = useLocaleFormatter()
-
- const [isValidating, setIsValidating] = useState(false)
- const [isUnderMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false })
- const addAddress = useModal('addAddress')
-
- const { accountId, assetId, to, amountCryptoPrecision, fiatAmount, memo, input } = useWatch({
- control,
- }) as Partial
-
- const asset = useAppSelector(state => selectAssetById(state, assetId ?? ''))
-
- const { balancesLoading, fieldName, handleSendMax, handleInputChange, isLoading, toggleIsFiat } =
- useSendDetails()
-
- const supportsENS = asset?.chainId === ethChainId
- const addressError = get(errors, `${SendFormFields.Input}.message`, null)
-
- useEffect(() => {
- trigger(SendFormFields.Input)
- }, [trigger])
-
- const handleNextClick = useCallback(() => navigate(SendRoutes.Confirm), [navigate])
- const handleBackClick = useCallback(() => navigate(SendRoutes.Address), [navigate])
- const handleAssetBackClick = useCallback(() => {
- setValue(SendFormFields.AssetId, '')
- navigate(SendRoutes.Select, {
- state: {
- toRoute: SelectAssetRoutes.Search,
- assetId: '',
- },
- })
- }, [navigate, setValue])
-
- const handleMaxClick = useCallback(async () => {
- await handleSendMax()
- }, [handleSendMax])
-
- const handleAccountChange = useCallback(
- (newAccountId: string) => {
- setValue(SendFormFields.AccountId, newAccountId)
- },
- [setValue],
- )
-
- const handleScanQrCode = useCallback(() => {
- navigate(SendRoutes.Scan)
- }, [navigate])
-
- const currentValue = fieldName === SendFormFields.FiatAmount ? fiatAmount : amountCryptoPrecision
- const isFiat = fieldName === SendFormFields.FiatAmount
-
- const displayPlaceholder = isFiat ? `${localeParts.prefix}0.00` : `0.00 ${asset?.symbol}`
-
- const showMemoField = useMemo(
- () => Boolean(assetId && fromAssetId(assetId).chainNamespace === CHAIN_NAMESPACE.CosmosSdk),
- [assetId],
- )
-
- const remainingMemoChars = useMemo(
- () => bnOrZero(MAX_COSMOS_SDK_MEMO_LENGTH - Number(memo?.length)),
- [memo],
- )
-
- const memoFieldError = remainingMemoChars.lt(0) && 'Characters Limit Exceeded'
-
- const assetMemoTranslation = useMemo(
- () => ['modals.send.sendForm.assetMemo', { assetSymbol: asset?.symbol ?? '' }],
- [asset?.symbol],
- )
-
- const addressInputRules = useMemo(
- () => ({
- required: true,
- validate: {
- validateAddress: async (rawInput: string) => {
- if (!asset) return
- if (rawInput === '') return
-
- const urlOrAddress = rawInput.trim()
- setIsValidating(true)
- setValue(SendFormFields.To, '')
- setValue(SendFormFields.VanityAddress, '')
- const { assetId, chainId } = asset
- const parseAddressInputWithChainIdArgs = {
- assetId,
- chainId,
- urlOrAddress,
- disableUrlParsing: true,
- }
- const { address, vanityAddress } = await parseAddressInputWithChainId(
- parseAddressInputWithChainIdArgs,
- )
- setIsValidating(false)
- setValue(SendFormFields.To, address)
- setValue(SendFormFields.VanityAddress, vanityAddress)
- const invalidMessage = 'common.invalidAddress'
- return address ? true : invalidMessage
- },
- },
- }),
- [asset, setValue],
- )
-
- const handleValueChange = useCallback(
- (onChange: (value: string) => void, value: string) => (values: NumberFormatValues) => {
- onChange(values.value)
- if (values.value !== value) handleInputChange(values.value)
- },
- [handleInputChange],
- )
-
- const renderController = useCallback(
- ({ field: { onChange, value } }: { field: any }) => {
- return (
-
- )
- },
- [
- asset?.precision,
- isFiat,
- localeParts.decimal,
- localeParts.group,
- localeParts.prefix,
- displayPlaceholder,
- handleValueChange,
- ],
- )
-
- const handleMemoChange = useCallback(
- (onChange: (value: string) => void) =>
- ({ target: { value } }: { target: { value: string } }) =>
- onChange(value),
- [],
- )
-
- const renderMemoController: RenderController = useCallback(
- ({ field: { onChange, value } }) => (
-
- ),
- [asset?.symbol, translate, handleMemoChange],
- )
-
- const handleSelectAddressBookEntry = useCallback(
- (entryAddress: string) => {
- setValue(SendFormFields.Input, entryAddress, { shouldValidate: true })
- },
- [setValue],
- )
-
- const handleEmptyChange = useCallback(() => {
- setValue(SendFormFields.Input, '', { shouldValidate: true })
- setValue(SendFormFields.To, '')
- setValue(SendFormFields.VanityAddress, '')
- }, [setValue])
-
- const handleSaveContact = useCallback(
- (e: React.MouseEvent) => {
- e.stopPropagation()
-
- if (to && asset?.chainId) {
- addAddress.open({ address: to, chainId: asset.chainId })
- }
- },
- [to, asset?.chainId, addAddress],
- )
-
- if (!asset) return null
-
- return (
-
-
-
-
-
-
-
-
-
- {translate('modals.send.sendForm.sendAsset', { asset: asset.name })}
-
-
-
-
-
-
-
-
-
- {translate('modals.send.sendForm.to')}
-
-
-
-
-
-
-
-
- {balancesLoading ? (
-
- ) : (
-
- {fieldName === SendFormFields.AmountCryptoPrecision && (
-
- )}
- {fieldName === SendFormFields.FiatAmount && (
-
- )}
-
-
-
- {isFiat ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
- )}
-
-
- {showMemoField && (
-
-
-
-
-
-
-
-
-
-
-
- {translate('modals.send.sendForm.charactersRemaining', {
- charactersRemaining: remainingMemoChars.toString(),
- })}
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
- {balancesLoading ? (
-
- ) : (
-
- {fieldName === SendFormFields.AmountCryptoPrecision && (
-
- )}
- {fieldName === SendFormFields.FiatAmount && (
-
- )}
-
-
-
- {isFiat ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
- )}
-
-
- {showMemoField && (
-
-
-
-
-
-
-
-
-
-
-
- {translate('modals.send.sendForm.charactersRemaining', {
- charactersRemaining: remainingMemoChars.toString(),
- })}
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
- {translate('modals.send.sendForm.from')}
-
-
-
-
-
-
-
-
-
-
- {translate(addressError ?? 'common.preview')}
-
-
-
-
- )
-}
diff --git a/src/components/Modals/Send/views/SendAmountDetails.tsx b/src/components/Modals/Send/views/SendAmountDetails.tsx
index 44a4bebfb18..7f8f9605c90 100644
--- a/src/components/Modals/Send/views/SendAmountDetails.tsx
+++ b/src/components/Modals/Send/views/SendAmountDetails.tsx
@@ -13,9 +13,7 @@ import {
Skeleton,
Stack,
Text as ChakraText,
- Text as CText,
Tooltip,
- useMediaQuery,
VStack,
} from '@chakra-ui/react'
import { CHAIN_NAMESPACE, ethChainId, fromAssetId } from '@shapeshiftoss/caip'
@@ -30,31 +28,29 @@ import NumberFormat from 'react-number-format'
import { useTranslate } from 'react-polyglot'
import { useLocation, useNavigate } from 'react-router-dom'
-import { AddressInputWithDropdown } from '../components/AddressInputWithDropdown'
import type { SendInput } from '../Form'
import { useSendDetails } from '../hooks/useSendDetails/useSendDetails'
import { SendFormFields, SendRoutes } from '../SendCommon'
import { AccountSelector } from '@/components/AccountSelector/AccountSelector'
import { Amount } from '@/components/Amount/Amount'
-import { Display } from '@/components/Display'
-import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis'
import { DialogBackButton } from '@/components/Modal/components/DialogBackButton'
import { DialogBody } from '@/components/Modal/components/DialogBody'
import { DialogFooter } from '@/components/Modal/components/DialogFooter'
import { DialogHeader } from '@/components/Modal/components/DialogHeader'
import { DialogTitle } from '@/components/Modal/components/DialogTitle'
+import { AddressInput } from '@/components/Modals/Send/AddressInput/AddressInput'
import { SendMaxButton } from '@/components/Modals/Send/SendMaxButton/SendMaxButton'
import { SelectAssetRoutes } from '@/components/SelectAssets/SelectAssetCommon'
import { SlideTransition } from '@/components/SlideTransition'
import { Text } from '@/components/Text/Text'
import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
+import { useModal } from '@/hooks/useModal/useModal'
import { parseAddressInputWithChainId } from '@/lib/address/address'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { allowedDecimalSeparators } from '@/state/slices/preferencesSlice/preferencesSlice'
import { selectAssetById } from '@/state/slices/selectors'
import { useAppSelector } from '@/state/store'
-import { breakpoints } from '@/theme/theme'
const accountDropdownBoxProps = { px: 0, my: 0 }
const accountDropdownButtonProps = { px: 2 }
@@ -128,7 +124,6 @@ export const SendAmountDetails = () => {
} = useLocaleFormatter()
const [isValidating, setIsValidating] = useState(false)
- const [isSmallerThanMd] = useMediaQuery(`(max-width: ${breakpoints.md})`, { ssr: false })
const isFromQrCode = useMemo(() => location.state?.isFromQrCode === true, [location.state])
@@ -152,6 +147,7 @@ export const SendAmountDetails = () => {
const supportsENS = asset?.chainId === ethChainId
const addressError = get(errors, `${SendFormFields.Input}.message`, null)
+ const addAddress = useModal('addAddress')
useEffect(() => {
trigger(SendFormFields.Input)
@@ -195,10 +191,6 @@ export const SendAmountDetails = () => {
[setValue],
)
- const handleScanQrCode = useCallback(() => {
- navigate(SendRoutes.Scan)
- }, [navigate])
-
const currentValue = fieldName === SendFormFields.FiatAmount ? fiatAmount : amountCryptoPrecision
const isFiat = fieldName === SendFormFields.FiatAmount
@@ -259,6 +251,17 @@ export const SendAmountDetails = () => {
[handleInputChange],
)
+ const handleSaveContact = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+
+ if (to && asset?.chainId) {
+ addAddress.open({ address: to, chainId: asset.chainId })
+ }
+ },
+ [to, asset?.chainId, addAddress],
+ )
+
const renderController = useCallback(
({ field: { onChange, value } }: { field: any }) => {
return (
@@ -317,12 +320,7 @@ export const SendAmountDetails = () => {
return (
-
-
-
-
-
-
+
{translate('modals.send.sendForm.sendAsset', { asset: asset.name })}
@@ -331,33 +329,17 @@ export const SendAmountDetails = () => {
-
-
-
-
-
-
-
- {translate('trade.to')}
-
-
-
-
-
-
-
+
{balancesLoading ? (
@@ -379,15 +361,15 @@ export const SendAmountDetails = () => {
/>
)}
-
-
- {isFiat ? (
-
- ) : (
-
- )}
-
-
+
+
+
+ {isFiat ? (
+
+ ) : (
+
+ )}
+
@@ -466,7 +448,7 @@ export const SendAmountDetails = () => {
Date: Tue, 21 Oct 2025 08:54:33 +0200
Subject: [PATCH 12/36] fix: review feedbacks
---
.../Modals/Send/AddressInput/AddressInput.tsx | 6 +++---
.../Modals/Send/views/SendAmountDetails.tsx | 16 +---------------
2 files changed, 4 insertions(+), 18 deletions(-)
diff --git a/src/components/Modals/Send/AddressInput/AddressInput.tsx b/src/components/Modals/Send/AddressInput/AddressInput.tsx
index 64c329febe5..f895d698173 100644
--- a/src/components/Modals/Send/AddressInput/AddressInput.tsx
+++ b/src/components/Modals/Send/AddressInput/AddressInput.tsx
@@ -3,6 +3,7 @@ import {
Avatar,
Box,
Button,
+ Text as CText,
Flex,
HStack,
IconButton,
@@ -10,7 +11,6 @@ import {
InputGroup,
InputLeftElement,
InputRightElement,
- Text as CText,
Text,
VStack,
} from '@chakra-ui/react'
@@ -194,7 +194,7 @@ export const AddressInput = ({
{addressBookEntry.name}
-
+
@@ -220,7 +220,7 @@ export const AddressInput = ({
{translate('modals.send.sendForm.to')}
-
+
{onSaveContact && (
diff --git a/src/components/Modals/Send/views/SendAmountDetails.tsx b/src/components/Modals/Send/views/SendAmountDetails.tsx
index 7f8f9605c90..3d58bbcf499 100644
--- a/src/components/Modals/Send/views/SendAmountDetails.tsx
+++ b/src/components/Modals/Send/views/SendAmountDetails.tsx
@@ -3,6 +3,7 @@ import {
AlertIcon,
Box,
Button,
+ Text as ChakraText,
Flex,
FormControl,
FormHelperText,
@@ -12,7 +13,6 @@ import {
Input,
Skeleton,
Stack,
- Text as ChakraText,
Tooltip,
VStack,
} from '@chakra-ui/react'
@@ -41,7 +41,6 @@ import { DialogHeader } from '@/components/Modal/components/DialogHeader'
import { DialogTitle } from '@/components/Modal/components/DialogTitle'
import { AddressInput } from '@/components/Modals/Send/AddressInput/AddressInput'
import { SendMaxButton } from '@/components/Modals/Send/SendMaxButton/SendMaxButton'
-import { SelectAssetRoutes } from '@/components/SelectAssets/SelectAssetCommon'
import { SlideTransition } from '@/components/SlideTransition'
import { Text } from '@/components/Text/Text'
import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
@@ -166,19 +165,6 @@ export const SendAmountDetails = () => {
() => navigate(isFromQrCode ? SendRoutes.Scan : SendRoutes.Address),
[navigate, isFromQrCode],
)
- const handleAssetBackClick = useCallback(() => {
- if (isFromQrCode) {
- navigate(SendRoutes.Scan)
- return
- }
- setValue(SendFormFields.AssetId, '')
- navigate(SendRoutes.Select, {
- state: {
- toRoute: SelectAssetRoutes.Search,
- assetId: '',
- },
- })
- }, [navigate, setValue, isFromQrCode])
const handleMaxClick = useCallback(async () => {
await handleSendMax()
From cb2e498136ab13c14cc194e4c9277b553512f9d6 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:27:26 +0200
Subject: [PATCH 13/36] fix: review feedbacks
---
src/assets/translations/en/main.json | 2 +-
.../Modals/Send/AddressBook/AddressBook.tsx | 73 +++++++++++++++----
.../Modals/Send/AddressInput/AddressInput.tsx | 2 +-
src/components/Modals/Send/views/Address.tsx | 3 +-
.../Modals/Send/views/SendAmountDetails.tsx | 2 +-
.../slices/addressBookSlice/selectors.ts | 30 ++++++++
6 files changed, 92 insertions(+), 20 deletions(-)
diff --git a/src/assets/translations/en/main.json b/src/assets/translations/en/main.json
index ff5d46efc59..ae654cfb3ac 100644
--- a/src/assets/translations/en/main.json
+++ b/src/assets/translations/en/main.json
@@ -1447,7 +1447,7 @@
"scanQrCode": "Scan QR Code",
"qrCode": "QR Code",
"addressBook": "Address Book",
- "noEntries": "No address yet.",
+ "noEntries": "No address found.",
"addNewAddress": "Add New Address",
"addContact": {
"nameRequired": "Name is required",
diff --git a/src/components/Modals/Send/AddressBook/AddressBook.tsx b/src/components/Modals/Send/AddressBook/AddressBook.tsx
index 9c1a8709f6a..677156cf6b0 100644
--- a/src/components/Modals/Send/AddressBook/AddressBook.tsx
+++ b/src/components/Modals/Send/AddressBook/AddressBook.tsx
@@ -10,14 +10,20 @@ import {
} from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
import { useCallback, useMemo, useState } from 'react'
+import { get, useFormContext, useWatch } from 'react-hook-form'
import { FaRegAddressBook, FaTrash } from 'react-icons/fa'
import { useTranslate } from 'react-polyglot'
+import { SendFormFields } from '../SendCommon'
+
import { ConfirmDelete } from '@/components/Modals/Send/AddressBook/ConfirmDelete'
import { Text } from '@/components/Text'
import { makeBlockiesUrl } from '@/lib/blockies/makeBlockiesUrl'
import { addressBookSlice } from '@/state/slices/addressBookSlice/addressBookSlice'
-import { selectAddressBookEntriesByChainNamespace } from '@/state/slices/addressBookSlice/selectors'
+import {
+ selectAddressBookEntriesByChainNamespace,
+ selectAddressBookEntriesBySearchQuery,
+} from '@/state/slices/addressBookSlice/selectors'
import { useAppDispatch, useAppSelector } from '@/state/store'
export type AddressBookEntry = {
@@ -114,15 +120,42 @@ export const AddressBook = ({
}: AddressBookProps) => {
const translate = useTranslate()
const dispatch = useAppDispatch()
+ const {
+ control,
+ formState: { errors },
+ } = useFormContext()
const { isOpen, onClose, onOpen } = useDisclosure({
defaultIsOpen: false,
})
const [selectedDeleteEntry, setSelectedDeleteEntry] = useState(null)
-
const addressBookEntries = useAppSelector(state =>
selectAddressBookEntriesByChainNamespace(state, chainId ?? ''),
)
+ const address = useWatch({
+ control,
+ name: SendFormFields.To,
+ }) as string
+ const addressError = get(errors, `${SendFormFields.Input}.message`, null)
+
+ const input = useWatch({
+ control,
+ name: SendFormFields.Input,
+ }) as string
+
+ const selectedEntry = useMemo(() => {
+ return addressBookEntries.find(entry => entry.address === input)
+ }, [addressBookEntries, input])
+
+ const searchQuery = useMemo(() => input ?? '', [input])
+
+ const addressBookSearchEntries = useAppSelector(state =>
+ selectAddressBookEntriesBySearchQuery(state, {
+ chainId: chainId ?? '',
+ searchQuery,
+ }),
+ )
+
const handleDelete = useCallback(
(id: string) => () => {
dispatch(addressBookSlice.actions.deleteAddress(id))
@@ -138,6 +171,12 @@ export const AddressBook = ({
[onOpen],
)
+ const entries = useMemo(() => {
+ if (selectedEntry || (address && !addressError)) return addressBookEntries
+
+ return addressBookSearchEntries
+ }, [selectedEntry, addressBookEntries, addressBookSearchEntries, address, addressError])
+
return (
@@ -147,20 +186,22 @@ export const AddressBook = ({
-
- {addressBookEntries.length === 0 ? (
-
- ) : (
- addressBookEntries.map(entry => (
-
- ))
- )}
-
+
+
+ {entries.length === 0 ? (
+
+ ) : (
+ entries.map(entry => (
+
+ ))
+ )}
+
+
{selectedDeleteEntry && (
{
const handleSelectAddressBookEntry = useCallback(
(entryAddress: string) => {
setValue(SendFormFields.Input, entryAddress, { shouldValidate: true })
+ navigate(SendRoutes.AmountDetails)
},
- [setValue],
+ [setValue, navigate],
)
const handleSaveContact = useCallback(
diff --git a/src/components/Modals/Send/views/SendAmountDetails.tsx b/src/components/Modals/Send/views/SendAmountDetails.tsx
index 3d58bbcf499..de5e479636c 100644
--- a/src/components/Modals/Send/views/SendAmountDetails.tsx
+++ b/src/components/Modals/Send/views/SendAmountDetails.tsx
@@ -3,7 +3,6 @@ import {
AlertIcon,
Box,
Button,
- Text as ChakraText,
Flex,
FormControl,
FormHelperText,
@@ -13,6 +12,7 @@ import {
Input,
Skeleton,
Stack,
+ Text as ChakraText,
Tooltip,
VStack,
} from '@chakra-ui/react'
diff --git a/src/state/slices/addressBookSlice/selectors.ts b/src/state/slices/addressBookSlice/selectors.ts
index 5ceed733430..d35ee7cd4db 100644
--- a/src/state/slices/addressBookSlice/selectors.ts
+++ b/src/state/slices/addressBookSlice/selectors.ts
@@ -1,6 +1,7 @@
import { createSelector } from '@reduxjs/toolkit'
import type { ChainId, ChainNamespace } from '@shapeshiftoss/caip'
import { fromChainId } from '@shapeshiftoss/caip'
+import { matchSorter } from 'match-sorter'
import createCachedSelector from 're-reselect'
import { addressBookSlice } from './addressBookSlice'
@@ -30,3 +31,32 @@ export const selectAddressBookEntriesByChainNamespace = createCachedSelector(
const { chainNamespace } = fromChainId(chainId)
return chainNamespace
})
+
+type SearchQueryFilter = {
+ chainId: ChainId
+ searchQuery: string
+}
+
+const selectSearchQueryFromFilter = (_state: ReduxState, filter: SearchQueryFilter) =>
+ filter.searchQuery
+
+export const selectAddressBookEntriesBySearchQuery = createCachedSelector(
+ (state: ReduxState, filter: SearchQueryFilter) =>
+ selectAddressBookEntriesByChainNamespace(state, filter.chainId),
+ selectSearchQueryFromFilter,
+ (entries, searchQuery) => {
+ if (!searchQuery) return entries
+
+ const matchedEntries = matchSorter(entries, searchQuery, {
+ keys: [
+ { key: 'name', threshold: matchSorter.rankings.MATCHES },
+ { key: 'address', threshold: matchSorter.rankings.CONTAINS },
+ ],
+ })
+
+ return matchedEntries
+ },
+)(
+ (_state: ReduxState, filter: SearchQueryFilter) =>
+ `${filter.chainId}_${filter.searchQuery ?? 'addressBookEntriesBySearchQuery'}`,
+)
From 5b34a3fca9a2f8aa050c5b3534de6af85ccbbf44 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:37:56 +0200
Subject: [PATCH 14/36] fix: review feedbacks
---
src/components/GasSelection/index.ts | 2 --
.../Modals/Send/AddressBook/AddressBook.tsx | 15 +++++++++++++--
2 files changed, 13 insertions(+), 4 deletions(-)
delete mode 100644 src/components/GasSelection/index.ts
diff --git a/src/components/GasSelection/index.ts b/src/components/GasSelection/index.ts
deleted file mode 100644
index cfbb3f70c6b..00000000000
--- a/src/components/GasSelection/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { GasSelection, SPEED_OPTIONS } from './GasSelection'
-export type { GasSelectionProps } from './GasSelection'
diff --git a/src/components/Modals/Send/AddressBook/AddressBook.tsx b/src/components/Modals/Send/AddressBook/AddressBook.tsx
index 677156cf6b0..fb849064af6 100644
--- a/src/components/Modals/Send/AddressBook/AddressBook.tsx
+++ b/src/components/Modals/Send/AddressBook/AddressBook.tsx
@@ -2,9 +2,9 @@ import {
Avatar,
Box,
Button,
+ Text as CText,
HStack,
Icon,
- Text as CText,
useDisclosure,
VStack,
} from '@chakra-ui/react'
@@ -113,6 +113,11 @@ type AddressBookProps = {
emptyMessage?: string
}
+const addressBookMaxHeight = {
+ base: 'auto',
+ md: '200px',
+}
+
export const AddressBook = ({
chainId,
onSelectEntry,
@@ -186,7 +191,13 @@ export const AddressBook = ({
-
+
{entries.length === 0 ? (
From 71a9ecd2614250f3800d21b6a9f636e173b199d5 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:38:12 +0200
Subject: [PATCH 15/36] fix: review feedbacks
---
src/components/Modals/Send/AddressBook/AddressBook.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/Modals/Send/AddressBook/AddressBook.tsx b/src/components/Modals/Send/AddressBook/AddressBook.tsx
index fb849064af6..f211bbca2de 100644
--- a/src/components/Modals/Send/AddressBook/AddressBook.tsx
+++ b/src/components/Modals/Send/AddressBook/AddressBook.tsx
@@ -2,9 +2,9 @@ import {
Avatar,
Box,
Button,
- Text as CText,
HStack,
Icon,
+ Text as CText,
useDisclosure,
VStack,
} from '@chakra-ui/react'
From 9a979c3aca12d2ac7fc6f74420965f87516ec84d Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:46:28 +0200
Subject: [PATCH 16/36] fix: review feedbacks
---
src/state/migrations/clearAddressBook.ts | 9 +++++++++
src/state/migrations/index.ts | 5 +++++
src/state/reducer.ts | 4 +++-
src/state/slices/addressBookSlice/addressBookSlice.ts | 2 +-
4 files changed, 18 insertions(+), 2 deletions(-)
create mode 100644 src/state/migrations/clearAddressBook.ts
diff --git a/src/state/migrations/clearAddressBook.ts b/src/state/migrations/clearAddressBook.ts
new file mode 100644
index 00000000000..4a6fcf178d1
--- /dev/null
+++ b/src/state/migrations/clearAddressBook.ts
@@ -0,0 +1,9 @@
+import type { PersistPartial } from 'redux-persist/es/persistReducer'
+
+import type { AddressBookState } from '@/state/slices/addressBookSlice/addressBookSlice'
+import { initialState } from '@/state/slices/addressBookSlice/addressBookSlice'
+
+export const clearAddressBook = (_state: AddressBookState): AddressBookState & PersistPartial => {
+ // Migration to clear addressBook state
+ return initialState as AddressBookState & PersistPartial
+}
diff --git a/src/state/migrations/index.ts b/src/state/migrations/index.ts
index 23acc9159fa..5b8686bc2b7 100644
--- a/src/state/migrations/index.ts
+++ b/src/state/migrations/index.ts
@@ -1,6 +1,7 @@
import type { MigrationManifest } from 'redux-persist'
import { clearAction } from './clearAction'
+import { clearAddressBook } from './clearAddressBook'
import { clearAssets } from './clearAssets'
import { clearLocalWallet } from './clearLocalWallet'
import { clearMarketData } from './clearMarketData'
@@ -259,3 +260,7 @@ export const clearActionMigrations = {
export const clearSwapsMigrations = {
0: clearSwaps,
} as unknown as Omit
+
+export const clearAddressBookMigrations = {
+ 0: clearAddressBook,
+} as unknown as Omit
diff --git a/src/state/reducer.ts b/src/state/reducer.ts
index d1c7fe6840c..4f8937ede3a 100644
--- a/src/state/reducer.ts
+++ b/src/state/reducer.ts
@@ -12,6 +12,7 @@ import { snapshot, snapshotApi } from './apis/snapshot/snapshot'
import { swapperApi } from './apis/swapper/swapperApi'
import {
clearActionMigrations,
+ clearAddressBookMigrations,
clearAssetsMigrations,
clearMarketDataMigrations,
clearOpportunitiesMigrations,
@@ -143,7 +144,8 @@ const swapPersistConfig = {
const addressBookPersistConfig = {
key: 'addressBook',
storage: localforage,
- version: 0,
+ version: Math.max(...Object.keys(clearAddressBookMigrations).map(Number)),
+ migrate: createMigrate(clearAddressBookMigrations, { debug: false }),
}
export const sliceReducers = {
diff --git a/src/state/slices/addressBookSlice/addressBookSlice.ts b/src/state/slices/addressBookSlice/addressBookSlice.ts
index 6106051b410..1b314ff35f6 100644
--- a/src/state/slices/addressBookSlice/addressBookSlice.ts
+++ b/src/state/slices/addressBookSlice/addressBookSlice.ts
@@ -15,7 +15,7 @@ export type AddressBookState = {
ids: string[]
}
-const initialState: AddressBookState = {
+export const initialState: AddressBookState = {
byId: {},
ids: [],
}
From 8373ce24694951ed7c860f5bc432f01c5659224e Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:53:10 +0200
Subject: [PATCH 17/36] fix: feature flag
---
.env.development | 1 +
.env.production | 1 +
src/components/Modals/Send/views/Address.tsx | 12 ++++++++----
src/config.ts | 1 +
.../slices/preferencesSlice/preferencesSlice.ts | 2 ++
src/test/mocks/store.ts | 1 +
src/vite-env.d.ts | 1 +
7 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/.env.development b/.env.development
index 9d5d54cb71c..2f8ec1546ad 100644
--- a/.env.development
+++ b/.env.development
@@ -6,6 +6,7 @@
# feature flags
VITE_FEATURE_THORCHAIN_TCY_ACTIVITY=true
+VITE_FEATURE_ADDRESS_BOOK=true
# mixpanel
VITE_MIXPANEL_TOKEN=a867ce40912a6b7d01d088cf62b0e1ff
diff --git a/.env.production b/.env.production
index e32e5cacfd1..c040290c591 100644
--- a/.env.production
+++ b/.env.production
@@ -1,6 +1,7 @@
# feature flags
VITE_FEATURE_MIXPANEL=true
VITE_ENABLE_ADDRESSABLE=true
+VITE_FEATURE_ADDRESS_BOOK=false
VITE_FEATURE_THORCHAIN_TCY_ACTIVITY=false
diff --git a/src/components/Modals/Send/views/Address.tsx b/src/components/Modals/Send/views/Address.tsx
index 533fc3327e5..1eac8cd71d8 100644
--- a/src/components/Modals/Send/views/Address.tsx
+++ b/src/components/Modals/Send/views/Address.tsx
@@ -1,10 +1,10 @@
import {
Button,
+ Text as CText,
Flex,
FormControl,
Icon,
Stack,
- Text as CText,
useColorModeValue,
VStack,
} from '@chakra-ui/react'
@@ -29,6 +29,7 @@ import { DialogTitle } from '@/components/Modal/components/DialogTitle'
import { SelectAssetRoutes } from '@/components/SelectAssets/SelectAssetCommon'
import { SlideTransition } from '@/components/SlideTransition'
import { Text } from '@/components/Text'
+import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag'
import { useModal } from '@/hooks/useModal/useModal'
import { parseAddressInputWithChainId } from '@/lib/address/address'
import { selectAddressBookEntriesByChainNamespace } from '@/state/slices/addressBookSlice/selectors'
@@ -59,6 +60,7 @@ export const Address = () => {
const assetId = useWatch({ name: SendFormFields.AssetId })
const qrBackground = useColorModeValue('blackAlpha.200', 'whiteAlpha.200')
const addAddress = useModal('addAddress')
+ const isAddressBookEnabled = useFeatureFlag('AddressBook')
const location = useLocation()
const isFromQrCode = useMemo(() => location.state?.isFromQrCode === true, [location.state])
@@ -95,8 +97,8 @@ export const Address = () => {
}, [input, address, addressBookEntries, asset?.chainId])
useEffect(() => {
- setShowSaveButton(isCustomAddress && !!address && !addressError)
- }, [isCustomAddress, address, addressError])
+ setShowSaveButton(isCustomAddress && !!address && !addressError && isAddressBookEnabled)
+ }, [isCustomAddress, address, addressError, isAddressBookEnabled])
useEffect(() => {
trigger(SendFormFields.Input)
@@ -238,7 +240,9 @@ export const Address = () => {
-
+ {isAddressBookEnabled && (
+
+ )}
diff --git a/src/config.ts b/src/config.ts
index 537cba99b52..3f68aeebf6e 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -193,6 +193,7 @@ const validators = {
VITE_TENDERLY_ACCOUNT_SLUG: str(),
VITE_TENDERLY_PROJECT_SLUG: str(),
VITE_TENDERLY_API_KEY: str(),
+ VITE_FEATURE_ADDRESS_BOOK: bool({ default: false }),
}
function reporter({ errors }: envalid.ReporterOptions) {
diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts
index 548c54153ba..10a91de753a 100644
--- a/src/state/slices/preferencesSlice/preferencesSlice.ts
+++ b/src/state/slices/preferencesSlice/preferencesSlice.ts
@@ -91,6 +91,7 @@ export type FeatureFlags = {
QuickBuy: boolean
NewWalletManager: boolean
SwapperFiatRamps: boolean
+ AddressBook: boolean
}
export type Flag = keyof FeatureFlags
@@ -215,6 +216,7 @@ const initialState: Preferences = {
QuickBuy: getConfig().VITE_FEATURE_QUICK_BUY,
NewWalletManager: getConfig().VITE_FEATURE_NEW_WALLET_MANAGER,
SwapperFiatRamps: getConfig().VITE_FEATURE_SWAPPER_FIAT_RAMPS,
+ AddressBook: getConfig().VITE_FEATURE_ADDRESS_BOOK,
},
selectedLocale: simpleLocale(),
hasWalletSeenTcyClaimAlert: {},
diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts
index b4b8aaf50a2..e77901f7ce3 100644
--- a/src/test/mocks/store.ts
+++ b/src/test/mocks/store.ts
@@ -164,6 +164,7 @@ export const mockStore: ReduxState = {
NewWalletManager: false,
SwapperFiatRamps: false,
LedgerReadOnly: false,
+ AddressBook: false,
},
quickBuyAmounts: [10, 50, 100],
quoteDisplayOption: QuoteDisplayOption.Basic,
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 28e2d6448fb..aad94951ec7 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -116,6 +116,7 @@ interface ImportMetaEnv {
readonly VITE_TENDERLY_ACCOUNT_SLUG: string
readonly VITE_TENDERLY_PROJECT_SLUG: string
readonly VITE_TENDERLY_API_KEY: string
+ readonly VITE_FEATURE_ADDRESS_BOOK: string
// Unchained URLs and node URLs - present in all envs (prod, development, private)
// even though they're not present in base env
From 49c93da03d3890e7489c51a71eed8f68afeb9450 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 21 Oct 2025 14:53:27 +0200
Subject: [PATCH 18/36] fix: review feedbacks
---
src/components/Modals/Send/views/Address.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/Modals/Send/views/Address.tsx b/src/components/Modals/Send/views/Address.tsx
index 1eac8cd71d8..ea8cfc4549e 100644
--- a/src/components/Modals/Send/views/Address.tsx
+++ b/src/components/Modals/Send/views/Address.tsx
@@ -1,10 +1,10 @@
import {
Button,
- Text as CText,
Flex,
FormControl,
Icon,
Stack,
+ Text as CText,
useColorModeValue,
VStack,
} from '@chakra-ui/react'
From 4f90c096222f5c87cefe4569729ba5705999e531 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 21 Oct 2025 15:03:00 +0200
Subject: [PATCH 19/36] fix: review feedbacks
---
src/components/Modals/Send/AddressBook/ConfirmDelete.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/Modals/Send/AddressBook/ConfirmDelete.tsx b/src/components/Modals/Send/AddressBook/ConfirmDelete.tsx
index 3f6504b2def..1d65958bf17 100644
--- a/src/components/Modals/Send/AddressBook/ConfirmDelete.tsx
+++ b/src/components/Modals/Send/AddressBook/ConfirmDelete.tsx
@@ -25,14 +25,14 @@ export const ConfirmDelete = ({ entryName, onDelete, onClose, isOpen }: ConfirmD
}, [onDelete, onClose])
return (
-
From ce0f6773531e7a7933b42652a21a8394f77bdef5 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Mon, 27 Oct 2025 16:59:56 +0100
Subject: [PATCH 29/36] fix: review feedbacks
---
.../Modals/Send/AddressInput/AddressInput.tsx | 33 +++++++++++++++----
src/components/Modals/Send/views/Address.tsx | 14 +-------
.../Modals/Send/views/SendAmountDetails.tsx | 16 ---------
3 files changed, 27 insertions(+), 36 deletions(-)
diff --git a/src/components/Modals/Send/AddressInput/AddressInput.tsx b/src/components/Modals/Send/AddressInput/AddressInput.tsx
index 698e07b1e34..27ceb4bee46 100644
--- a/src/components/Modals/Send/AddressInput/AddressInput.tsx
+++ b/src/components/Modals/Send/AddressInput/AddressInput.tsx
@@ -27,6 +27,8 @@ import { SendFormFields, SendRoutes } from '../SendCommon'
import { QRCodeIcon } from '@/components/Icons/QRCode'
import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis'
import type { SendInput } from '@/components/Modals/Send/Form'
+import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag'
+import { useModal } from '@/hooks/useModal/useModal'
import { makeBlockiesUrl } from '@/lib/blockies/makeBlockiesUrl'
import { isUtxoAccountId } from '@/lib/utils/utxo'
import {
@@ -34,7 +36,7 @@ import {
selectInternalAccountIdByAddress,
} from '@/state/slices/addressBookSlice/selectors'
import { accountIdToLabel } from '@/state/slices/portfolioSlice/utils'
-import { selectPortfolioAccountMetadata } from '@/state/slices/selectors'
+import { selectAssetById, selectPortfolioAccountMetadata } from '@/state/slices/selectors'
import { useAppSelector } from '@/state/store'
type AddressInputProps = {
@@ -45,7 +47,7 @@ type AddressInputProps = {
resolvedAddress?: string
isReadOnly?: boolean
chainId?: ChainId
- onSaveContact?: (e: React.MouseEvent) => void
+ shouldShowSaveButton?: boolean
} & Omit
const addressInputSx = {
@@ -63,7 +65,7 @@ export const AddressInput = ({
isReadOnly = false,
resolvedAddress,
chainId,
- onSaveContact,
+ shouldShowSaveButton = true,
onFocus,
onBlur,
onPaste,
@@ -76,8 +78,24 @@ export const AddressInput = ({
const isDirty = useFormContext().formState.isDirty
const isValidating = useFormContext().formState.isValidating
- const { vanityAddress, input: value } = useWatch() as Partial
+ const isAddressBookEnabled = useFeatureFlag('AddressBook')
+
+ const { vanityAddress, input: value, to, assetId } = useWatch() as Partial
const inputRef = useRef(null)
+ const addressBookSaveModal = useModal('addressBookSave')
+
+ const asset = useAppSelector(state => selectAssetById(state, assetId ?? ''))
+
+ const handleSaveContact = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+
+ if (to && asset?.chainId) {
+ addressBookSaveModal.open({ address: to, chainId: asset.chainId })
+ }
+ },
+ [to, asset?.chainId, addressBookSaveModal],
+ )
const addressBookEntryFilter = useMemo(
() => ({
@@ -316,8 +334,8 @@ export const AddressInput = ({
ensOrRawAddress
)}
- {onSaveContact && !internalAccountId && (
-
+ {isAddressBookEnabled && shouldShowSaveButton && !internalAccountId && (
+
{translate('common.save')}
)}
@@ -341,7 +359,8 @@ export const AddressInput = ({
internalAccountId,
internalAccountLabel,
avatarUrl,
- onSaveContact,
+ shouldShowSaveButton,
+ isAddressBookEnabled,
],
)
diff --git a/src/components/Modals/Send/views/Address.tsx b/src/components/Modals/Send/views/Address.tsx
index 285369247fb..e7c77edb2ec 100644
--- a/src/components/Modals/Send/views/Address.tsx
+++ b/src/components/Modals/Send/views/Address.tsx
@@ -62,7 +62,6 @@ export const Address = () => {
const qrCode = useModal('qrCode')
const assetId = useWatch({ name: SendFormFields.AssetId })
const qrBackground = useColorModeValue('blackAlpha.200', 'whiteAlpha.200')
- const addressBookSaveModal = useModal('addressBookSave')
const isAddressBookEnabled = useFeatureFlag('AddressBook')
const location = useLocation()
@@ -194,17 +193,6 @@ export const Address = () => {
[setValue, navigate],
)
- const handleSaveAddress = useCallback(
- (e: React.MouseEvent) => {
- e.stopPropagation()
-
- if (address && asset?.chainId) {
- addressBookSaveModal.open({ address, chainId: asset.chainId })
- }
- },
- [address, asset?.chainId, addressBookSaveModal],
- )
-
const handleEmptyChange = useCallback(() => {
setValue(SendFormFields.Input, '', { shouldValidate: true })
setValue(SendFormFields.To, '')
@@ -231,8 +219,8 @@ export const Address = () => {
)}
resolvedAddress={address}
chainId={asset?.chainId}
- onSaveContact={showSaveButton ? handleSaveAddress : undefined}
onEmptied={handleEmptyChange}
+ shouldShowSaveButton={showSaveButton}
/>
diff --git a/src/components/Modals/Send/views/SendAmountDetails.tsx b/src/components/Modals/Send/views/SendAmountDetails.tsx
index 997eb3dc2fd..e23ca7e319f 100644
--- a/src/components/Modals/Send/views/SendAmountDetails.tsx
+++ b/src/components/Modals/Send/views/SendAmountDetails.tsx
@@ -43,9 +43,7 @@ import { AddressInput } from '@/components/Modals/Send/AddressInput/AddressInput
import { SendMaxButton } from '@/components/Modals/Send/SendMaxButton/SendMaxButton'
import { SlideTransition } from '@/components/SlideTransition'
import { Text } from '@/components/Text/Text'
-import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag'
import { useLocaleFormatter } from '@/hooks/useLocaleFormatter/useLocaleFormatter'
-import { useModal } from '@/hooks/useModal/useModal'
import { parseAddressInputWithChainId } from '@/lib/address/address'
import { bnOrZero } from '@/lib/bignumber/bignumber'
import { allowedDecimalSeparators } from '@/state/slices/preferencesSlice/preferencesSlice'
@@ -147,8 +145,6 @@ export const SendAmountDetails = () => {
const supportsENS = asset?.chainId === ethChainId
const addressError = get(errors, `${SendFormFields.Input}.message`, null)
- const addressBookSaveModal = useModal('addressBookSave')
- const isAddressBookEnabled = useFeatureFlag('AddressBook')
useEffect(() => {
trigger(SendFormFields.Input)
@@ -239,17 +235,6 @@ export const SendAmountDetails = () => {
[handleInputChange],
)
- const handleSaveContact = useCallback(
- (e: React.MouseEvent) => {
- e.stopPropagation()
-
- if (to && asset?.chainId) {
- addressBookSaveModal.open({ address: to, chainId: asset.chainId })
- }
- },
- [to, asset?.chainId, addressBookSaveModal],
- )
-
const renderController = useCallback(
({ field: { onChange, value } }: { field: any }) => {
return (
@@ -323,7 +308,6 @@ export const SendAmountDetails = () => {
supportsENS ? 'modals.send.toAddressOrEns' : 'modals.send.toAddress',
)}
resolvedAddress={to}
- onSaveContact={isAddressBookEnabled ? handleSaveContact : undefined}
chainId={asset?.chainId}
isReadOnly
onClick={handleBackClick}
From 733f1334e05d0092f94488041214a45f68570787 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Mon, 27 Oct 2025 17:01:37 +0100
Subject: [PATCH 30/36] fix: review feedbacks
---
src/components/Modals/Send/AddressInput/AddressInput.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/components/Modals/Send/AddressInput/AddressInput.tsx b/src/components/Modals/Send/AddressInput/AddressInput.tsx
index 27ceb4bee46..31507e93107 100644
--- a/src/components/Modals/Send/AddressInput/AddressInput.tsx
+++ b/src/components/Modals/Send/AddressInput/AddressInput.tsx
@@ -3,6 +3,7 @@ import {
Avatar,
Box,
Button,
+ Text as CText,
Flex,
HStack,
IconButton,
@@ -10,7 +11,6 @@ import {
InputGroup,
InputLeftElement,
InputRightElement,
- Text as CText,
Text,
VStack,
} from '@chakra-ui/react'
@@ -361,6 +361,8 @@ export const AddressInput = ({
avatarUrl,
shouldShowSaveButton,
isAddressBookEnabled,
+ vanityAddress,
+ ensOrRawAddress,
],
)
From 265fd6331580af7aac3b333a1abb48d71b7fb2c1 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Mon, 27 Oct 2025 17:10:56 +0100
Subject: [PATCH 31/36] fix: review feedbacks
---
src/components/Modals/Send/AddressInput/AddressInput.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/Modals/Send/AddressInput/AddressInput.tsx b/src/components/Modals/Send/AddressInput/AddressInput.tsx
index 31507e93107..5995d9cecde 100644
--- a/src/components/Modals/Send/AddressInput/AddressInput.tsx
+++ b/src/components/Modals/Send/AddressInput/AddressInput.tsx
@@ -3,7 +3,6 @@ import {
Avatar,
Box,
Button,
- Text as CText,
Flex,
HStack,
IconButton,
@@ -11,6 +10,7 @@ import {
InputGroup,
InputLeftElement,
InputRightElement,
+ Text as CText,
Text,
VStack,
} from '@chakra-ui/react'
From f0c87bbf5491832dccc03b880505696a05932353 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 28 Oct 2025 10:26:39 +0100
Subject: [PATCH 32/36] fix: review feedbacks
---
src/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/index.tsx b/src/index.tsx
index dad58289ffe..c54f969132b 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -23,7 +23,7 @@ import { renderConsoleArt } from './lib/consoleArt'
import { reportWebVitals } from './lib/reportWebVitals'
import { httpClientIntegration } from './utils/sentry/httpclient'
-const enableReactScan = true
+const enableReactScan = false
const SENTRY_ENABLED = true
From 07894f54b5158a35f3206bb27c6f0734b36a9a96 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Tue, 28 Oct 2025 10:28:24 +0100
Subject: [PATCH 33/36] fix: review feedbacks
---
src/components/CryptoFiatInput/CryptoFiatInput.tsx | 2 +-
src/components/Modals/Send/views/SendAmountDetails.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/components/CryptoFiatInput/CryptoFiatInput.tsx b/src/components/CryptoFiatInput/CryptoFiatInput.tsx
index 16885e455de..33c4b333d79 100644
--- a/src/components/CryptoFiatInput/CryptoFiatInput.tsx
+++ b/src/components/CryptoFiatInput/CryptoFiatInput.tsx
@@ -142,7 +142,7 @@ export const CryptoFiatInput = ({
)}
-
+
diff --git a/src/components/Modals/Send/views/SendAmountDetails.tsx b/src/components/Modals/Send/views/SendAmountDetails.tsx
index 7007f2d13ce..aabd28c392d 100644
--- a/src/components/Modals/Send/views/SendAmountDetails.tsx
+++ b/src/components/Modals/Send/views/SendAmountDetails.tsx
@@ -11,7 +11,7 @@ import {
Skeleton,
Stack,
Tooltip,
- VStack
+ VStack,
} from '@chakra-ui/react'
import { CHAIN_NAMESPACE, ethChainId, fromAssetId } from '@shapeshiftoss/caip'
import get from 'lodash/get'
From 757a7ddd6631de0719b78e63f9b94a87b7039543 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Thu, 30 Oct 2025 12:57:26 +0100
Subject: [PATCH 34/36] fix: review feedbacks
---
.../Modals/Send/AddressBook/AddressBook.tsx | 12 ++-
.../Modals/Send/AddressBookSaveModal.tsx | 8 +-
.../Modals/Send/AddressInput/AddressInput.tsx | 4 +-
src/components/Modals/Send/views/Address.tsx | 16 ++--
src/components/Modals/Send/views/Confirm.tsx | 7 +-
.../LimitOrder/hooks/useLimitOrders.tsx | 4 +-
.../addressBookSlice/addressBookSlice.ts | 74 ++++++-------------
.../slices/addressBookSlice/selectors.ts | 61 ++++++++-------
src/state/slices/addressBookSlice/types.ts | 18 ++---
src/state/slices/addressBookSlice/utils.ts | 14 ++--
src/state/slices/common-selectors.ts | 21 ++++--
11 files changed, 110 insertions(+), 129 deletions(-)
diff --git a/src/components/Modals/Send/AddressBook/AddressBook.tsx b/src/components/Modals/Send/AddressBook/AddressBook.tsx
index ec994eaed2e..d7e54bc3f01 100644
--- a/src/components/Modals/Send/AddressBook/AddressBook.tsx
+++ b/src/components/Modals/Send/AddressBook/AddressBook.tsx
@@ -1,6 +1,5 @@
import { Box, HStack, Icon, Text as CText, useDisclosure, VStack } from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
-import { toAccountId } from '@shapeshiftoss/caip'
import { useCallback, useMemo, useState } from 'react'
import { useFormContext, useWatch } from 'react-hook-form'
import { FaRegAddressBook } from 'react-icons/fa'
@@ -10,6 +9,7 @@ import { SendFormFields } from '../SendCommon'
import { AddressBookEntryButton } from '@/components/Modals/Send/AddressBook/AddressBookEntryButton'
import { ConfirmDelete } from '@/components/Modals/Send/AddressBook/ConfirmDelete'
+import type { SendInput } from '@/components/Modals/Send/Form'
import { Text } from '@/components/Text'
import { makeBlockiesUrl } from '@/lib/blockies/makeBlockiesUrl'
import { addressBookSlice } from '@/state/slices/addressBookSlice/addressBookSlice'
@@ -41,7 +41,7 @@ export const AddressBook = ({
const {
control,
formState: { errors },
- } = useFormContext()
+ } = useFormContext()
const { isOpen, onClose, onOpen } = useDisclosure()
const [selectedDeleteEntry, setSelectedDeleteEntry] = useState(null)
@@ -50,7 +50,7 @@ export const AddressBook = ({
const input = useWatch({
control,
name: SendFormFields.Input,
- }) as string
+ })
const addressBookEntriesFilter = useMemo(() => ({ chainId }), [chainId])
const addressBookEntries = useAppSelector(state =>
@@ -107,15 +107,13 @@ export const AddressBook = ({
return
return entries?.map(entry => {
- const entryKey = toAccountId({ chainId: entry.chainId, account: entry.address })
-
return (
diff --git a/src/components/Modals/Send/AddressBookSaveModal.tsx b/src/components/Modals/Send/AddressBookSaveModal.tsx
index 71408a3d4ed..3c3bf1199a1 100644
--- a/src/components/Modals/Send/AddressBookSaveModal.tsx
+++ b/src/components/Modals/Send/AddressBookSaveModal.tsx
@@ -8,6 +8,7 @@ import {
VStack,
} from '@chakra-ui/react'
import type { ChainId } from '@shapeshiftoss/caip'
+import { toAccountId } from '@shapeshiftoss/caip'
import { useCallback, useMemo } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslate } from 'react-polyglot'
@@ -28,6 +29,7 @@ import { useAppDispatch, useAppSelector } from '@/state/store'
export type AddressBookSaveModalProps = {
address: string
+ vanityAddress?: string
chainId: ChainId
onSuccess?: () => void
}
@@ -47,6 +49,7 @@ const hoverInputStyle = {
export const AddressBookSaveModal = ({
address,
+ vanityAddress,
chainId,
onSuccess,
}: AddressBookSaveModalProps) => {
@@ -74,7 +77,7 @@ export const AddressBookSaveModal = ({
} = useForm({
mode: 'onChange',
defaultValues: {
- label: '',
+ label: vanityAddress,
},
})
@@ -90,9 +93,10 @@ export const AddressBookSaveModal = ({
// Can't access here if it's an internal account id as its supposed to be added automatically as a system
if (internalAccountId) return
+ const accountId = toAccountId({ chainId, account: address })
const entry: AddressBookEntry = {
address,
- chainId,
+ accountId,
label: data.label,
isInternal: false,
isExternal: true,
diff --git a/src/components/Modals/Send/AddressInput/AddressInput.tsx b/src/components/Modals/Send/AddressInput/AddressInput.tsx
index 5995d9cecde..3c9fda8f0e1 100644
--- a/src/components/Modals/Send/AddressInput/AddressInput.tsx
+++ b/src/components/Modals/Send/AddressInput/AddressInput.tsx
@@ -91,10 +91,10 @@ export const AddressInput = ({
e.stopPropagation()
if (to && asset?.chainId) {
- addressBookSaveModal.open({ address: to, chainId: asset.chainId })
+ addressBookSaveModal.open({ address: to, vanityAddress, chainId: asset.chainId })
}
},
- [to, asset?.chainId, addressBookSaveModal],
+ [to, vanityAddress, asset?.chainId, addressBookSaveModal],
)
const addressBookEntryFilter = useMemo(
diff --git a/src/components/Modals/Send/views/Address.tsx b/src/components/Modals/Send/views/Address.tsx
index e7c77edb2ec..fc71f0a265c 100644
--- a/src/components/Modals/Send/views/Address.tsx
+++ b/src/components/Modals/Send/views/Address.tsx
@@ -48,7 +48,6 @@ const qrCodeSx = {
export const Address = () => {
const [isValidating, setIsValidating] = useState(false)
- const [showSaveButton, setShowSaveButton] = useState(false)
const navigate = useNavigate()
const translate = useTranslate()
const {
@@ -105,14 +104,12 @@ export const Address = () => {
const supportsENS = asset?.chainId === ethChainId // We only support ENS resolution on ETH mainnet
const addressError = get(errors, `${SendFormFields.Input}.message`, null)
- useEffect(() => {
- setShowSaveButton(
- Boolean(
- (!isInAddressBook || internalAccountId) &&
- !!address &&
- !addressError &&
- isAddressBookEnabled,
- ),
+ const showSaveButton = useMemo(() => {
+ return Boolean(
+ (!isInAddressBook || !internalAccountId) &&
+ !!address &&
+ !addressError &&
+ isAddressBookEnabled,
)
}, [isInAddressBook, address, addressError, isAddressBookEnabled, internalAccountId])
@@ -250,7 +247,6 @@ export const Address = () => {
)}
-
{
return accountIdToLabel(internalAccountId)
}
- // Fallback to "Account" if accountNumber is not available yet
- return accountNumber !== undefined
- ? translate('accounts.accountNumber', { accountNumber })
- : translate('common.account')
+ if (accountNumber === undefined) return
+
+ return translate('accounts.accountNumber', { accountNumber })
}, [internalAccountId, accountNumber, translate])
const displayDestinationContent = useMemo(() => {
diff --git a/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrders.tsx b/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrders.tsx
index d37a0c93569..37a44b091b4 100644
--- a/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrders.tsx
+++ b/src/components/MultiHopTrade/components/LimitOrder/hooks/useLimitOrders.tsx
@@ -12,16 +12,16 @@ import { useFeatureFlag } from '@/hooks/useFeatureFlag/useFeatureFlag'
import { useNotificationToast } from '@/hooks/useNotificationToast'
import { useWallet } from '@/hooks/useWallet/useWallet'
import { useGetLimitOrdersQuery } from '@/state/apis/limit-orders/limitOrderApi'
+import { selectPartitionedAccountIds } from '@/state/slices/common-selectors'
import {
selectAssetById,
- selectEvmAccountIds,
selectLimitOrderActionsByWallet,
selectPortfolioLoadingStatus,
} from '@/state/slices/selectors'
import { store, useAppSelector } from '@/state/store'
export const useLimitOrdersQuery = () => {
- const evmAccountIds = useAppSelector(selectEvmAccountIds)
+ const { evmAccountIds } = useAppSelector(selectPartitionedAccountIds)
const portfolioLoadingStatus = useAppSelector(selectPortfolioLoadingStatus)
const {
state: { isLoadingLocalWallet, modal, isConnected },
diff --git a/src/state/slices/addressBookSlice/addressBookSlice.ts b/src/state/slices/addressBookSlice/addressBookSlice.ts
index c94b803c4d1..09e9b1e687b 100644
--- a/src/state/slices/addressBookSlice/addressBookSlice.ts
+++ b/src/state/slices/addressBookSlice/addressBookSlice.ts
@@ -1,9 +1,8 @@
import type { PayloadAction } from '@reduxjs/toolkit'
import { createSlice } from '@reduxjs/toolkit'
-import type { AccountId, ChainId } from '@shapeshiftoss/caip'
-import { CHAIN_NAMESPACE, fromChainId, toAccountId } from '@shapeshiftoss/caip'
+import type { AccountId } from '@shapeshiftoss/caip'
+import { CHAIN_NAMESPACE, fromAccountId, fromChainId } from '@shapeshiftoss/caip'
-import { getChainAdapterManager } from '@/context/PluginProvider/chainAdapterSingleton'
import type { AddressBookEntry } from '@/state/slices/addressBookSlice/types'
import { isDuplicateEntry } from '@/state/slices/addressBookSlice/utils'
@@ -15,16 +14,6 @@ export const initialState: AddressBookState = {
byAccountId: {},
}
-const getSupportedEvmChainIds = (): ChainId[] => {
- const chainAdapterManager = getChainAdapterManager()
- const chainIds = Array.from(chainAdapterManager.keys())
-
- return chainIds.filter((chainId: ChainId) => {
- const { chainNamespace } = fromChainId(chainId)
- return chainNamespace === CHAIN_NAMESPACE.Evm
- })
-}
-
export const addressBookSlice = createSlice({
name: 'addressBook',
initialState,
@@ -35,59 +24,44 @@ export const addressBookSlice = createSlice({
}
const entry = action.payload
- const { chainNamespace } = fromChainId(entry.chainId)
-
- // For EVM entries (both external and own account), create entries for ALL supported EVM chains
- if (chainNamespace === CHAIN_NAMESPACE.Evm) {
- const supportedEvmChainIds = getSupportedEvmChainIds()
-
- // Create an entry for each supported EVM chain
- supportedEvmChainIds.forEach(chainId => {
- const evmEntry: AddressBookEntry = {
- ...entry,
- chainId,
- }
-
- const key = toAccountId({ chainId: evmEntry.chainId, account: evmEntry.address })
-
- state.byAccountId[key] = evmEntry
- })
-
- return
- }
-
- // For non-EVM entries, add single entry
- const key = toAccountId({ chainId: entry.chainId, account: entry.address })
- state.byAccountId[key] = entry
+ // Store single entry regardless of chain namespace
+ // Selectors will handle cross-chain EVM matching
+ state.byAccountId[entry.accountId] = entry
}),
deleteAddress: create.reducer((state, action: PayloadAction) => {
- const key = toAccountId({ chainId: action.payload.chainId, account: action.payload.address })
- const entryToDelete = state.byAccountId[key]
+ const entryToDelete = state.byAccountId[action.payload.accountId]
if (!entryToDelete) return
- const { chainNamespace } = fromChainId(entryToDelete.chainId)
+ const { chainId } = fromAccountId(entryToDelete.accountId)
+ const { chainNamespace } = fromChainId(chainId)
- // For EVM entries (both external and own account), delete ALL entries with this address across all EVM chains
+ // For EVM entries, we need to find and delete the actual stored entry
+ // (which might be stored under a different EVM chain's accountId)
if (chainNamespace === CHAIN_NAMESPACE.Evm) {
const normalizedAddress = entryToDelete.address.toLowerCase()
- // Find and delete all EVM entries with this address
- Object.values(state.byAccountId).forEach(entry => {
- const entryChainNamespace = fromChainId(entry.chainId).chainNamespace
- if (
+ // Find and delete the EVM entry with this address
+ const entryToDeleteKey = Object.keys(state.byAccountId).find(accountId => {
+ const entry = state.byAccountId[accountId]
+ const { chainId: entryChainId } = fromAccountId(accountId)
+ const entryChainNamespace = fromChainId(entryChainId).chainNamespace
+
+ return (
entryChainNamespace === CHAIN_NAMESPACE.Evm &&
entry.address.toLowerCase() === normalizedAddress
- ) {
- const key = toAccountId({ chainId: entry.chainId, account: entry.address })
- delete state.byAccountId[key]
- }
+ )
})
+ if (entryToDeleteKey) {
+ delete state.byAccountId[entryToDeleteKey]
+ }
+
return
}
- delete state.byAccountId[key]
+ // For non-EVM, just delete the single entry
+ delete state.byAccountId[entryToDelete.accountId]
}),
clear: create.reducer(() => initialState),
}),
diff --git a/src/state/slices/addressBookSlice/selectors.ts b/src/state/slices/addressBookSlice/selectors.ts
index ca6573b6357..9170c6c1467 100644
--- a/src/state/slices/addressBookSlice/selectors.ts
+++ b/src/state/slices/addressBookSlice/selectors.ts
@@ -1,6 +1,5 @@
import { CHAIN_NAMESPACE, fromAccountId, fromChainId } from '@shapeshiftoss/caip'
import { matchSorter } from 'match-sorter'
-import { createSelector } from 'reselect'
import { addressBookSlice } from './addressBookSlice'
@@ -10,10 +9,10 @@ import {
selectChainIdParamFromFilter,
selectSearchQueryFromFilter,
} from '@/state/selectors'
-import { selectAccountIdsWithoutEvms, selectEvmAccountIds } from '@/state/slices/common-selectors'
+import { selectPartitionedAccountIds } from '@/state/slices/common-selectors'
-export const selectAddressBookEntries = createSelector(
- [addressBookSlice.selectors.selectEntriesByAccountId],
+export const selectAddressBookEntries = createDeepEqualOutputSelector(
+ addressBookSlice.selectors.selectEntriesByAccountId,
byAccountId => Object.values(byAccountId),
)
@@ -23,7 +22,20 @@ export const selectAddressBookEntriesByChainId = createDeepEqualOutputSelector(
(chainId, entries) => {
if (!chainId) return []
- return entries.filter(entry => entry.chainId === chainId)
+ const { chainNamespace } = fromChainId(chainId)
+
+ return entries.filter(entry => {
+ const { chainId: entryChainId } = fromAccountId(entry.accountId)
+ const { chainNamespace: entryChainNamespace } = fromChainId(entryChainId)
+
+ // For EVM chains, return all EVM entries (cross-chain matching)
+ if (chainNamespace === CHAIN_NAMESPACE.Evm && entryChainNamespace === CHAIN_NAMESPACE.Evm) {
+ return true
+ }
+
+ // For non-EVM chains, exact chain match only
+ return entryChainId === chainId
+ })
},
)
@@ -36,7 +48,7 @@ export const selectAddressBookEntriesBySearchQuery = createDeepEqualOutputSelect
const matchedEntries = matchSorter(entries, searchQuery, {
keys: [
- { key: 'label', threshold: matchSorter.rankings.MATCHES },
+ { key: 'label', threshold: matchSorter.rankings.CONTAINS },
{ key: 'address', threshold: matchSorter.rankings.CONTAINS },
],
})
@@ -59,10 +71,11 @@ export const selectIsAddressInAddressBook = createDeepEqualOutputSelector(
const normalizedAddress = accountAddress.toLowerCase()
const { chainNamespace } = fromChainId(chainId)
- // For EVM chains, check if address exists in any EVM entry
+ // For EVM chains, check if address exists in any entry
if (chainNamespace === CHAIN_NAMESPACE.Evm) {
return entries.some(entry => {
- const entryChainNamespace = fromChainId(entry.chainId).chainNamespace
+ const { chainId: entryChainId } = fromAccountId(entry.accountId)
+ const entryChainNamespace = fromChainId(entryChainId).chainNamespace
return (
entry.isExternal &&
entryChainNamespace === CHAIN_NAMESPACE.Evm &&
@@ -72,12 +85,14 @@ export const selectIsAddressInAddressBook = createDeepEqualOutputSelector(
}
// For non-EVM chains, check for exact chain + address match
- return entries.some(
- entry =>
+ return entries.some(entry => {
+ const { chainId: entryChainId } = fromAccountId(entry.accountId)
+ return (
entry.isExternal &&
- entry.chainId === chainId &&
- entry.address.toLowerCase() === normalizedAddress,
- )
+ entryChainId === chainId &&
+ entry.address.toLowerCase() === normalizedAddress
+ )
+ })
},
)
@@ -89,9 +104,8 @@ export const selectIsAddressInAddressBook = createDeepEqualOutputSelector(
export const selectInternalAccountIdByAddress = createDeepEqualOutputSelector(
selectAccountAddressParamFromFilter,
selectChainIdParamFromFilter,
- selectAccountIdsWithoutEvms,
- selectEvmAccountIds,
- (accountAddress, chainId, accountIdsWithoutEvms, evmAccountIds) => {
+ selectPartitionedAccountIds,
+ (accountAddress, chainId, { evmAccountIds, nonEvmAccountIds }) => {
if (!accountAddress || !chainId) return null
const normalizedAddress = accountAddress.toLowerCase()
@@ -99,22 +113,17 @@ export const selectInternalAccountIdByAddress = createDeepEqualOutputSelector(
// For EVM chains, find first matching EVM account
if (chainNamespace === CHAIN_NAMESPACE.Evm) {
- const accountId = evmAccountIds.find(accountId => {
- const { account, chainId: accountChainId } = fromAccountId(accountId)
- const accountChainNamespace = fromChainId(accountChainId).chainNamespace
-
- return (
- accountChainNamespace === CHAIN_NAMESPACE.Evm &&
- account.toLowerCase() === normalizedAddress
- )
+ const accountId = evmAccountIds.find(evmAccountId => {
+ const { account } = fromAccountId(evmAccountId)
+ return account.toLowerCase() === normalizedAddress
})
return accountId ?? null
}
// For non-EVM chains, find exact chainId + address match
- const accountId = accountIdsWithoutEvms.find(accountId => {
- const { account, chainId: accountChainId } = fromAccountId(accountId)
+ const accountId = nonEvmAccountIds.find(nonEvmAccountId => {
+ const { account, chainId: accountChainId } = fromAccountId(nonEvmAccountId)
return accountChainId === chainId && account.toLowerCase() === normalizedAddress
})
return accountId ?? null
diff --git a/src/state/slices/addressBookSlice/types.ts b/src/state/slices/addressBookSlice/types.ts
index 0f23198f6a7..33c83daf019 100644
--- a/src/state/slices/addressBookSlice/types.ts
+++ b/src/state/slices/addressBookSlice/types.ts
@@ -1,17 +1,9 @@
-import type { AccountId, ChainId } from '@shapeshiftoss/caip'
+import type { AccountId } from '@shapeshiftoss/caip'
export type AddressBookEntry = {
label: string
address: string
- chainId: ChainId
-} & (
- | {
- isExternal: true
- isInternal: false
- }
- | {
- isInternal: true
- isExternal: false
- accountId: AccountId
- }
-)
+ isExternal: boolean
+ isInternal: boolean
+ accountId: AccountId
+}
diff --git a/src/state/slices/addressBookSlice/utils.ts b/src/state/slices/addressBookSlice/utils.ts
index 388606a4672..bb4c82d72e0 100644
--- a/src/state/slices/addressBookSlice/utils.ts
+++ b/src/state/slices/addressBookSlice/utils.ts
@@ -1,4 +1,4 @@
-import { CHAIN_NAMESPACE, fromChainId, toAccountId } from '@shapeshiftoss/caip'
+import { CHAIN_NAMESPACE, fromAccountId, fromChainId } from '@shapeshiftoss/caip'
import type { AddressBookEntry } from '@/state/slices/addressBookSlice/types'
@@ -7,22 +7,23 @@ export const isDuplicateEntry = (
newEntry: AddressBookEntry,
): boolean => {
if (newEntry.isInternal) {
- const newKey = toAccountId({ chainId: newEntry.chainId, account: newEntry.address })
- return newKey in entries
+ return newEntry.accountId in entries
}
if (!newEntry.isExternal) {
throw new Error('Invalid entry type')
}
- const { chainNamespace } = fromChainId(newEntry.chainId)
+ const { chainId } = fromAccountId(newEntry.accountId)
+ const { chainNamespace } = fromChainId(chainId)
const normalizedAddress = newEntry.address.toLowerCase()
// For EVM chains, check if this address exists on ANY EVM chain
if (chainNamespace === CHAIN_NAMESPACE.Evm) {
return Object.values(entries).some(entry => {
if (!entry.isExternal) return false
- const entryChainNamespace = fromChainId(entry.chainId).chainNamespace
+ const { chainId: entryChainId } = fromAccountId(entry.accountId)
+ const entryChainNamespace = fromChainId(entryChainId).chainNamespace
return (
entryChainNamespace === CHAIN_NAMESPACE.Evm &&
@@ -32,6 +33,5 @@ export const isDuplicateEntry = (
}
// For non-EVM chains, use exact key match
- const newKey = toAccountId({ chainId: newEntry.chainId, account: newEntry.address })
- return newKey in entries
+ return newEntry.accountId in entries
}
diff --git a/src/state/slices/common-selectors.ts b/src/state/slices/common-selectors.ts
index 1bf8c0cfc79..a264ed99179 100644
--- a/src/state/slices/common-selectors.ts
+++ b/src/state/slices/common-selectors.ts
@@ -68,14 +68,23 @@ export const selectWalletAccountIds = createDeepEqualOutputSelector(
},
)
-export const selectEvmAccountIds = createDeepEqualOutputSelector(
+export const selectPartitionedAccountIds = createDeepEqualOutputSelector(
selectEnabledWalletAccountIds,
- accountIds => accountIds.filter(accountId => isEvmChainId(fromAccountId(accountId).chainId)),
-)
+ accountIds => {
+ const evmAccountIds: AccountId[] = []
+ const nonEvmAccountIds: AccountId[] = []
-export const selectAccountIdsWithoutEvms = createDeepEqualOutputSelector(
- selectEnabledWalletAccountIds,
- accountIds => accountIds.filter(accountId => !isEvmChainId(fromAccountId(accountId).chainId)),
+ accountIds.forEach(accountId => {
+ const { chainId } = fromAccountId(accountId)
+ if (isEvmChainId(chainId)) {
+ evmAccountIds.push(accountId)
+ } else {
+ nonEvmAccountIds.push(accountId)
+ }
+ })
+
+ return { evmAccountIds, nonEvmAccountIds }
+ },
)
export const selectWalletConnectedChainIds = createDeepEqualOutputSelector(
From 07c34244e44467ca25b14f6c6b5d53c25d864904 Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Thu, 30 Oct 2025 13:10:00 +0100
Subject: [PATCH 35/36] fix: review feedbacks
---
src/components/Modals/Send/views/Address.tsx | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/src/components/Modals/Send/views/Address.tsx b/src/components/Modals/Send/views/Address.tsx
index fc71f0a265c..e7b905f6ab9 100644
--- a/src/components/Modals/Send/views/Address.tsx
+++ b/src/components/Modals/Send/views/Address.tsx
@@ -1,10 +1,10 @@
import {
Button,
+ Text as CText,
Flex,
FormControl,
Icon,
Stack,
- Text as CText,
useColorModeValue,
VStack,
} from '@chakra-ui/react'
@@ -106,10 +106,7 @@ export const Address = () => {
const showSaveButton = useMemo(() => {
return Boolean(
- (!isInAddressBook || !internalAccountId) &&
- !!address &&
- !addressError &&
- isAddressBookEnabled,
+ !isInAddressBook && !internalAccountId && !!address && !addressError && isAddressBookEnabled,
)
}, [isInAddressBook, address, addressError, isAddressBookEnabled, internalAccountId])
From 4ed769ce46c8c49d7a1268c3cb70a798fdd8504e Mon Sep 17 00:00:00 2001
From: NeOMakinG <14963751+NeOMakinG@users.noreply.github.com>
Date: Thu, 30 Oct 2025 13:22:23 +0100
Subject: [PATCH 36/36] fix: review feedbacks
---
src/components/Modals/Send/views/Address.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/Modals/Send/views/Address.tsx b/src/components/Modals/Send/views/Address.tsx
index e7b905f6ab9..09e70b2ac19 100644
--- a/src/components/Modals/Send/views/Address.tsx
+++ b/src/components/Modals/Send/views/Address.tsx
@@ -1,10 +1,10 @@
import {
Button,
- Text as CText,
Flex,
FormControl,
Icon,
Stack,
+ Text as CText,
useColorModeValue,
VStack,
} from '@chakra-ui/react'