Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e0aa2ab
feat: new mobile send flow
NeOMakinG Oct 3, 2025
18c9102
fix: finish amount screen
NeOMakinG Oct 6, 2025
05fa17f
fix: send details
NeOMakinG Oct 7, 2025
6a44104
fix: finish betweek quote
NeOMakinG Oct 7, 2025
5a59b81
fix: hnnng desktop hnnng
NeOMakinG Oct 8, 2025
d581be6
fix: review feedbacks
NeOMakinG Oct 8, 2025
214f0d9
Merge branch 'develop' into send-flow-mobile
NeOMakinG Oct 8, 2025
cc3d2b1
fix: address book babt
NeOMakinG Oct 13, 2025
54a7b3e
fix: qr code and default asset route
NeOMakinG Oct 13, 2025
09cb446
Merge branch 'send-flow-mobile' of github.com:shapeshift/web into sen…
NeOMakinG Oct 13, 2025
d43913b
Merge branch 'send-flow-mobile' into address-book
NeOMakinG Oct 13, 2025
957ff34
fix: continue
NeOMakinG Oct 15, 2025
e514817
fix: review feedbacks
NeOMakinG Oct 20, 2025
e82c56b
Merge remote-tracking branch 'origin/develop' into address-book
NeOMakinG Oct 20, 2025
92c20c1
fix: review feedbacks
NeOMakinG Oct 20, 2025
f07dd93
fix: review feedbacks
NeOMakinG Oct 21, 2025
cb2e498
fix: review feedbacks
NeOMakinG Oct 21, 2025
5b34a3f
fix: review feedbacks
NeOMakinG Oct 21, 2025
71a9ecd
fix: review feedbacks
NeOMakinG Oct 21, 2025
9a979c3
fix: review feedbacks
NeOMakinG Oct 21, 2025
8373ce2
fix: feature flag
NeOMakinG Oct 21, 2025
49c93da
fix: review feedbacks
NeOMakinG Oct 21, 2025
4f90c09
fix: review feedbacks
NeOMakinG Oct 21, 2025
58ff44f
fix: address CodeRabbit review comments
NeOMakinG Oct 22, 2025
526fb38
chore: run lint:fix
NeOMakinG Oct 22, 2025
f373009
fix: review feedbacks
NeOMakinG Oct 22, 2025
a73e838
fix: review feedbacks
NeOMakinG Oct 27, 2025
c58d3e8
fix: review feedbacks
NeOMakinG Oct 27, 2025
5639b25
fix: review feedbacks
NeOMakinG Oct 27, 2025
54f346b
fix: review feedbacks
NeOMakinG Oct 27, 2025
6c10dc2
fix: review feedbacks
NeOMakinG Oct 27, 2025
9e467f0
fix: review feedbacks
NeOMakinG Oct 27, 2025
ce0f677
fix: review feedbacks
NeOMakinG Oct 27, 2025
ee9c02e
Merge branch 'develop' into address-book
NeOMakinG Oct 27, 2025
733f133
fix: review feedbacks
NeOMakinG Oct 27, 2025
265fd63
fix: review feedbacks
NeOMakinG Oct 27, 2025
3954bac
Merge branch 'develop' into address-book
NeOMakinG Oct 28, 2025
f0c87bb
fix: review feedbacks
NeOMakinG Oct 28, 2025
07894f5
fix: review feedbacks
NeOMakinG Oct 28, 2025
bd63f10
Merge branch 'develop' into address-book
NeOMakinG Oct 30, 2025
757a7dd
fix: review feedbacks
NeOMakinG Oct 30, 2025
07c3424
fix: review feedbacks
NeOMakinG Oct 30, 2025
4ed769c
fix: review feedbacks
NeOMakinG Oct 30, 2025
97fd979
Merge branch 'develop' into address-book
NeOMakinG Oct 30, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: review feedbacks
  • Loading branch information
NeOMakinG committed Oct 27, 2025
commit a73e838503384cbfffdeabd84021619b08c0622b
4 changes: 2 additions & 2 deletions src/components/Modals/QrCode/QrCode.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { AccountId, AssetId } from '@shapeshiftoss/caip'
import { MemoryRouter } from 'react-router-dom'

import { sendRoutes } from '../Send/SendCommon'
import { initialEntries } from '../Send/SendCommon'
import { Form } from './Form'

import { Dialog } from '@/components/Modal/components/Dialog'
Expand All @@ -17,7 +17,7 @@ export const QrCodeModal = ({ assetId, accountId }: QrCodeModalProps) => {

return (
<Dialog isOpen={isOpen} onClose={close} isFullScreen>
<MemoryRouter initialEntries={sendRoutes}>
<MemoryRouter initialEntries={initialEntries}>
<Form assetId={assetId} accountId={accountId} />
</MemoryRouter>
</Dialog>
Expand Down
170 changes: 58 additions & 112 deletions src/components/Modals/Send/AddressBook/AddressBook.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,25 @@
import {
Avatar,
Box,
Button,
HStack,
Icon,
Text as CText,
useDisclosure,
VStack,
} from '@chakra-ui/react'
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, FaTrash } from 'react-icons/fa'
import { FaRegAddressBook } from 'react-icons/fa'
import { useTranslate } from 'react-polyglot'

import { SendFormFields } from '../SendCommon'

import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis'
import { AddressBookEntryButton } from '@/components/Modals/Send/AddressBook/AddressBookEntryButton'
import { ConfirmDelete } from '@/components/Modals/Send/AddressBook/ConfirmDelete'
import { Text } from '@/components/Text'
import { makeBlockiesUrl } from '@/lib/blockies/makeBlockiesUrl'
import type { AddressBookEntry } from '@/state/slices/addressBookSlice/addressBookSlice'
import { addressBookSlice } from '@/state/slices/addressBookSlice/addressBookSlice'
import {
selectAddressBookEntriesByChainNamespace,
selectAddressBookEntriesByChainId,
selectAddressBookEntriesBySearchQuery,
} from '@/state/slices/addressBookSlice/selectors'
import type { AddressBookEntry } from '@/state/slices/addressBookSlice/types'
import { useAppDispatch, useAppSelector } from '@/state/store'

type AddressBookEntryButtonProps = {
entry: AddressBookEntry
onSelect: (address: string) => void
onDelete: (id: string) => void
}

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 handleDelete = useCallback(
(id: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.preventDefault()
onDelete(id)
},
[onDelete],
)

return (
<Box
cursor='pointer'
alignItems='center'
justifyContent='space-between'
display='flex'
overflow='hidden'
width='full'
>
<HStack
px={2}
py={1}
borderRadius='lg'
spacing={3}
align='center'
flex={1}
minWidth={0}
onClick={handleClick}
transition='all 0.2s'
sx={addressSx}
me={2}
>
<Avatar src={avatarUrl} size='sm' flexShrink={0} />
<VStack align='start' spacing={0} flex={1} minWidth={0}>
<CText fontSize='md' fontWeight='semibold' color='text.primary' lineHeight={1}>
{entry.name}
</CText>
<MiddleEllipsis fontSize='sm' color='text.subtle' noOfLines={1} value={entry.address} />
</VStack>
</HStack>
<Button size='sm' onClick={handleDelete(entry.id)} sx={deleteButtonSx} flexShrink={0}>
<Icon as={FaTrash} boxSize={4} />
</Button>
</Box>
)
}

type AddressBookProps = {
chainId?: ChainId
onSelectEntry: (address: string) => void
Expand All @@ -120,35 +44,37 @@ export const AddressBook = ({
} = useFormContext()
const { isOpen, onClose, onOpen } = useDisclosure()
const [selectedDeleteEntry, setSelectedDeleteEntry] = useState<AddressBookEntry | null>(null)
const addressBookEntries = useAppSelector(state =>
selectAddressBookEntriesByChainNamespace(state, chainId ?? ''),
)

const address = useWatch({
control,
name: SendFormFields.To,
}) as string
const addressError = errors[SendFormFields.Input]?.message ?? null

const input = useWatch({
control,
name: SendFormFields.Input,
}) as string

const addressBookEntriesFilter = useMemo(() => ({ chainId }), [chainId])
const addressBookEntries = useAppSelector(state =>
selectAddressBookEntriesByChainId(state, addressBookEntriesFilter),
)

const selectedEntry = useMemo(() => {
return addressBookEntries.find(entry => entry.address === input)
if (!input) return undefined
return addressBookEntries?.find(entry => entry.isExternal && entry.address === input)
}, [addressBookEntries, input])

// Only run expensive search when we have input
const addressBookSearchEntriesFilter = useMemo(
() => ({ chainId, searchQuery: input }),
[chainId, input],
)

const addressBookSearchEntries = useAppSelector(state =>
selectAddressBookEntriesBySearchQuery(state, {
chainId: chainId ?? '',
searchQuery: input ?? '',
}),
selectAddressBookEntriesBySearchQuery(state, addressBookSearchEntriesFilter),
)

const handleDelete = useCallback(
(id: string) => () => {
dispatch(addressBookSlice.actions.deleteAddress(id))
(entry: AddressBookEntry) => () => {
dispatch(addressBookSlice.actions.deleteAddress(entry))
},
[dispatch],
)
Expand All @@ -162,10 +88,41 @@ export const AddressBook = ({
)

const entries = useMemo(() => {
if (selectedEntry || (address && !addressError)) return addressBookEntries
if (selectedEntry || !input || (input && !addressError)) return addressBookEntries

return addressBookSearchEntries
}, [selectedEntry, addressBookEntries, addressBookSearchEntries, address, addressError])
}, [selectedEntry, addressBookEntries, addressBookSearchEntries, input, addressError])

const entryAvatars = useMemo(() => {
return entries?.reduce(
(acc, entry) => {
acc[entry.address] = makeBlockiesUrl(entry.address)
return acc
},
{} as Record<string, string>,
)
}, [entries])

const addressBookButtons = useMemo(() => {
if (entries?.length === 0)
return <Text translation={emptyMessage} size='xs' mx={2} color='text.subtle' />

return entries?.map(entry => {
const entryKey = toAccountId({ chainId: entry.chainId, account: entry.address })

return (
<AddressBookEntryButton
key={entryKey}
avatarUrl={entryAvatars?.[entry.address] ?? ''}
label={entry.label}
address={entry.address}
entryKey={entryKey}
onSelect={onSelectEntry}
onDelete={handleDeleteConfirm(entry)}
/>
)
})
}, [entries, entryAvatars, handleDeleteConfirm, onSelectEntry, emptyMessage])

return (
<Box>
Expand All @@ -184,26 +141,15 @@ export const AddressBook = ({
px={2}
>
<VStack spacing={3} align='stretch'>
{entries.length === 0 ? (
<Text translation={emptyMessage} size='xs' mx={2} color='text.subtle' />
) : (
entries.map(entry => (
<AddressBookEntryButton
key={entry.id}
entry={entry}
onSelect={onSelectEntry}
onDelete={handleDeleteConfirm(entry)}
/>
))
)}
{addressBookButtons}
</VStack>
</Box>
{selectedDeleteEntry && (
<ConfirmDelete
isOpen={isOpen}
onDelete={handleDelete(selectedDeleteEntry.id)}
onDelete={handleDelete(selectedDeleteEntry)}
onClose={onClose}
entryName={selectedDeleteEntry.name}
entryName={selectedDeleteEntry.label}
/>
)}
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Avatar, Box, Button, HStack, Icon, Text as CText, VStack } from '@chakra-ui/react'
import { memo, useCallback } from 'react'
import { FaTrash } from 'react-icons/fa'

import { MiddleEllipsis } from '@/components/MiddleEllipsis/MiddleEllipsis'

type AddressBookEntryButtonProps = {
avatarUrl: string
label: string
address: string
entryKey: string
onSelect: (address: string) => void
onDelete: (key: string) => void
}

const addressSx = {
_hover: {
background: 'background.surface.raised.base',
},
}

const deleteButtonSx = {
svg: {
width: '12px',
height: '12px',
},
}

const AddressBookEntryButtonComponent = ({
avatarUrl,
label,
address,
entryKey,
onSelect,
onDelete,
}: AddressBookEntryButtonProps) => {
const handleClick = useCallback(() => onSelect(address), [onSelect, address])
const handleDelete = useCallback(
(key: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
e.preventDefault()
onDelete(key)
},
[onDelete],
)

return (
<Box
cursor='pointer'
alignItems='center'
justifyContent='space-between'
display='flex'
overflow='hidden'
width='full'
>
<HStack
px={2}
py={1}
borderRadius='lg'
spacing={3}
align='center'
flex={1}
minWidth={0}
onClick={handleClick}
transition='all 0.2s'
sx={addressSx}
me={2}
>
<Avatar src={avatarUrl} size='sm' flexShrink={0} />
<VStack align='start' spacing={0} flex={1} minWidth={0}>
<CText fontSize='md' fontWeight='semibold' color='text.primary' lineHeight={1}>
{label}
</CText>
<MiddleEllipsis fontSize='sm' color='text.subtle' noOfLines={1} value={address} />
</VStack>
</HStack>
<Button size='sm' onClick={handleDelete(entryKey)} sx={deleteButtonSx} flexShrink={0}>
<Icon as={FaTrash} boxSize={4} />
</Button>
</Box>
)
}

// Memoize to prevent re-renders when props haven't changed
export const AddressBookEntryButton = memo(AddressBookEntryButtonComponent)
Loading