(Bugfix) - #1246 Addressbook entries removed when reloading page (#1300)

* Fix addressbook types
Restructure addressbook store type

* Add more safe types

* Fix imports

* Removes .toJS() usage

* Fix condition for saving addressBook

* Types & remove send button from addressbook if user not an owner

* Add types for addressBook actions
Remove unused saveAndUpdateAddressBook action

* Refactor addressBook: make it global and removes immutableJS
Add types
Removes unused addAddressBook action

* Remove todo

* Fix edit and remove entries style when user is not owner

* Adds and updates safe name in addressBook

* Adds checkIfOwnerWasDeletedFromAddressBook
Let the user remove owners users without adding them again each time the safe loads

* Simplify loadAddressBookFromStorage

* Fix compilation errors included in pr #1301

* Uses sameAddress function

* Add migration function for old stored address books

* Update tests

* Replaces shouldAvoidUpdatesNotifications with addAddressBookEntryOptions on addAddressBookEntry

* Update tests

* Unify return on getOwnersWithNameFromAddressBook

* Reword shouldAvoidUpdatesNotifications

* Replaces adbk with addressBook

* Fix condition

* Fix typos

* Fix typo

Co-authored-by: Daniel Sanchez <daniel.sanchez@gnosis.pm>
This commit is contained in:
Agustin Pane 2020-09-22 09:31:07 -03:00 committed by GitHub
parent fffabf02ce
commit 59dc1f711c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 460 additions and 350 deletions

View File

@ -1,15 +1,17 @@
import { Record, RecordOf } from 'immutable'
export interface AddressBookEntryProps {
export type AddressBookEntry = {
address: string
name: string
isOwner: boolean
}
export const makeAddressBookEntry = Record<AddressBookEntryProps>({
address: '',
name: '',
isOwner: false,
export const makeAddressBookEntry = ({
address = '',
name = '',
}: {
address: string
name?: string
}): AddressBookEntry => ({
address,
name,
})
export type AddressBookEntryRecord = RecordOf<AddressBookEntryProps>
export type AddressBookState = AddressBookEntry[]

View File

@ -1,8 +0,0 @@
import { createAction } from 'redux-actions'
export const ADD_ADDRESS_BOOK = 'ADD_ADDRESS_BOOK'
export const addAddressBook = createAction(ADD_ADDRESS_BOOK, (addressBook, safeAddress) => ({
addressBook,
safeAddress,
}))

View File

@ -1,7 +1,22 @@
import { createAction } from 'redux-actions'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
export const ADD_ENTRY = 'ADD_ENTRY'
export const addAddressBookEntry = createAction(ADD_ENTRY, (entry) => ({
type addAddressBookEntryOptions = {
notifyEntryUpdate: boolean
}
export const addAddressBookEntry = createAction(
ADD_ENTRY,
(entry: AddressBookEntry, options: addAddressBookEntryOptions) => {
let notifyEntryUpdate = true
if (options) {
notifyEntryUpdate = options.notifyEntryUpdate
}
return {
entry,
}))
shouldAvoidUpdatesNotifications: !notifyEntryUpdate,
}
},
)

View File

@ -1,8 +1,8 @@
import { createAction } from 'redux-actions'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
export const ADD_OR_UPDATE_ENTRY = 'ADD_OR_UPDATE_ENTRY'
export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entryAddress, entry) => ({
entryAddress,
export const addOrUpdateAddressBookEntry = createAction(ADD_OR_UPDATE_ENTRY, (entry: AddressBookEntry) => ({
entry,
}))

View File

@ -1,7 +1,8 @@
import { createAction } from 'redux-actions'
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
export const LOAD_ADDRESS_BOOK = 'LOAD_ADDRESS_BOOK'
export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook) => ({
export const loadAddressBook = createAction(LOAD_ADDRESS_BOOK, (addressBook: AddressBookState) => ({
addressBook,
}))

View File

@ -1,29 +1,17 @@
import { List } from 'immutable'
import { loadAddressBook } from 'src/logic/addressBook/store/actions/loadAddressBook'
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
import { getAddressBookFromStorage } from 'src/logic/addressBook/utils'
import { safesListSelector } from 'src/logic/safe/store/selectors'
import { Dispatch } from 'redux'
const loadAddressBookFromStorage = () => async (dispatch, getState) => {
const loadAddressBookFromStorage = () => async (dispatch: Dispatch): Promise<void> => {
try {
const state = getState()
let storedAdBk = await getAddressBookFromStorage()
if (!storedAdBk) {
storedAdBk = []
}
let addressBook = buildAddressBook(storedAdBk)
// Fetch all the current safes, in case that we don't have a safe on the adbk, we add it
const safes = safesListSelector(state)
const adbkEntries = addressBook.keySeq().toArray()
safes.forEach((safe) => {
const { address } = safe
const found = adbkEntries.includes(address)
if (!found) {
addressBook = addressBook.set(address, List([]))
}
})
const addressBook = buildAddressBook(storedAdBk)
dispatch(loadAddressBook(addressBook))
} catch (err) {
// eslint-disable-next-line

View File

@ -2,6 +2,6 @@ import { createAction } from 'redux-actions'
export const REMOVE_ENTRY = 'REMOVE_ENTRY'
export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress) => ({
export const removeAddressBookEntry = createAction(REMOVE_ENTRY, (entryAddress: string) => ({
entryAddress,
}))

View File

@ -1,15 +0,0 @@
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { saveAddressBook } from 'src/logic/addressBook/utils'
const saveAndUpdateAddressBook = (addressBook) => async (dispatch) => {
try {
dispatch(updateAddressBookEntry(makeAddressBookEntry(addressBook)))
await saveAddressBook(addressBook)
} catch (err) {
// eslint-disable-next-line
console.error('Error while loading active tokens from storage:', err)
}
}
export default saveAndUpdateAddressBook

View File

@ -1,7 +1,8 @@
import { createAction } from 'redux-actions'
import { AddressBookEntry } from 'src/logic/addressBook/model/addressBook'
export const UPDATE_ENTRY = 'UPDATE_ENTRY'
export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry) => ({
export const updateAddressBookEntry = createAction(UPDATE_ENTRY, (entry: AddressBookEntry) => ({
entry,
}))

View File

@ -2,7 +2,7 @@ import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEnt
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { addressBookMapSelector } from 'src/logic/addressBook/store/selectors'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { saveAddressBook } from 'src/logic/addressBook/utils'
import { enhanceSnackbarForAction, getNotificationsFromTxType } from 'src/logic/notifications'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
@ -16,15 +16,15 @@ const addressBookMiddleware = (store) => (next) => async (action) => {
if (watchedActions.includes(action.type)) {
const state = store.getState()
const { dispatch } = store
const addressBook = addressBookMapSelector(state)
if (addressBook) {
const addressBook = addressBookSelector(state)
if (addressBook.length) {
await saveAddressBook(addressBook)
}
switch (action.type) {
case ADD_ENTRY: {
const { isOwner } = action.payload.entry
if (!isOwner) {
const { shouldAvoidUpdatesNotifications } = action.payload
if (!shouldAvoidUpdatesNotifications) {
const notification = getNotificationsFromTxType(TX_NOTIFICATION_TYPES.ADDRESSBOOK_NEW_ENTRY)
dispatch(enqueueSnackbar(enhanceSnackbarForAction(notification.afterExecution.noMoreConfirmationsNeeded)))
}

View File

@ -1,134 +1,71 @@
import { List, Map } from 'immutable'
import { handleActions } from 'redux-actions'
import {
AddressBookEntryRecord,
AddressBookEntryProps,
makeAddressBookEntry,
} from 'src/logic/addressBook/model/addressBook'
import { ADD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/addAddressBook'
import { AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { ADD_ENTRY } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
import { ADD_OR_UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { LOAD_ADDRESS_BOOK } from 'src/logic/addressBook/store/actions/loadAddressBook'
import { REMOVE_ENTRY } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
import { UPDATE_ENTRY } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { checksumAddress } from 'src/utils/checksumAddress'
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
export const ADDRESS_BOOK_REDUCER_ID = 'addressBook'
export type AddressBookCollection = List<AddressBookEntryRecord>
export type AddressBookState = Map<string, Map<string, AddressBookCollection>>
export const buildAddressBook = (storedAdbk: AddressBookEntryProps[]): Map<string, AddressBookCollection> => {
let addressBookBuilt: Map<string, AddressBookCollection> = Map([])
Object.entries(storedAdbk).forEach((adbkProps: any) => {
const safeAddress = checksumAddress(adbkProps[0])
const adbkRecords: AddressBookEntryRecord[] = adbkProps[1].map(makeAddressBookEntry)
const adbkSafeEntries = List(adbkRecords)
addressBookBuilt = addressBookBuilt.set(safeAddress, adbkSafeEntries)
export const buildAddressBook = (storedAdbk: AddressBookState): AddressBookState => {
return storedAdbk.map((addressBookEntry) => {
const { address, name } = addressBookEntry
return makeAddressBookEntry({ address: checksumAddress(address), name })
})
return addressBookBuilt
}
export default handleActions(
{
[LOAD_ADDRESS_BOOK]: (state, action) => {
const { addressBook } = action.payload
return state.set('addressBook', addressBook)
},
[ADD_ADDRESS_BOOK]: (state, action) => {
const { addressBook, safeAddress } = action.payload
// Adds the address book if it does not exists
const found = state.getIn(['addressBook', safeAddress])
if (!found) {
return state.setIn(['addressBook', safeAddress], addressBook)
}
return state
return addressBook
},
[ADD_ENTRY]: (state, action) => {
const { entry } = action.payload
// Adds the entry to all the safes (if it does not already exists)
const newState = state.withMutations((map) => {
const adbkMap = map.get('addressBook')
const entryFound = state.find((oldEntry) => oldEntry.address === entry.address)
if (adbkMap) {
adbkMap.keySeq().forEach((safeAddress) => {
const safeAddressBook = state.getIn(['addressBook', safeAddress])
if (safeAddressBook) {
const adbkAddressList = getAddressesListFromSafeAddressBook(safeAddressBook)
const found = adbkAddressList.includes(entry.address)
if (!found) {
const updatedSafeAdbkList = safeAddressBook.push(entry)
map.setIn(['addressBook', safeAddress], updatedSafeAdbkList)
// Only adds entries with valid names
const validName = getValidAddressBookName(entry.name)
if (!entryFound && validName) {
state.push(entry)
}
}
})
}
})
return newState
return state
},
[UPDATE_ENTRY]: (state, action) => {
const { entry } = action.payload
// Updates the entry from all the safes
const newState = state.withMutations((map) => {
map
.get('addressBook')
.keySeq()
.forEach((safeAddress) => {
const entriesList = state.getIn(['addressBook', safeAddress])
const entryIndex = entriesList.findIndex((entryItem) => sameAddress(entryItem.address, entry.address))
const updatedEntriesList = entriesList.set(entryIndex, entry)
map.setIn(['addressBook', safeAddress], updatedEntriesList)
})
})
return newState
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entry.address)
if (entryIndex >= 0) {
state[entryIndex] = entry
}
return state
},
[REMOVE_ENTRY]: (state, action) => {
const { entryAddress } = action.payload
// Removes the entry from all the safes
const newState = state.withMutations((map) => {
map
.get('addressBook')
.keySeq()
.forEach((safeAddress) => {
const entriesList = state.getIn(['addressBook', safeAddress])
const entryIndex = entriesList.findIndex((entry) => sameAddress(entry.address, entryAddress))
const updatedEntriesList = entriesList.remove(entryIndex)
map.setIn(['addressBook', safeAddress], updatedEntriesList)
})
})
return newState
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress)
state.splice(entryIndex, 1)
return state
},
[ADD_OR_UPDATE_ENTRY]: (state, action) => {
const { entry, entryAddress } = action.payload
// Adds or Updates the entry to all the safes
return state.withMutations((map) => {
const addressBook = map.get('addressBook')
if (addressBook) {
addressBook.keySeq().forEach((safeAddress) => {
const safeAddressBook = state.getIn(['addressBook', safeAddress])
const entryIndex = safeAddressBook.findIndex((entryItem) => sameAddress(entryItem.address, entryAddress))
if (entryIndex !== -1) {
const updatedEntriesList = safeAddressBook.update(entryIndex, (currentEntry) => currentEntry.merge(entry))
map.setIn(['addressBook', safeAddress], updatedEntriesList)
// Only updates entries with valid names
const validName = getValidAddressBookName(entry.name)
if (!validName) {
return state
}
const entryIndex = state.findIndex((oldEntry) => oldEntry.address === entryAddress)
if (entryIndex >= 0) {
state[entryIndex] = entry
} else {
const updatedSafeAdbkList = safeAddressBook.push(makeAddressBookEntry(entry))
map.setIn(['addressBook', safeAddress], updatedSafeAdbkList)
state.push(entry)
}
})
}
})
return state
},
},
Map({
addressBook: Map({}),
}),
[],
)

View File

@ -1,24 +0,0 @@
import { AddressBookEntryRecord, AddressBookEntryProps } from 'src/logic/addressBook/model/addressBook'
import { Map, List } from 'immutable'
export interface AddressBookReducerState {
addressBook: AddressBookMap
}
interface AddressBookMapSerialized {
[key: string]: AddressBookEntryProps
}
interface AddressBookReducerStateSerialized extends AddressBookReducerState {
addressBook: Record<string, AddressBookEntryProps[]>
}
export interface AddressBookMap extends Map<string> {
toJS(): AddressBookMapSerialized
get(key: string, notSetValue: unknown): List<AddressBookEntryRecord>
}
export interface AddressBookReducerMap extends Map<string, any> {
toJS(): AddressBookReducerStateSerialized
get<K extends keyof AddressBookReducerState>(key: K): AddressBookReducerState[K]
}

View File

@ -1,36 +1,22 @@
import { AppReduxState } from 'src/store'
import { List } from 'immutable'
import { createSelector } from 'reselect'
import { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
import { AddressBookMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d'
import { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
export const addressBookMapSelector = (state: AppReduxState): AddressBookMap =>
state[ADDRESS_BOOK_REDUCER_ID].get('addressBook')
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
export const getAddressBook = createSelector(
addressBookMapSelector,
safeParamAddressFromStateSelector,
(addressBook, safeAddress) => {
let result: List<AddressBookEntryRecord> = List([])
if (addressBook && safeAddress) {
result = addressBook.get(safeAddress, List())
}
return result
},
)
export const addressBookSelector = (state: AppReduxState): AddressBookState => state[ADDRESS_BOOK_REDUCER_ID]
export const getNameFromAddressBook = createSelector(
getAddressBook,
addressBookSelector,
(_, address) => address,
(addressBook, address) => {
const adbkEntry = addressBook.find((addressBookItem) => addressBookItem.address === address)
if (adbkEntry) {
return adbkEntry.name
}
return 'UNKNOWN'
},
)

View File

@ -1,25 +1,31 @@
import { Map, List } from 'immutable'
import { List } from 'immutable'
import {
getAddressBookFromStorage,
getAddressesListFromSafeAddressBook,
getNameFromSafeAddressBook,
getAddressesListFromAddressBook,
getNameFromAddressBook,
getOwnersWithNameFromAddressBook,
migrateOldAddressBook,
OldAddressBookEntry,
OldAddressBookType,
saveAddressBook,
} from 'src/logic/addressBook/utils/index'
import { buildAddressBook } from 'src/logic/addressBook/store/reducer/addressBook'
import { AddressBookEntryRecord, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
const getMockAddressBookEntry = (
address: string,
name: string = 'test',
isOwner: boolean = false,
): AddressBookEntryRecord =>
const getMockAddressBookEntry = (address: string, name: string = 'test'): AddressBookEntry =>
makeAddressBookEntry({
address,
name,
isOwner,
})
const getMockOldAddressBookEntry = ({ address = '', name = '', isOwner = false }): OldAddressBookEntry => {
return {
address,
name,
isOwner,
}
}
describe('getAddressesListFromAdbk', () => {
const entry1 = getMockAddressBookEntry('123456', 'test1')
const entry2 = getMockAddressBookEntry('78910', 'test2')
@ -27,11 +33,11 @@ describe('getAddressesListFromAdbk', () => {
it('It should returns the list of addresses within the addressBook given a safeAddressBook', () => {
// given
const safeAddressBook = List([entry1, entry2, entry3])
const safeAddressBook = [entry1, entry2, entry3]
const expectedResult = [entry1.address, entry2.address, entry3.address]
// when
const result = getAddressesListFromSafeAddressBook(safeAddressBook)
const result = getAddressesListFromAddressBook(safeAddressBook)
// then
expect(result).toStrictEqual(expectedResult)
@ -44,11 +50,11 @@ describe('getNameFromSafeAddressBook', () => {
const entry3 = getMockAddressBookEntry('4781321', 'test3')
it('It should returns the user name given a safeAddressBook and an user account', () => {
// given
const safeAddressBook = List([entry1, entry2, entry3])
const safeAddressBook = [entry1, entry2, entry3]
const expectedResult = entry2.name
// when
const result = getNameFromSafeAddressBook(safeAddressBook, entry2.address)
const result = getNameFromAddressBook(safeAddressBook, entry2.address)
// then
expect(result).toBe(expectedResult)
@ -61,7 +67,7 @@ describe('getOwnersWithNameFromAddressBook', () => {
const entry3 = getMockAddressBookEntry('4781321', 'test3')
it('It should returns the list of owners with their names given a safeAddressBook and a list of owners', () => {
// given
const safeAddressBook = List([entry1, entry2, entry3])
const safeAddressBook = [entry1, entry2, entry3]
const ownerList = List([
{ address: entry1.address, name: '' },
{ address: entry2.address, name: '' },
@ -80,14 +86,15 @@ describe('getOwnersWithNameFromAddressBook', () => {
})
describe('saveAddressBook', () => {
const safeAddress1 = '0xdfA693da0D16F5E7E78FdCBeDe8FC6eBEa44f1Cf'
const safeAddress2 = '0x344B941b1aAE2e4Be73987212FC4741687Bf0503'
const entry1 = getMockAddressBookEntry('123456', 'test1')
const entry2 = getMockAddressBookEntry('78910', 'test2')
const entry3 = getMockAddressBookEntry('4781321', 'test3')
const mockAdd1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
const mockAdd2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
const mockAdd3 = '0x537BD452c3505FC07bA242E437bD29D4E1DC9126'
const entry1 = getMockAddressBookEntry(mockAdd1, 'test1')
const entry2 = getMockAddressBookEntry(mockAdd2, 'test2')
const entry3 = getMockAddressBookEntry(mockAdd3, 'test3')
it('It should save a given addressBook to the localStorage', async () => {
// given
const addressBook = Map({ [safeAddress1]: List([entry1, entry2]), [safeAddress2]: List([entry3]) })
const addressBook: AddressBookState = [entry1, entry2, entry3]
// when
// @ts-ignore
@ -100,3 +107,95 @@ describe('saveAddressBook', () => {
expect(result).toStrictEqual(addressBook)
})
})
describe('migrateOldAddressBook', () => {
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
it('It should receive an addressBook in old format and return the same addressBook in new format', () => {
// given
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
const oldAddressBook: OldAddressBookType = {
[safeAddress1]: [entry1],
[safeAddress2]: [entry2],
}
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
const expectedResult = [expectedEntry1, expectedEntry2]
// when
const result = migrateOldAddressBook(oldAddressBook)
// then
expect(result).toStrictEqual(expectedResult)
})
})
jest.mock('src/utils/storage/index')
describe('getAddressBookFromStorage', () => {
const safeAddress1 = '0x696fd93D725d84acfFf6c62a1fe8C94E1c9E934A'
const safeAddress2 = '0x2C7aC78b01Be0FC66AD29b684ffAb0C93B381D00'
const mockAdd1 = '0x9163c2F4452E3399CB60AAf737231Af87548DA91'
const mockAdd2 = '0xC4e446Da9C3D37385C86488294C6758c4e25dbD8'
afterAll(() => {
jest.unmock('src/utils/storage/index')
})
it('It should return null if no addressBook in storage', async () => {
// given
const expectedResult = null
const storageUtils = require('src/utils/storage/index')
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => null)
// when
const result = await getAddressBookFromStorage()
// then
expect(result).toStrictEqual(expectedResult)
expect(spy).toHaveBeenCalled()
})
it('It should return migrated addressBook if old addressBook in storage', async () => {
// given
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
const entry1 = getMockOldAddressBookEntry({ name: 'test1', address: mockAdd1 })
const entry2 = getMockOldAddressBookEntry({ name: 'test2', address: mockAdd2 })
const oldAddressBook: OldAddressBookType = {
[safeAddress1]: [entry1],
[safeAddress2]: [entry2],
}
const expectedResult = [expectedEntry1, expectedEntry2]
const storageUtils = require('src/utils/storage/index')
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => oldAddressBook)
// when
const result = await getAddressBookFromStorage()
// then
expect(result).toStrictEqual(expectedResult)
expect(spy).toHaveBeenCalled()
})
it('It should return addressBook if addressBook in storage', async () => {
// given
const expectedEntry1 = getMockAddressBookEntry(mockAdd1, 'test1')
const expectedEntry2 = getMockAddressBookEntry(mockAdd2, 'test2')
const expectedResult = [expectedEntry1, expectedEntry2]
const storageUtils = require('src/utils/storage/index')
const spy = storageUtils.loadFromStorage.mockImplementationOnce(() => JSON.stringify(expectedResult))
// when
const result = await getAddressBookFromStorage()
// then
expect(result).toStrictEqual(expectedResult)
expect(spy).toHaveBeenCalled()
})
})

View File

@ -1,28 +1,62 @@
import { List } from 'immutable'
import { loadFromStorage, saveToStorage } from 'src/utils/storage'
import { AddressBookEntryRecord, AddressBookEntryProps } from '../model/addressBook'
import { AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { SafeOwner } from 'src/logic/safe/store/models/safe'
import { AddressBookCollection } from '../store/reducer/addressBook'
import { AddressBookMap } from '../store/reducer/types/addressBook'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
const ADDRESS_BOOK_STORAGE_KEY = 'ADDRESS_BOOK_STORAGE_KEY'
export const getAddressBookFromStorage = async (): Promise<Array<AddressBookEntryProps> | undefined> => {
return await loadFromStorage<Array<AddressBookEntryProps>>(ADDRESS_BOOK_STORAGE_KEY)
export type OldAddressBookEntry = {
address: string
name: string
isOwner: boolean
}
export const saveAddressBook = async (addressBook: AddressBookMap): Promise<void> => {
export type OldAddressBookType = {
[safeAddress: string]: [OldAddressBookEntry]
}
export const migrateOldAddressBook = (oldAddressBook: OldAddressBookType): AddressBookState => {
const values: AddressBookState = []
const adbkValues = Object.values(oldAddressBook)
for (const safeIterator of adbkValues) {
for (const safeAddressBook of safeIterator) {
if (!values.find((entry) => sameAddress(entry.address, safeAddressBook.address))) {
values.push(makeAddressBookEntry({ address: safeAddressBook.address, name: safeAddressBook.name }))
}
}
}
return values
}
export const getAddressBookFromStorage = async (): Promise<AddressBookState | null> => {
const result: OldAddressBookType | string | undefined = await loadFromStorage(ADDRESS_BOOK_STORAGE_KEY)
if (!result) {
return null
}
if (typeof result === 'string') {
return JSON.parse(result)
}
return migrateOldAddressBook(result as OldAddressBookType)
}
export const saveAddressBook = async (addressBook: AddressBookState): Promise<void> => {
try {
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, addressBook.toJS())
await saveToStorage(ADDRESS_BOOK_STORAGE_KEY, JSON.stringify(addressBook))
} catch (err) {
console.error('Error storing addressBook in localstorage', err)
}
}
export const getAddressesListFromSafeAddressBook = (addressBook: AddressBookCollection): string[] =>
Array.from(addressBook).map((entry: AddressBookEntryRecord) => entry.address)
export const getAddressesListFromAddressBook = (addressBook: AddressBookState): string[] =>
addressBook.map((entry) => entry.address)
export const getNameFromSafeAddressBook = (addressBook: AddressBookCollection, userAddress: string): string | null => {
export const getNameFromAddressBook = (addressBook: AddressBookState, userAddress: string): string | null => {
const entry = addressBook.find((addressBookItem) => addressBookItem.address === userAddress)
if (entry) {
return entry.name
@ -30,15 +64,22 @@ export const getNameFromSafeAddressBook = (addressBook: AddressBookCollection, u
return null
}
export const getValidAddressBookName = (addressbookName: string): string | null => {
const INVALID_NAMES = ['UNKNOWN', 'OWNER #', 'MY WALLET']
const isInvalid = INVALID_NAMES.find((invalidName) => addressbookName.toUpperCase().includes(invalidName))
if (isInvalid) return null
return addressbookName
}
export const getOwnersWithNameFromAddressBook = (
addressBook: AddressBookCollection,
addressBook: AddressBookState,
ownerList: List<SafeOwner>,
): List<SafeOwner> | [] => {
): List<SafeOwner> => {
if (!ownerList) {
return []
return List([])
}
return ownerList.map((owner) => {
const ownerName = getNameFromSafeAddressBook(addressBook, owner.address)
const ownerName = getNameFromAddressBook(addressBook, owner.address)
return {
address: owner.address,
name: ownerName || owner.name,

View File

@ -18,15 +18,16 @@ export const buildOwnersFrom = (names: string[], addresses: string[]): List<Safe
return List(owners)
}
export const addSafe = createAction(ADD_SAFE, (safe) => ({
export const addSafe = createAction(ADD_SAFE, (safe: SafeRecordProps, loadedFromStorage = false) => ({
safe,
loadedFromStorage,
}))
const saveSafe = (safe: SafeRecordProps) => (dispatch: Dispatch, getState: () => AppReduxState): void => {
const state = getState()
const safeList = safesListSelector(state)
dispatch(addSafe(safe))
dispatch(addSafe(safe, true))
if (safeList.size === 0) {
dispatch(setDefaultSafe(safe.address))

View File

@ -13,7 +13,7 @@ const loadSafesFromStorage = () => async (dispatch: Dispatch): Promise<void> =>
if (safes) {
Object.values(safes).forEach((safeProps) => {
dispatch(addSafe(buildSafe(safeProps)))
dispatch(addSafe(buildSafe(safeProps), true))
})
}
} catch (err) {

View File

@ -1,4 +1,3 @@
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
import { saveDefaultSafe, saveSafes } from 'src/logic/safe/utils'
import { tokensSelector } from 'src/logic/tokens/store/selectors'
@ -13,6 +12,12 @@ import { REPLACE_SAFE_OWNER } from 'src/logic/safe/store/actions/replaceSafeOwne
import { SET_DEFAULT_SAFE } from 'src/logic/safe/store/actions/setDefaultSafe'
import { UPDATE_SAFE } from 'src/logic/safe/store/actions/updateSafe'
import { getActiveTokensAddressesForAllSafes, safesMapSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'
import { AddressBookEntry, AddressBookState, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
const watchedActions = [
ADD_SAFE,
@ -41,6 +46,30 @@ const recalculateActiveTokens = (state) => {
saveActiveTokens(activeTokens)
}
/**
* If the owner has a valid name that means that should be on the addressBook
* if the owner is not currently on the addressBook, that means the user deleted it
* or that it's a new safe with valid names, so we also check if it's a new safe or an already loaded one
* @param name
* @param address
* @param addressBook
* @param safeAlreadyLoaded -> true if the safe was loaded from the localStorage
*/
// TODO TEST
const checkIfOwnerWasDeletedFromAddressBook = (
{ name, address }: AddressBookEntry,
addressBook: AddressBookState,
safeAlreadyLoaded: boolean,
) => {
if (!safeAlreadyLoaded) {
return false
}
const addressShouldBeOnTheAddressBook = !!getValidAddressBookName(name)
const isAlreadyInAddressBook = !!addressBook.find((entry) => sameAddress(entry.address, address))
return addressShouldBeOnTheAddressBook && !isAlreadyInAddressBook
}
const safeStorageMware = (store) => (next) => async (action) => {
const handledAction = next(action)
@ -48,6 +77,7 @@ const safeStorageMware = (store) => (next) => async (action) => {
const state = store.getState()
const { dispatch } = store
const safes = safesMapSelector(state)
const addressBook = addressBookSelector(state)
await saveSafes(safes.toJSON())
switch (action.type) {
@ -56,19 +86,42 @@ const safeStorageMware = (store) => (next) => async (action) => {
break
}
case ADD_SAFE: {
const { safe } = action.payload
const ownersArray = safe.owners.toJS()
// Adds the owners to the address book
ownersArray.forEach((owner) => {
dispatch(addAddressBookEntry(makeAddressBookEntry({ ...owner, isOwner: true })))
const { safe, loadedFromStorage } = action.payload
safe.owners.forEach((owner) => {
const checksumEntry = makeAddressBookEntry({ address: checksumAddress(owner.address), name: owner.name })
const ownerWasAlreadyInAddressBook = checkIfOwnerWasDeletedFromAddressBook(
checksumEntry,
addressBook,
loadedFromStorage,
)
if (!ownerWasAlreadyInAddressBook) {
dispatch(addAddressBookEntry(checksumEntry, { notifyEntryUpdate: false }))
}
})
const safeWasAlreadyInAddressBook = checkIfOwnerWasDeletedFromAddressBook(
{ address: safe.address, name: safe.name },
addressBook,
loadedFromStorage,
)
if (!safeWasAlreadyInAddressBook) {
dispatch(
addAddressBookEntry(makeAddressBookEntry({ address: safe.address, name: safe.name }), {
notifyEntryUpdate: true,
}),
)
}
break
}
case UPDATE_SAFE: {
const { activeTokens } = action.payload
const { activeTokens, name, address } = action.payload
if (activeTokens) {
recalculateActiveTokens(state)
}
if (name) {
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ name, address })), { notifyEntryUpdate: false })
}
break
}
case SET_DEFAULT_SAFE: {

View File

@ -1,7 +1,10 @@
import { handleActions } from 'redux-actions'
import { Transaction } from '../models/types/transactions'
import { LOAD_MORE_TRANSACTIONS, LoadMoreTransactionsAction } from '../actions/allTransactions/pagination'
import { Transaction } from 'src/logic/safe/store/models/types/transactions'
import {
LOAD_MORE_TRANSACTIONS,
LoadMoreTransactionsAction,
} from 'src/logic/safe/store/actions/allTransactions/pagination'
export const TRANSACTIONS = 'allTransactions'

View File

@ -19,8 +19,8 @@ import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
import { getAddressesListFromSafeAddressBook } from 'src/logic/addressBook/utils'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getAddressesListFromAddressBook } from 'src/logic/addressBook/utils'
export const CREATE_ENTRY_INPUT_NAME_ID = 'create-entry-input-name'
export const CREATE_ENTRY_INPUT_ADDRESS_ID = 'create-entry-input-address'
@ -42,8 +42,8 @@ const CreateEditEntryModalComponent = ({
}
}
const addressBook = useSelector(getAddressBook)
const addressBookAddressesList = getAddressesListFromSafeAddressBook(addressBook)
const addressBook = useSelector(addressBookSelector)
const addressBookAddressesList = getAddressesListFromAddressBook(addressBook)
const entryDoesntExist = uniqueAddress(addressBookAddressesList)
const formMutators = {

View File

@ -1,4 +1,5 @@
import { List } from 'immutable'
import { TableCellProps } from '@material-ui/core/TableCell/TableCell'
export const ADDRESS_BOOK_ROW_ID = 'address-book-row'
export const TX_TABLE_ADDRESS_BOOK_ID = 'idAddressBook'
@ -9,7 +10,17 @@ export const EDIT_ENTRY_BUTTON = 'edit-entry-btn'
export const REMOVE_ENTRY_BUTTON = 'remove-entry-btn'
export const SEND_ENTRY_BUTTON = 'send-entry-btn'
export const generateColumns = () => {
type AddressBookColumn = {
id: string
order: boolean
disablePadding?: boolean
label: string
width?: number
custom?: boolean
align?: TableCellProps['align']
}
export const generateColumns = (): List<AddressBookColumn> => {
const nameColumn = {
id: AB_NAME_ID,
order: false,

View File

@ -1,10 +1,8 @@
import TableCell from '@material-ui/core/TableCell'
import TableContainer from '@material-ui/core/TableContainer'
import TableRow from '@material-ui/core/TableRow'
import { withStyles } from '@material-ui/core/styles'
// import CallMade from '@material-ui/icons/CallMade'
import { makeStyles } from '@material-ui/core/styles'
import cn from 'classnames'
// import classNames from 'classnames/bind'
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
@ -18,11 +16,11 @@ import ButtonLink from 'src/components/layout/ButtonLink'
import Col from 'src/components/layout/Col'
import Img from 'src/components/layout/Img'
import Row from 'src/components/layout/Row'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { AddressBookEntry, makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { addAddressBookEntry } from 'src/logic/addressBook/store/actions/addAddressBookEntry'
import { removeAddressBookEntry } from 'src/logic/addressBook/store/actions/removeAddressBookEntry'
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { isUserAnOwnerOfAnySafe } from 'src/logic/wallets/ethAddresses'
import CreateEditEntryModal from 'src/routes/safe/components/AddressBook/CreateEditEntryModal'
import DeleteEntryModal from 'src/routes/safe/components/AddressBook/DeleteEntryModal'
@ -38,19 +36,32 @@ import SendModal from 'src/routes/safe/components/Balances/SendModal'
import OwnerAddressTableCell from 'src/routes/safe/components/Settings/ManageOwners/OwnerAddressTableCell'
import RenameOwnerIcon from 'src/routes/safe/components/Settings/ManageOwners/assets/icons/rename-owner.svg'
import RemoveOwnerIcon from 'src/routes/safe/components/Settings/assets/icons/bin.svg'
import RemoveOwnerIconDisabled from 'src/routes/safe/components/Settings/assets/icons/disabled-bin.svg'
import { addressBookQueryParamsSelector, safesListSelector } from 'src/logic/safe/store/selectors'
import { checksumAddress } from 'src/utils/checksumAddress'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
const AddressBookTable = ({ classes }) => {
const useStyles = makeStyles(styles)
interface AddressBookSelectedEntry extends AddressBookEntry {
isNew?: boolean
}
const AddressBookTable = (): React.ReactElement => {
const classes = useStyles()
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const dispatch = useDispatch()
const safesList = useSelector(safesListSelector)
const entryAddressToEditOrCreateNew = useSelector(addressBookQueryParamsSelector)
const addressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState<any>(null)
const addressBook = useSelector(addressBookSelector)
const granted = useSelector(grantedSelector)
const [selectedEntry, setSelectedEntry] = useState<{
entry?: AddressBookSelectedEntry
index?: number
isOwnerAddress?: boolean
} | null>(null)
const [editCreateEntryModalOpen, setEditCreateEntryModalOpen] = useState(false)
const [deleteEntryModalOpen, setDeleteEntryModalOpen] = useState(false)
const [sendFundsModalOpen, setSendFundsModalOpen] = useState(false)
@ -69,11 +80,10 @@ const AddressBookTable = ({ classes }) => {
useEffect(() => {
if (entryAddressToEditOrCreateNew) {
const checksumEntryAdd = checksumAddress(entryAddressToEditOrCreateNew)
const key = addressBook.findKey((entry) => entry.address === checksumEntryAdd)
if (key && key >= 0) {
const oldEntryIndex = addressBook.findIndex((entry) => entry.address === checksumEntryAdd)
if (oldEntryIndex >= 0) {
// Edit old entry
const value = addressBook.get(key)
setSelectedEntry({ entry: value, index: key })
setSelectedEntry({ entry: addressBook[oldEntryIndex], index: oldEntryIndex })
} else {
// Create new entry
setSelectedEntry({
@ -107,7 +117,7 @@ const AddressBookTable = ({ classes }) => {
}
const deleteEntryModalHandler = () => {
const entryAddress = checksumAddress(selectedEntry.entry.address)
const entryAddress = selectedEntry && selectedEntry.entry ? checksumAddress(selectedEntry.entry.address) : ''
setSelectedEntry(null)
setDeleteEntryModalOpen(false)
dispatch(removeAddressBookEntry(entryAddress))
@ -138,7 +148,7 @@ const AddressBookTable = ({ classes }) => {
defaultRowsPerPage={25}
disableLoadingOnEmptyTable
label="Owners"
size={addressBook?.size || 0}
size={addressBook?.length || 0}
>
{(sortedData) =>
sortedData.map((row, index) => {
@ -151,20 +161,22 @@ const AddressBookTable = ({ classes }) => {
key={index}
tabIndex={-1}
>
{autoColumns.map((column: any) => (
{autoColumns.map((column) => {
return (
<TableCell align={column.align} component="td" key={column.id} style={cellWidth(column.width)}>
{column.id === AB_ADDRESS_ID ? (
<OwnerAddressTableCell address={row[column.id]} showLinks />
) : (
row[column.id]
getValidAddressBookName(row[column.id])
)}
</TableCell>
))}
)
})}
<TableCell component="td">
<Row align="end" className={classes.actions}>
<Img
alt="Edit entry"
className={classes.editEntryButton}
className={granted ? classes.editEntryButton : classes.editEntryButtonNonOwner}
onClick={() => {
setSelectedEntry({
entry: row,
@ -177,16 +189,15 @@ const AddressBookTable = ({ classes }) => {
/>
<Img
alt="Remove entry"
className={userOwner ? classes.removeEntryButtonDisabled : classes.removeEntryButton}
className={granted ? classes.removeEntryButton : classes.removeEntryButtonNonOwner}
onClick={() => {
if (!userOwner) {
setSelectedEntry({ entry: row })
setDeleteEntryModalOpen(true)
}
}}
src={userOwner ? RemoveOwnerIconDisabled : RemoveOwnerIcon}
src={RemoveOwnerIcon}
testId={REMOVE_ENTRY_BUTTON}
/>
{granted ? (
<Button
className={classes.send}
color="primary"
@ -198,12 +209,9 @@ const AddressBookTable = ({ classes }) => {
testId={SEND_ENTRY_BUTTON}
variant="contained"
>
{/* <CallMade
alt="Send Transaction"
className={classNames(classes.leftIcon, classes.iconSmall)}
/> */}
Send
</Button>
) : null}
</Row>
</TableCell>
</TableRow>
@ -236,4 +244,4 @@ const AddressBookTable = ({ classes }) => {
)
}
export default withStyles(styles as any)(AddressBookTable)
export default AddressBookTable

View File

@ -1,6 +1,7 @@
import { lg, marginButtonImg, md, sm } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
formContainer: {
minHeight: '250px',
},
@ -38,6 +39,9 @@ export const styles = () => ({
cursor: 'pointer',
marginBottom: marginButtonImg,
},
editEntryButtonNonOwner: {
cursor: 'pointer',
},
removeEntryButton: {
marginLeft: lg,
marginRight: lg,
@ -50,6 +54,11 @@ export const styles = () => ({
marginBottom: marginButtonImg,
cursor: 'default',
},
removeEntryButtonNonOwner: {
marginLeft: lg,
marginRight: lg,
cursor: 'pointer',
},
message: {
padding: `${md} 0`,
maxHeight: '54px',

View File

@ -1,7 +1,6 @@
import MuiTextField from '@material-ui/core/TextField'
import makeStyles from '@material-ui/core/styles/makeStyles'
import Autocomplete from '@material-ui/lab/Autocomplete'
import { List } from 'immutable'
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { trimSpaces } from 'src/utils/strings'
@ -10,10 +9,10 @@ import { styles } from './style'
import Identicon from 'src/components/Identicon'
import { mustBeEthereumAddress, mustBeEthereumContractAddress } from 'src/components/forms/validator'
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getAddressFromENS } from 'src/logic/wallets/getWeb3'
import { isValidEnsName } from 'src/logic/wallets/ethAddresses'
import { AddressBookEntryRecord } from 'src/logic/addressBook/model/addressBook'
import { AddressBookEntry, AddressBookState } from 'src/logic/addressBook/model/addressBook'
export interface AddressBookProps {
fieldMutator: (address: string) => void
@ -44,12 +43,10 @@ const textFieldInputStyle = makeStyles(() => ({
},
}))
const filterAddressBookWithContractAddresses = async (
addressBook: List<AddressBookEntryRecord>,
): Promise<List<AddressBookEntryRecord>> => {
const filterAddressBookWithContractAddresses = async (addressBook: AddressBookState): Promise<AddressBookEntry[]> => {
const abFlags = await Promise.all(
addressBook.map(
async ({ address }: AddressBookEntryRecord): Promise<boolean> => {
async ({ address }: AddressBookEntry): Promise<boolean> => {
return (await mustBeEthereumContractAddress(address)) === undefined
},
),
@ -58,11 +55,6 @@ const filterAddressBookWithContractAddresses = async (
return addressBook.filter((_, index) => abFlags[index])
}
interface FilteredAddressBookEntry {
name: string
address: string
}
const AddressBookInput = ({
fieldMutator,
isCustomTx,
@ -72,12 +64,12 @@ const AddressBookInput = ({
setSelectedEntry,
}: AddressBookProps): React.ReactElement => {
const classes = useStyles()
const addressBook = useSelector(getAddressBook)
const addressBook = useSelector(addressBookSelector)
const [isValidForm, setIsValidForm] = useState(true)
const [validationText, setValidationText] = useState<string>('')
const [inputTouched, setInputTouched] = useState(false)
const [blurred, setBlurred] = useState(pristine)
const [adbkList, setADBKList] = useState<List<FilteredAddressBookEntry>>(List([]))
const [adbkList, setADBKList] = useState<AddressBookEntry[]>([])
const [inputAddValue, setInputAddValue] = useState(recipientAddress)
@ -168,7 +160,7 @@ const AddressBookInput = ({
freeSolo
getOptionLabel={(adbkEntry) => adbkEntry.address || ''}
id="free-solo-demo"
onChange={(_, value: FilteredAddressBookEntry) => {
onChange={(_, value: AddressBookEntry) => {
let address = ''
let name = ''
if (value) {
@ -184,7 +176,7 @@ const AddressBookInput = ({
setBlurred(false)
}}
open={!blurred}
options={adbkList.toArray()}
options={adbkList}
renderInput={(params) => (
<MuiTextField
{...params}

View File

@ -20,8 +20,8 @@ import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
import { getNameFromSafeAddressBook } from 'src/logic/addressBook/utils'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
import { nftTokensSelector, safeActiveSelectorMap } from 'src/logic/collectibles/store/selectors'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
@ -41,14 +41,20 @@ const formMutators = {
},
}
const useStyles = makeStyles(styles as any)
const useStyles = makeStyles(styles)
const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = {} }) => {
const SendCollectible = ({
initialValues,
onClose,
onNext,
recipientAddress,
selectedToken = {},
}): React.ReactElement => {
const classes = useStyles()
const nftAssets = useSelector(safeActiveSelectorMap)
const nftTokens = useSelector(nftTokensSelector)
const addressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState({
const addressBook = useSelector(addressBookSelector)
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
})
@ -64,7 +70,7 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
const handleSubmit = (values) => {
// If the input wasn't modified, there was no mutation of the recipientAddress
if (!values.recipientAddress) {
values.recipientAddress = selectedEntry.address
values.recipientAddress = selectedEntry?.address
}
values.assetName = nftAssets[values.assetAddress].name
@ -97,10 +103,10 @@ const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, sel
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromSafeAddressBook(addressBook, scannedAddress) : ''
const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName || '',
name: scannedName,
address: scannedAddress,
})
closeQrModal()

View File

@ -1,6 +1,7 @@
import { lg, md, secondaryText } from 'src/theme/variables'
import { createStyles } from '@material-ui/core'
export const styles = () => ({
export const styles = createStyles({
heading: {
padding: `${md} ${lg}`,
justifyContent: 'flex-start',

View File

@ -25,8 +25,8 @@ import Col from 'src/components/layout/Col'
import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
import { getNameFromSafeAddressBook } from 'src/logic/addressBook/utils'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { getNameFromAddressBook } from 'src/logic/addressBook/utils'
import SafeInfo from 'src/routes/safe/components/Balances/SendModal/SafeInfo'
import AddressBookInput from 'src/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
@ -69,8 +69,8 @@ const SendFunds = ({
}: SendFundsProps): React.ReactElement => {
const classes = useStyles()
const tokens = useSelector(extendedSafeTokensSelector)
const addressBook = useSelector(getAddressBook)
const [selectedEntry, setSelectedEntry] = useState({
const addressBook = useSelector(addressBookSelector)
const [selectedEntry, setSelectedEntry] = useState<{ address?: string; name?: string | null } | null>({
address: recipientAddress || initialValues.recipientAddress,
name: '',
})
@ -88,7 +88,7 @@ const SendFunds = ({
const submitValues = values
// If the input wasn't modified, there was no mutation of the recipientAddress
if (!values.recipientAddress) {
submitValues.recipientAddress = selectedEntry.address
submitValues.recipientAddress = selectedEntry?.address
}
onNext(submitValues)
}
@ -118,7 +118,7 @@ const SendFunds = ({
if (scannedAddress.startsWith('ethereum:')) {
scannedAddress = scannedAddress.replace('ethereum:', '')
}
const scannedName = addressBook ? getNameFromSafeAddressBook(addressBook, scannedAddress) : ''
const scannedName = addressBook ? getNameFromAddressBook(addressBook, scannedAddress) : ''
mutators.setRecipient(scannedAddress)
setSelectedEntry({
name: scannedName || '',

View File

@ -20,12 +20,12 @@ import Hairline from 'src/components/layout/Hairline'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import { makeAddressBookEntry } from 'src/logic/addressBook/model/addressBook'
import { updateAddressBookEntry } from 'src/logic/addressBook/store/actions/updateAddressBookEntry'
import { NOTIFICATIONS } from 'src/logic/notifications'
import editSafeOwner from 'src/logic/safe/store/actions/editSafeOwner'
import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors'
import { sm } from 'src/theme/variables'
import enqueueSnackbar from 'src/logic/notifications/store/actions/enqueueSnackbar'
import { addOrUpdateAddressBookEntry } from 'src/logic/addressBook/store/actions/addOrUpdateAddressBookEntry'
export const RENAME_OWNER_INPUT_TEST_ID = 'rename-owner-input'
export const SAVE_OWNER_CHANGES_BTN_TEST_ID = 'save-owner-changes-btn'
@ -47,7 +47,7 @@ const EditOwnerComponent = ({ isOpen, onClose, ownerAddress, selectedOwnerName }
const { ownerName } = values
dispatch(editSafeOwner({ safeAddress, ownerAddress, ownerName }))
dispatch(updateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
dispatch(addOrUpdateAddressBookEntry(makeAddressBookEntry({ address: ownerAddress, name: ownerName })))
dispatch(enqueueSnackbar(NOTIFICATIONS.OWNER_NAME_CHANGE_EXECUTED_MSG))
onClose()

View File

@ -6,6 +6,7 @@ import Block from 'src/components/layout/Block'
import Paragraph from 'src/components/layout/Paragraph'
import { useWindowDimensions } from 'src/logic/hooks/useWindowDimensions'
import { useEffect, useState } from 'react'
import { getValidAddressBookName } from 'src/logic/addressBook/utils'
type OwnerAddressTableCellProps = {
address: string
@ -34,7 +35,7 @@ const OwnerAddressTableCell = (props: OwnerAddressTableCellProps): React.ReactEl
<Identicon address={address} diameter={32} />
{showLinks ? (
<div style={{ marginLeft: 10, flexShrink: 1, minWidth: 0 }}>
{!userName || userName === 'UNKNOWN' ? null : userName}
{userName && getValidAddressBookName(userName)}
<EtherScanLink knownAddress={knownAddress} type="address" value={address} cut={cut} />
</div>
) : (

View File

@ -30,8 +30,8 @@ import Paragraph from 'src/components/layout/Paragraph/index'
import Row from 'src/components/layout/Row'
import { getOwnersWithNameFromAddressBook } from 'src/logic/addressBook/utils'
import { useAnalytics, SAFE_NAVIGATION_EVENT } from 'src/utils/googleAnalytics'
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
import { SafeOwner } from 'src/logic/safe/store/models/safe'
import { AddressBookCollection } from 'src/logic/addressBook/store/reducer/addressBook'
export const RENAME_OWNER_BTN_TEST_ID = 'rename-owner-btn'
export const REMOVE_OWNER_BTN_TEST_ID = 'remove-owner-btn'
@ -42,7 +42,7 @@ export const OWNERS_ROW_TEST_ID = 'owners-row'
const useStyles = makeStyles(styles)
type Props = {
addressBook: unknown
addressBook: AddressBookState
granted: boolean
owners: List<SafeOwner>
}
@ -84,7 +84,7 @@ const ManageOwners = ({ addressBook, granted, owners }: Props): React.ReactEleme
const columns = generateColumns()
const autoColumns = columns.filter((c) => !c.custom)
const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook as AddressBookCollection, owners)
const ownersAdbk = getOwnersWithNameFromAddressBook(addressBook, owners)
const ownerData = getOwnerData(ownersAdbk)
return (

View File

@ -23,7 +23,7 @@ import Img from 'src/components/layout/Img'
import Paragraph from 'src/components/layout/Paragraph'
import Row from 'src/components/layout/Row'
import Span from 'src/components/layout/Span'
import { getAddressBook } from 'src/logic/addressBook/store/selectors'
import { addressBookSelector } from 'src/logic/addressBook/store/selectors'
import { grantedSelector } from 'src/routes/safe/container/selector'
import { safeNeedsUpdateSelector, safeOwnersSelector } from 'src/logic/safe/store/selectors'
@ -42,7 +42,7 @@ const Settings: React.FC = () => {
const owners = useSelector(safeOwnersSelector)
const needsUpdate = useSelector(safeNeedsUpdateSelector)
const granted = useSelector(grantedSelector)
const addressBook = useSelector(getAddressBook)
const addressBook = useSelector(addressBookSelector)
const handleChange = (menuOptionIndex) => () => {
setState((prevState) => ({ ...prevState, menuOptionIndex }))

View File

@ -6,7 +6,6 @@ import thunk from 'redux-thunk'
import addressBookMiddleware from 'src/logic/addressBook/store/middleware/addressBookMiddleware'
import addressBook, { ADDRESS_BOOK_REDUCER_ID } from 'src/logic/addressBook/store/reducer/addressBook'
import { AddressBookReducerMap } from 'src/logic/addressBook/store/reducer/types/addressBook.d'
import {
NFT_ASSETS_REDUCER_ID,
NFT_TOKENS_REDUCER_ID,
@ -19,8 +18,10 @@ import currencyValues, {
CURRENCY_VALUES_KEY,
CurrencyValuesState,
} from 'src/logic/currencyValues/store/reducer/currencyValues'
import { CurrentSessionState } from 'src/logic/currentSession/store/reducer/currentSession'
import currentSession, { CURRENT_SESSION_REDUCER_ID } from 'src/logic/currentSession/store/reducer/currentSession'
import currentSession, {
CURRENT_SESSION_REDUCER_ID,
CurrentSessionState,
} from 'src/logic/currentSession/store/reducer/currentSession'
import notifications, { NOTIFICATIONS_REDUCER_ID } from 'src/logic/notifications/store/reducer/notifications'
import tokens, { TOKEN_REDUCER_ID, TokenState } from 'src/logic/tokens/store/reducer/tokens'
import providerWatcher from 'src/logic/wallets/store/middlewares/providerWatcher'
@ -38,7 +39,8 @@ import safe, { SAFE_REDUCER_ID } from 'src/logic/safe/store/reducer/safe'
import transactions, { TRANSACTIONS_REDUCER_ID } from 'src/logic/safe/store/reducer/transactions'
import { NFTAssets, NFTTokens } from 'src/logic/collectibles/sources/OpenSea'
import { SafeReducerMap } from 'src/routes/safe/store/reducer/types/safe'
import allTransactions, { TRANSACTIONS, TransactionsState } from 'src/logic/safe/store/reducer/allTransactions'
import allTransactions, { TRANSACTIONS, TransactionsState } from '../logic/safe/store/reducer/allTransactions'
import { AddressBookState } from 'src/logic/addressBook/model/addressBook'
export const history = createHashHistory()
@ -86,7 +88,7 @@ export type AppReduxState = CombinedState<{
[NOTIFICATIONS_REDUCER_ID]: Map<string, any>
[CURRENCY_VALUES_KEY]: CurrencyValuesState
[COOKIES_REDUCER_ID]: Map<string, any>
[ADDRESS_BOOK_REDUCER_ID]: AddressBookReducerMap
[ADDRESS_BOOK_REDUCER_ID]: AddressBookState
[CURRENT_SESSION_REDUCER_ID]: CurrentSessionState
[TRANSACTIONS]: TransactionsState
router: RouterState