diff --git a/components/designSystem/Curtains/AddressBookCurtain.tsx b/components/designSystem/Curtains/AddressBookCurtain.tsx index 301692b..ef4a3df 100644 --- a/components/designSystem/Curtains/AddressBookCurtain.tsx +++ b/components/designSystem/Curtains/AddressBookCurtain.tsx @@ -19,7 +19,7 @@ import {AvatarWrapper} from '../Avatar'; import {NetworkDropdownSelector} from '../NetworkSelector/Dropdown'; import {SmolAddressInputSimple} from '../SmolAddressInput.simple'; -import type {TAddressBookEntryReducer} from 'pages/apps/address-book'; +import type {TAddressBookEntryReducer} from 'contexts/useAddressBookCurtain'; import type {Dispatch, ReactElement, SetStateAction} from 'react'; import type {TInputAddressLike} from '@utils/tools.address'; @@ -122,7 +122,7 @@ function NameInput(props: { useEffect(() => { const entry = getCachedEntry({label: selectedEntry.label}); const currentCustomValidity = inputRef.current?.validationMessage; - if (entry !== undefined && entry.id !== selectedEntry.id) { + if (entry !== undefined && entry.id !== selectedEntry.id && !entry.isHidden) { inputRef.current?.setCustomValidity('This name is already used in your address book'); props.onRefresh?.(); } else if (currentCustomValidity !== '') { @@ -220,7 +220,7 @@ function AddressInput(props: { const entry = getCachedEntry({address: props.addressLike.address}); const currentCustomValidity = inputRef.current?.validationMessage; - if (entry !== undefined && entry.id !== props.selectedEntry.id) { + if (entry !== undefined && entry.id !== props.selectedEntry.id && !entry.isHidden) { inputRef.current?.setCustomValidity('This address is already in your address book'); onChange(); } else if (currentCustomValidity !== '') { @@ -316,10 +316,10 @@ export function AddressBookCurtain(props: { }); } if (props.selectedEntry.id === undefined) { - updateEntry({...currentEntry, address: addressLike.address}); + updateEntry({...currentEntry, address: addressLike.address, isHidden: false}); props.onOpenChange({isOpen: false, isEditing: false}); } else { - updateEntry({...currentEntry, address: addressLike.address}); + updateEntry({...currentEntry, address: addressLike.address, isHidden: false}); set_isEditMode(false); props.onOpenChange({isOpen: true, isEditing: false}); } diff --git a/components/designSystem/Layout.tsx b/components/designSystem/Layout.tsx index 0b09bcc..c4640a2 100644 --- a/components/designSystem/Layout.tsx +++ b/components/designSystem/Layout.tsx @@ -1,5 +1,6 @@ import {type ReactElement, type ReactNode} from 'react'; import {WithAddressBook} from 'contexts/useAddressBook'; +import {WithAddressBookCurtain} from 'contexts/useAddressBookCurtain'; import {AnimatePresence, motion} from 'framer-motion'; import {cl} from '@builtbymom/web3/utils'; import {IconQuestionMark} from '@icons/IconQuestionMark'; @@ -89,21 +90,23 @@ export default function Layout(props: AppProps): ReactElement { initial={'initial'} className={'relative mb-10 min-h-app w-full overflow-x-hidden rounded-lg bg-neutral-0'}> - - - {getLayout(, router)} - - + + + + {getLayout(, router)} + + + diff --git a/components/designSystem/SmolAddressInput.tsx b/components/designSystem/SmolAddressInput.tsx index fb81738..4e634b1 100644 --- a/components/designSystem/SmolAddressInput.tsx +++ b/components/designSystem/SmolAddressInput.tsx @@ -39,7 +39,7 @@ export function useValidateAddressInput(): { ** Check if the input is an address from the address book **********************************************************/ const fromAddressBook = await getEntry({label: input, address: toAddress(input)}); - if (fromAddressBook) { + if (fromAddressBook && !fromAddressBook.isHidden) { if (signal?.aborted) { console.log('aborted'); throw new Error('Aborted!'); @@ -68,8 +68,7 @@ export function useValidateAddressInput(): { if (isAddress(address)) { set_isCheckingValidity(false); const fromAddressBook = await getEntry({label: input, address: toAddress(address)}); - if (fromAddressBook) { - console.log(3); + if (fromAddressBook && !fromAddressBook.isHidden) { return { address: toAddress(fromAddressBook.address), label: fromAddressBook.label || fromAddressBook.ens || input, diff --git a/components/sections/Send/SendStatus.tsx b/components/sections/Send/SendStatus.tsx index a9493f9..8c5288d 100644 --- a/components/sections/Send/SendStatus.tsx +++ b/components/sections/Send/SendStatus.tsx @@ -1,6 +1,7 @@ import {useState} from 'react'; import Link from 'next/link'; import {useAddressBook} from 'contexts/useAddressBook'; +import {useAddressBookCurtain} from 'contexts/useAddressBookCurtain'; import {useAsyncTrigger} from '@builtbymom/web3/hooks/useAsyncTrigger'; import {useChainID} from '@builtbymom/web3/hooks/useChainID'; import {isEthAddress} from '@builtbymom/web3/utils'; @@ -10,9 +11,34 @@ import {Warning} from '@common/Primitives/Warning'; import {useSendFlow} from './useSendFlow'; -import type {ReactElement} from 'react'; +import type {ReactElement, ReactNode} from 'react'; import type {TWarningType} from '@common/Primitives/Warning'; +function TriggerAddressBookButton({children}: {children: ReactNode}): ReactElement { + const {set_curtainStatus, dispatchConfiguration} = useAddressBookCurtain(); + const {configuration} = useSendFlow(); + + return ( + + ); +} + export function SendStatus({isReceiverERC20}: {isReceiverERC20: boolean}): ReactElement | null { const {configuration} = useSendFlow(); const {safeChainID} = useChainID(); @@ -63,15 +89,31 @@ export function SendStatus({isReceiverERC20}: {isReceiverERC20: boolean}): React }); } - if (configuration.receiver.address && !fromAddressBook) { + if ( + configuration.receiver.address && + (!fromAddressBook || (fromAddressBook?.numberOfInteractions === 0 && fromAddressBook.isHidden)) + ) { return set_status({ - message: 'This is the first time you interact with this address, please be careful', + message: ( + <> + {'This is the first time you interact with this address, please be careful.'} + {'Wanna add it to Address Book?'} + + ), type: 'warning' }); } if (configuration.receiver.address && fromAddressBook?.isHidden) { - return set_status({message: 'This address isn’t in your address book. Wanna add it?', type: 'warning'}); + return set_status({ + message: ( + <> + {'This address isn’t in your address book.'}{' '} + {'Wanna add it?'} + + ), + type: 'warning' + }); } if (configuration.receiver.address && !fromAddressBook?.chains.includes(safeChainID)) { diff --git a/contexts/useAddressBook.tsx b/contexts/useAddressBook.tsx index fad8e41..16f381a 100755 --- a/contexts/useAddressBook.tsx +++ b/contexts/useAddressBook.tsx @@ -26,7 +26,7 @@ export type TAddressBookEntry = { tags?: string[]; // List of tags associated with the address. }; export type TSelectCallback = (item: TAddressBookEntry) => void; -export type TAddressBookCurtainProps = { +export type TAddressBookProps = { shouldOpenCurtain: boolean; listEntries: () => Promise; listCachedEntries: () => TAddressBookEntry[]; @@ -39,7 +39,7 @@ export type TAddressBookCurtainProps = { onOpenCurtain: (callbackFn: TSelectCallback) => void; onCloseCurtain: () => void; }; -const defaultProps: TAddressBookCurtainProps = { +const defaultProps: TAddressBookProps = { shouldOpenCurtain: false, listEntries: async (): Promise => [], listCachedEntries: (): TAddressBookEntry[] => [], @@ -78,13 +78,13 @@ const addressBookIDBConfig: IndexedDBConfig = { ] }; -const AddressBookContext = createContext(defaultProps); +const AddressBookContext = createContext(defaultProps); export const WithAddressBook = ({children}: {children: React.ReactElement}): React.ReactElement => { const [shouldOpenCurtain, set_shouldOpenCurtain] = useState(false); const [cachedEntries, set_cachedEntries] = useState([]); const [entryNonce, set_entryNonce] = useState(0); const [currentCallbackFunction, set_currentCallbackFunction] = useState(undefined); - const {add, getAll, getOneByKey, deleteByID, update} = useIndexedDBStore('address-book'); + const {add, getAll, getOneByKey, update} = useIndexedDBStore('address-book'); const {safeChainID} = useChainID(); useMountEffect(async () => setupIndexedDB(addressBookIDBConfig)); @@ -246,14 +246,14 @@ export const WithAddressBook = ({children}: {children: React.ReactElement}): Rea try { const existingEntry = await getEntry({address: address}); if (existingEntry) { - deleteByID(existingEntry.id); + update({...existingEntry, isHidden: true}); set_entryNonce(nonce => nonce + 1); } } catch { // Do nothing } }, - [deleteByID, getEntry] + [getEntry, update] ); /************************************************************************** @@ -295,7 +295,7 @@ export const WithAddressBook = ({children}: {children: React.ReactElement}): Rea * Context value that is passed to all children of this component. *************************************************************************/ const contextValue = useMemo( - (): TAddressBookCurtainProps => ({ + (): TAddressBookProps => ({ shouldOpenCurtain, listEntries, listCachedEntries, @@ -336,4 +336,4 @@ export const WithAddressBook = ({children}: {children: React.ReactElement}): Rea ); }; -export const useAddressBook = (): TAddressBookCurtainProps => useContext(AddressBookContext); +export const useAddressBook = (): TAddressBookProps => useContext(AddressBookContext); diff --git a/contexts/useAddressBookCurtain.tsx b/contexts/useAddressBookCurtain.tsx new file mode 100644 index 0000000..fc59e77 --- /dev/null +++ b/contexts/useAddressBookCurtain.tsx @@ -0,0 +1,110 @@ +import {createContext, type ReactElement, useContext, useReducer, useState} from 'react'; +import {AddressBookCurtain} from 'components/designSystem/Curtains/AddressBookCurtain'; +import {toAddress} from '@builtbymom/web3/utils'; + +import {useAddressBook} from './useAddressBook'; + +import type {TAddress} from '@builtbymom/web3/types'; +import type {TAddressBookEntry} from './useAddressBook'; + +export type TAddressBookEntryReducer = + | {type: 'SET_SELECTED_ENTRY'; payload: TAddressBookEntry} + | {type: 'SET_ADDRESS'; payload: TAddress | undefined} + | {type: 'SET_LABEL'; payload: string} + | {type: 'SET_CHAINS'; payload: number[]} + | {type: 'SET_IS_FAVORITE'; payload: boolean}; + +type TCurtainStatus = {isOpen: boolean; isEditing: boolean; label?: string}; + +type TAddressBookCurtainProps = { + selectedEntry: TAddressBookEntry | undefined; + dispatchConfiguration: React.Dispatch; + curtainStatus: TCurtainStatus; + set_curtainStatus: React.Dispatch>; +}; + +const deafultCurtainStatus = { + isOpen: false, + isEditing: false +}; + +const defaultProps: TAddressBookCurtainProps = { + selectedEntry: undefined, + dispatchConfiguration: (): void => undefined, + curtainStatus: deafultCurtainStatus, + set_curtainStatus: (): void => undefined +}; +const AddressBookCurtainContext = createContext(defaultProps); + +export const WithAddressBookCurtain = ({children}: {children: ReactElement}): ReactElement => { + const {updateEntry} = useAddressBook(); + const [curtainStatus, set_curtainStatus] = useState(deafultCurtainStatus); + + const entryReducer = (state: TAddressBookEntry, action: TAddressBookEntryReducer): TAddressBookEntry => { + switch (action.type) { + case 'SET_SELECTED_ENTRY': + return action.payload; + case 'SET_ADDRESS': + return {...state, address: toAddress(action.payload)}; + case 'SET_LABEL': + return {...state, label: action.payload}; + case 'SET_CHAINS': + return {...state, chains: action.payload}; + case 'SET_IS_FAVORITE': + updateEntry({...state, isFavorite: action.payload}); + return {...state, isFavorite: action.payload}; + } + }; + + const [selectedEntry, dispatch] = useReducer(entryReducer, { + address: undefined, + label: '', + slugifiedLabel: '', + chains: [], + isFavorite: false + }); + + const contextValue = { + selectedEntry, + dispatchConfiguration: dispatch, + curtainStatus, + set_curtainStatus + }; + + return ( + + {children} + + { + set_curtainStatus(status); + if (!status.isOpen) { + dispatch({ + type: 'SET_SELECTED_ENTRY', + payload: { + address: undefined, + label: '', + slugifiedLabel: '', + chains: [], + isFavorite: false + } + }); + } + }} + /> + + ); +}; + +export const useAddressBookCurtain = (): TAddressBookCurtainProps => { + const ctx = useContext(AddressBookCurtainContext); + if (!ctx) { + throw new Error('AddressBookCurtainContext not found'); + } + return ctx; +}; diff --git a/pages/apps/address-book.tsx b/pages/apps/address-book.tsx index f7fbb95..b1a964b 100644 --- a/pages/apps/address-book.tsx +++ b/pages/apps/address-book.tsx @@ -1,8 +1,8 @@ -import {Fragment, useCallback, useMemo, useReducer, useState} from 'react'; +import {Fragment, useCallback, useMemo, useState} from 'react'; import {AddressBookEntry} from 'components/designSystem/AddressBookEntry'; -import {AddressBookCurtain} from 'components/designSystem/Curtains/AddressBookCurtain'; import {TextInput} from 'components/Primitives/TextInput'; import {useAddressBook} from 'contexts/useAddressBook'; +import {useAddressBookCurtain} from 'contexts/useAddressBookCurtain'; import Papa from 'papaparse'; import {LayoutGroup, motion} from 'framer-motion'; import {cl, toAddress} from '@builtbymom/web3/utils'; @@ -15,13 +15,6 @@ import type {TAddressBookEntry} from 'contexts/useAddressBook'; import type {ChangeEvent, ReactElement} from 'react'; import type {TAddress} from '@builtbymom/web3/types'; -export type TAddressBookEntryReducer = - | {type: 'SET_SELECTED_ENTRY'; payload: TAddressBookEntry} - | {type: 'SET_ADDRESS'; payload: TAddress | undefined} - | {type: 'SET_LABEL'; payload: string} - | {type: 'SET_CHAINS'; payload: number[]} - | {type: 'SET_IS_FAVORITE'; payload: boolean}; - function AddContactButton(props: {onOpenCurtain: VoidFunction; label?: string}): ReactElement { return (