1+ import {
2+ Box ,
3+ Input ,
4+ Menu ,
5+ MenuButton ,
6+ MenuItem ,
7+ MenuList ,
8+ Text ,
9+ Flex ,
10+ VStack ,
11+ RadioGroup ,
12+ Radio ,
13+ Stack
14+ } from '@chakra-ui/react' ;
15+ import { useState , useMemo } from 'react' ;
16+ import { ChevronDownIcon } from '@chakra-ui/icons' ;
17+ import { useTypink , useBalances , formatBalance } from 'typink' ;
18+ import { shortenAddress } from '@/utils/string.ts' ;
19+ import { PolkadotApi } from '@dedot/chaintypes' ;
20+
21+ interface RecipientSelectorProps {
22+ value : string ;
23+ onChange : ( address : string ) => void ;
24+ isDisabled ?: boolean ;
25+ isInvalid ?: boolean ;
26+ }
27+
28+ // Simple address validation helper
29+ const isValidPolkadotAddress = ( address : string ) : boolean => {
30+ // Basic validation - should be 47-48 characters and start with '1' for Polkadot
31+ return / ^ [ 1 - 9 A - H J - N P - Z a - k m - z ] { 47 , 48 } $ / . test ( address . trim ( ) ) ;
32+ } ;
33+
34+ type SelectionMode = 'accounts' | 'custom' ;
35+
36+ export default function RecipientSelector ( {
37+ value,
38+ onChange,
39+ isDisabled = false ,
40+ isInvalid = false
41+ } : RecipientSelectorProps ) {
42+ const { accounts, connectedAccount, network } = useTypink < PolkadotApi > ( ) ;
43+
44+ // Get all accounts except the connected one
45+ const availableAccounts = useMemo ( ( ) => {
46+ return accounts . filter ( acc => acc . address !== connectedAccount ?. address ) ;
47+ } , [ accounts , connectedAccount ] ) ;
48+
49+ // Get balances for all available accounts
50+ const accountAddresses = useMemo ( ( ) => availableAccounts . map ( acc => acc . address ) , [ availableAccounts ] ) ;
51+ const balances = useBalances ( accountAddresses ) ;
52+
53+ // Find if current value matches an available account
54+ const selectedAccount = useMemo ( ( ) => {
55+ return availableAccounts . find ( acc => acc . address === value ) ;
56+ } , [ availableAccounts , value ] ) ;
57+
58+ // Initialize mode - default to 'accounts' if available accounts exist, otherwise 'custom'
59+ const [ mode , setMode ] = useState < SelectionMode > ( 'accounts' ) ;
60+
61+ const handleAccountSelect = ( account : typeof availableAccounts [ 0 ] ) => {
62+ onChange ( account . address ) ;
63+ } ;
64+
65+ const handleCustomInput = ( inputValue : string ) => {
66+ onChange ( inputValue ) ;
67+ } ;
68+
69+ const handleModeChange = ( newMode : SelectionMode ) => {
70+ setMode ( newMode ) ;
71+ if ( newMode === 'accounts' && availableAccounts . length > 0 ) {
72+ // If switching to accounts mode and we have a selected account, keep it
73+ // Otherwise, select the first available account
74+ if ( ! selectedAccount ) {
75+ onChange ( availableAccounts [ 0 ] . address ) ;
76+ }
77+ } else if ( newMode === 'custom' ) {
78+ // Clear the value when switching to custom mode unless it's already a custom address
79+ if ( selectedAccount ) {
80+ onChange ( '' ) ;
81+ }
82+ }
83+ } ;
84+
85+ return (
86+ < VStack align = 'stretch' spacing = { 3 } >
87+ { /* Radio buttons for mode selection */ }
88+ { availableAccounts . length > 0 && (
89+ < RadioGroup value = { mode } onChange = { handleModeChange } >
90+ < Stack direction = 'row' >
91+ < Radio value = 'accounts' size = 'sm' isDisabled = { isDisabled } >
92+ Select from connected accounts
93+ </ Radio >
94+ < Radio value = 'custom' size = 'sm' isDisabled = { isDisabled } >
95+ Enter custom address
96+ </ Radio >
97+ </ Stack >
98+ </ RadioGroup >
99+ ) }
100+
101+ { /* Conditional rendering based on mode */ }
102+ { mode === 'custom' || availableAccounts . length === 0 ? (
103+ < Box >
104+ < Input
105+ placeholder = { availableAccounts . length === 0
106+ ? 'Enter recipient address (no other accounts available)'
107+ : 'Enter recipient address (1abc...)'
108+ }
109+ value = { value }
110+ onChange = { ( e ) => handleCustomInput ( e . target . value ) }
111+ isDisabled = { isDisabled }
112+ isInvalid = { isInvalid || ( value . length > 0 && ! isValidPolkadotAddress ( value ) ) }
113+ />
114+ { value . length > 0 && ! isValidPolkadotAddress ( value ) && (
115+ < Text fontSize = 'xs' color = 'red.500' mt = { 1 } >
116+ Please enter a valid Polkadot address
117+ </ Text >
118+ ) }
119+ </ Box >
120+ ) : (
121+ < Menu >
122+ < MenuButton
123+ as = { Box }
124+ cursor = 'pointer'
125+ p = { 3 }
126+ border = '1px solid'
127+ borderColor = { isInvalid ? 'red.500' : 'gray.200' }
128+ borderRadius = 'md'
129+ _hover = { { borderColor : 'gray.300' } }
130+ _disabled = { { opacity : 0.6 , cursor : 'not-allowed' } }
131+ aria-disabled = { isDisabled }
132+ >
133+ { selectedAccount ? (
134+ < Flex align = 'center' justify = 'space-between' width = '100%' >
135+ < Box >
136+ < Text fontSize = 'sm' fontWeight = 'medium' >
137+ { selectedAccount . name }
138+ </ Text >
139+ < Text fontSize = 'xs' color = 'gray.600' >
140+ { shortenAddress ( selectedAccount . address ) }
141+ </ Text >
142+ </ Box >
143+ < Flex align = 'center' gap = { 2 } >
144+ < Text fontSize = 'xs' color = 'gray.500' >
145+ { formatBalance ( balances [ selectedAccount . address ] ?. free , network ) }
146+ </ Text >
147+ < ChevronDownIcon />
148+ </ Flex >
149+ </ Flex >
150+ ) : (
151+ < Flex align = 'center' justify = 'space-between' width = '100%' >
152+ < Text color = 'gray.500' > Select recipient account</ Text >
153+ < ChevronDownIcon />
154+ </ Flex >
155+ ) }
156+ </ MenuButton >
157+
158+ < MenuList maxH = '300px' overflowY = 'auto' >
159+ { availableAccounts . map ( ( account ) => (
160+ < MenuItem
161+ key = { account . address }
162+ onClick = { ( ) => handleAccountSelect ( account ) }
163+ bg = { selectedAccount ?. address === account . address ? 'gray.100' : undefined }
164+ isDisabled = { isDisabled }
165+ >
166+ < Flex direction = 'column' width = '100%' >
167+ < Flex justify = 'space-between' align = 'center' width = '100%' >
168+ < Text fontWeight = 'medium' fontSize = 'sm' >
169+ { account . name }
170+ </ Text >
171+ < Text fontSize = 'xs' color = 'gray.500' >
172+ { formatBalance ( balances [ account . address ] ?. free , network ) }
173+ </ Text >
174+ </ Flex >
175+ < Text fontSize = 'xs' color = 'gray.600' >
176+ { shortenAddress ( account . address ) }
177+ </ Text >
178+ </ Flex >
179+ </ MenuItem >
180+ ) ) }
181+ </ MenuList >
182+ </ Menu >
183+ ) }
184+ </ VStack >
185+ ) ;
186+ }
0 commit comments