11import React , { useState } from 'react' ;
2- import { View , Text , StyleSheet , FlatList , Alert } from 'react-native' ;
2+ import { View , Text , StyleSheet , FlatList , Alert , TouchableOpacity } from 'react-native' ;
33import { Button } from '../src/components/Button' ;
44import { Input } from '../src/components/Input' ;
55import { COLORS , SIZES , RADIUS } from '../src/constants/theme' ;
66import { useAppStore , Contact } from '../src/store/appStore' ;
77import { validateAddress } from '../src/utils/validation' ;
8- import { Trash2 , User } from 'lucide-react-native' ;
8+ import { findDuplicate , duplicateMessage , normalizeAddress } from '../src/utils/address' ;
9+ import { Trash2 , User , AlertTriangle , Pencil } from 'lucide-react-native' ;
910
1011export default function ContactsScreen ( ) {
11- const { contacts, addContact, removeContact, findContactByPublicKey } = useAppStore ( ) ;
12+ const { contacts, addContact, removeContact, updateContact } = useAppStore ( ) ;
1213 const [ name , setName ] = useState ( '' ) ;
1314 const [ publicKey , setPublicKey ] = useState ( '' ) ;
1415 const [ nameError , setNameError ] = useState < string | undefined > ( ) ;
1516 const [ keyError , setKeyError ] = useState < string | undefined > ( ) ;
1617 const [ duplicateError , setDuplicateError ] = useState < string | undefined > ( ) ;
18+ const [ foundDuplicate , setFoundDuplicate ] = useState < Contact | null > ( null ) ;
1719 const [ isAdding , setIsAdding ] = useState ( false ) ;
1820
1921 const handleNameChange = ( value : string ) => {
@@ -25,6 +27,7 @@ export default function ContactsScreen() {
2527 setPublicKey ( value ) ;
2628 setKeyError ( value . trim ( ) ? validateAddress ( value ) ?? undefined : undefined ) ;
2729 if ( duplicateError ) setDuplicateError ( undefined ) ;
30+ if ( foundDuplicate ) setFoundDuplicate ( null ) ;
2831 } ;
2932
3033 const handleAdd = async ( ) => {
@@ -33,32 +36,60 @@ export default function ContactsScreen() {
3336 setNameError ( currentNameError ) ;
3437 setKeyError ( currentKeyError ) ;
3538 setDuplicateError ( undefined ) ;
39+ setFoundDuplicate ( null ) ;
3640
3741 if ( currentNameError || currentKeyError ) {
3842 return ;
3943 }
4044
41- const existing = findContactByPublicKey ( publicKey ) ;
45+ // Duplicate detection via utility (consistent normalization)
46+ const existing = findDuplicate ( publicKey , contacts ) ;
4247 if ( existing ) {
43- setDuplicateError (
44- `This address is already saved as "${ existing . name } ". You cannot add duplicate addresses.` ,
45- ) ;
48+ setFoundDuplicate ( existing ) ;
49+ setDuplicateError ( duplicateMessage ( existing . name ) ) ;
4650 return ;
4751 }
4852
4953 const newContact : Contact = {
5054 id : Date . now ( ) . toString ( ) ,
5155 name : name . trim ( ) ,
52- publicKey : publicKey . trim ( ) ,
56+ publicKey : normalizeAddress ( publicKey ) ,
5357 } ;
5458
55- await addContact ( newContact ) ;
59+ // Store-level duplicate check (defense in depth)
60+ const result = await addContact ( newContact ) ;
61+ if ( ! result . success ) {
62+ setDuplicateError ( duplicateMessage ( result . duplicateName ?? '' ) ) ;
63+ return ;
64+ }
65+
66+ resetForm ( ) ;
67+ setIsAdding ( false ) ;
68+ } ;
69+
70+ const handleUpdateExisting = async ( ) => {
71+ if ( ! foundDuplicate ) return ;
72+ const newName = name . trim ( ) || foundDuplicate . name ;
73+ try {
74+ await updateContact ( foundDuplicate . id , newName ) ;
75+ Alert . alert (
76+ 'Updated' ,
77+ `Contact "${ foundDuplicate . name } " has been updated to "${ newName } ".` ,
78+ ) ;
79+ resetForm ( ) ;
80+ setIsAdding ( false ) ;
81+ } catch ( e : any ) {
82+ Alert . alert ( 'Error' , e . message || 'Failed to update contact.' ) ;
83+ }
84+ } ;
85+
86+ const resetForm = ( ) => {
5687 setName ( '' ) ;
5788 setPublicKey ( '' ) ;
5889 setNameError ( undefined ) ;
5990 setKeyError ( undefined ) ;
6091 setDuplicateError ( undefined ) ;
61- setIsAdding ( false ) ;
92+ setFoundDuplicate ( null ) ;
6293 } ;
6394
6495 const handleRemove = ( id : string ) => {
@@ -82,12 +113,33 @@ export default function ContactsScreen() {
82113 error = { keyError }
83114 autoCapitalize = "none"
84115 />
85- { duplicateError && (
116+ { foundDuplicate && (
117+ < View style = { styles . duplicateBanner } >
118+ < View style = { styles . duplicateBannerHeader } >
119+ < AlertTriangle color = { COLORS . warning } size = { 18 } />
120+ < Text style = { styles . duplicateBannerTitle } > Duplicate Address</ Text >
121+ </ View >
122+ < Text style = { styles . duplicateBannerText } >
123+ This address is already saved as "{ foundDuplicate . name } ".
124+ </ Text >
125+ < Text style = { styles . duplicateBannerHint } >
126+ You can update the existing entry's name below, or cancel to keep it unchanged.
127+ </ Text >
128+ < TouchableOpacity style = { styles . updateButton } onPress = { handleUpdateExisting } >
129+ < Pencil color = { COLORS . primary } size = { 16 } />
130+ < Text style = { styles . updateButtonText } >
131+ Update "{ foundDuplicate . name } " to "{ name . trim ( ) || foundDuplicate . name } "
132+ </ Text >
133+ </ TouchableOpacity >
134+ </ View >
135+ ) }
136+
137+ { duplicateError && ! foundDuplicate && (
86138 < Text style = { styles . duplicateWarning } > { duplicateError } </ Text >
87139 ) }
88140 < View style = { styles . actions } >
89141 < Button title = "Save Contact" onPress = { handleAdd } style = { styles . actionBtn } />
90- < Button title = "Cancel" variant = "outline" onPress = { ( ) => setIsAdding ( false ) } style = { styles . actionBtn } />
142+ < Button title = "Cancel" variant = "outline" onPress = { ( ) => { resetForm ( ) ; setIsAdding ( false ) ; } } style = { styles . actionBtn } />
91143 </ View >
92144 </ View >
93145 ) : (
@@ -193,5 +245,53 @@ const styles = StyleSheet.create({
193245 marginTop : SIZES . xs ,
194246 marginLeft : SIZES . xs ,
195247 marginBottom : SIZES . sm ,
196- }
248+ } ,
249+ duplicateBanner : {
250+ backgroundColor : 'rgba(255, 196, 0, 0.08)' ,
251+ borderRadius : RADIUS . md ,
252+ padding : SIZES . md ,
253+ marginTop : SIZES . sm ,
254+ marginBottom : SIZES . sm ,
255+ borderWidth : 1 ,
256+ borderColor : 'rgba(255, 196, 0, 0.25)' ,
257+ } ,
258+ duplicateBannerHeader : {
259+ flexDirection : 'row' ,
260+ alignItems : 'center' ,
261+ gap : SIZES . sm ,
262+ marginBottom : SIZES . xs ,
263+ } ,
264+ duplicateBannerTitle : {
265+ color : COLORS . warning ,
266+ fontSize : 14 ,
267+ fontWeight : '700' ,
268+ } ,
269+ duplicateBannerText : {
270+ color : COLORS . warning ,
271+ fontSize : 13 ,
272+ marginBottom : SIZES . xs ,
273+ lineHeight : 18 ,
274+ } ,
275+ duplicateBannerHint : {
276+ color : COLORS . textMuted ,
277+ fontSize : 12 ,
278+ marginBottom : SIZES . md ,
279+ lineHeight : 17 ,
280+ } ,
281+ updateButton : {
282+ flexDirection : 'row' ,
283+ alignItems : 'center' ,
284+ backgroundColor : 'rgba(0, 229, 255, 0.1)' ,
285+ borderRadius : RADIUS . sm ,
286+ padding : SIZES . sm + 2 ,
287+ gap : SIZES . sm ,
288+ borderWidth : 1 ,
289+ borderColor : 'rgba(0, 229, 255, 0.2)' ,
290+ } ,
291+ updateButtonText : {
292+ color : COLORS . primary ,
293+ fontSize : 13 ,
294+ fontWeight : '600' ,
295+ flex : 1 ,
296+ } ,
197297} ) ;
0 commit comments