wallet-stack 1.0.0-alpha.133 → 1.0.0-alpha.135

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,12 +31,11 @@ export interface ImportContactProgress {
31
31
  total: number
32
32
  }
33
33
 
34
- export interface AddressToVerificationStatus {
35
- [address: string]: boolean | undefined
36
- }
37
-
38
34
  export interface AddressToVerifiedByType {
39
- [address: string]: string | undefined
35
+ // undefined = never checked / unknown
36
+ // null = checked, no known verifier
37
+ // string = checked, verified by that verifier (e.g. "valora", "minipay")
38
+ [address: string]: string | null | undefined
40
39
  }
41
40
 
42
41
  interface State {
@@ -48,8 +47,6 @@ interface State {
48
47
  // Has the user already been asked for contacts permission
49
48
  askedContactsPermission: boolean
50
49
  importContactsProgress: ImportContactProgress
51
- // Mapping of address to verification status; undefined entries represent a loading state
52
- addressToVerificationStatus: AddressToVerificationStatus
53
50
  // Mapping of address to the entity that verified it (e.g. "valora", "minipay")
54
51
  addressToVerifiedBy: AddressToVerifiedByType
55
52
  lastSavedContactsHash: string | null
@@ -66,7 +63,6 @@ const initialState: State = {
66
63
  current: 0,
67
64
  total: 0,
68
65
  },
69
- addressToVerificationStatus: {},
70
66
  addressToVerifiedBy: {},
71
67
  lastSavedContactsHash: null,
72
68
  shouldRefreshStoredPasswordHash: false,
@@ -144,21 +140,11 @@ export const reducer = (
144
140
  e164NumberToAddress: state.e164NumberToAddress,
145
141
  }
146
142
  case Actions.FETCH_ADDRESS_VERIFICATION_STATUS:
147
- // If the current status is false or does not exist, we set it to undefined
148
- // to mark it as being in a loading state.
149
- return {
150
- ...state,
151
- addressToVerificationStatus: {
152
- ...state.addressToVerificationStatus,
153
- [action.address]: state.addressToVerificationStatus[action.address] || undefined,
154
- },
155
- }
156
- case Actions.ADDRESS_VERIFICATION_STATUS_RECEIVED:
157
143
  return {
158
144
  ...state,
159
- addressToVerificationStatus: {
160
- ...state.addressToVerificationStatus,
161
- [action.address]: action.addressVerified,
145
+ addressToVerifiedBy: {
146
+ ...state.addressToVerifiedBy,
147
+ [action.address]: undefined,
162
148
  },
163
149
  }
164
150
  case Actions.CONTACTS_SAVED:
@@ -1,8 +1,6 @@
1
1
  import { RootState } from 'src/redux/reducers'
2
2
 
3
3
  export const e164NumberToAddressSelector = (state: RootState) => state.identity.e164NumberToAddress
4
- export const addressToVerificationStatusSelector = (state: RootState) =>
5
- state.identity.addressToVerificationStatus
6
4
  export const addressToE164NumberSelector = (state: RootState) => state.identity.addressToE164Number
7
5
  export const addressToVerifiedBySelector = (state: RootState) => state.identity.addressToVerifiedBy
8
6
  export const importContactsProgressSelector = (state: RootState) =>
@@ -4,23 +4,12 @@ import 'react-native'
4
4
  import { Provider } from 'react-redux'
5
5
  import RecipientItem from 'src/recipients/RecipientItemV2'
6
6
  import { createMockStore } from 'test/utils'
7
- import {
8
- mockAddressRecipient,
9
- mockInvitableRecipient,
10
- mockPhoneRecipient,
11
- mockRecipient,
12
- } from 'test/values'
7
+ import { mockAddressRecipient, mockInvitableRecipient, mockPhoneRecipient } from 'test/values'
13
8
 
14
9
  describe('RecipientItemV2', () => {
15
- it('renders correctly with no app icon if phone number recipient is not a known app user (number never looked up)', () => {
16
- const { queryByTestId, getByText } = render(
17
- <Provider
18
- store={createMockStore({
19
- identity: {
20
- e164NumberToAddress: {},
21
- },
22
- })}
23
- >
10
+ it('renders contact name and phone number', () => {
11
+ const { getByText } = render(
12
+ <Provider store={createMockStore()}>
24
13
  <RecipientItem
25
14
  recipient={mockInvitableRecipient}
26
15
  onSelectRecipient={jest.fn()}
@@ -30,35 +19,23 @@ describe('RecipientItemV2', () => {
30
19
  )
31
20
  expect(getByText(mockInvitableRecipient.name)).toBeTruthy()
32
21
  expect(getByText(mockInvitableRecipient.displayNumber)).toBeTruthy()
33
- expect(queryByTestId('RecipientItem/AppIcon')).toBeFalsy()
34
- expect(queryByTestId('RecipientItem/ActivityIndicator')).toBeFalsy()
35
22
  })
36
23
 
37
- it('renders correctly with no app icon if phone number recipient is not a known app user (number looked up before)', () => {
38
- const { queryByTestId, getByText } = render(
39
- <Provider
40
- store={createMockStore({
41
- identity: {
42
- e164NumberToAddress: { [mockInvitableRecipient.e164PhoneNumber]: null },
43
- },
44
- })}
45
- >
24
+ it('renders spinner while loading', () => {
25
+ const { getByTestId } = render(
26
+ <Provider store={createMockStore()}>
46
27
  <RecipientItem
47
28
  recipient={mockInvitableRecipient}
48
29
  onSelectRecipient={jest.fn()}
49
- loading={false}
30
+ loading={true}
50
31
  />
51
32
  </Provider>
52
33
  )
53
- expect(getByText(mockInvitableRecipient.name)).toBeTruthy()
54
- expect(getByText(mockInvitableRecipient.displayNumber)).toBeTruthy()
55
- expect(queryByTestId('RecipientItem/AppIcon')).toBeFalsy()
56
- expect(queryByTestId('RecipientItem/ActivityIndicator')).toBeFalsy()
34
+ expect(getByTestId('RecipientItem/ActivityIndicator')).toBeTruthy()
57
35
  })
58
36
 
59
- it('renders correctly with app icon if phone number recipient is an app user', () => {
60
- const { queryByTestId, getByText, getByTestId } = render(
61
- // default store includes a cached mapping
37
+ it('hides spinner when not loading', () => {
38
+ const { queryByTestId } = render(
62
39
  <Provider store={createMockStore()}>
63
40
  <RecipientItem
64
41
  recipient={mockInvitableRecipient}
@@ -67,47 +44,9 @@ describe('RecipientItemV2', () => {
67
44
  />
68
45
  </Provider>
69
46
  )
70
- expect(getByText(mockInvitableRecipient.name)).toBeTruthy()
71
- expect(getByText(mockInvitableRecipient.displayNumber)).toBeTruthy()
72
- expect(getByTestId('RecipientItem/AppIcon')).toBeTruthy()
73
- expect(queryByTestId('RecipientItem/ActivityIndicator')).toBeFalsy()
74
- })
75
-
76
- it('renders correctly with app icon if address recipient is an app user', () => {
77
- const { queryByTestId, getByText, getByTestId } = render(
78
- // default store includes a cached mapping
79
- <Provider
80
- store={createMockStore({
81
- identity: {
82
- addressToVerificationStatus: {
83
- [mockRecipient.address]: true,
84
- },
85
- },
86
- })}
87
- >
88
- <RecipientItem recipient={mockRecipient} onSelectRecipient={jest.fn()} loading={false} />
89
- </Provider>
90
- )
91
- expect(getByText(mockRecipient.name)).toBeTruthy()
92
- expect(getByTestId('RecipientItem/AppIcon')).toBeTruthy()
93
47
  expect(queryByTestId('RecipientItem/ActivityIndicator')).toBeFalsy()
94
48
  })
95
49
 
96
- it('renders correctly if loading is set to true', () => {
97
- const { getByText, getByTestId } = render(
98
- <Provider store={createMockStore()}>
99
- <RecipientItem
100
- recipient={mockInvitableRecipient}
101
- onSelectRecipient={jest.fn()}
102
- loading={true}
103
- />
104
- </Provider>
105
- )
106
- expect(getByText(mockInvitableRecipient.name)).toBeTruthy()
107
- expect(getByText(mockInvitableRecipient.displayNumber)).toBeTruthy()
108
- expect(getByTestId('RecipientItem/ActivityIndicator')).toBeTruthy()
109
- })
110
-
111
50
  it('tapping item invokes onSelectRecipient', () => {
112
51
  const mockSelectRecipient = jest.fn()
113
52
  const { getByTestId } = render(
@@ -1,23 +1,16 @@
1
- import React, { memo, useMemo } from 'react'
1
+ import React, { memo } from 'react'
2
2
  import { useTranslation } from 'react-i18next'
3
3
  import { ActivityIndicator, Keyboard, StyleSheet, Text, View } from 'react-native'
4
4
  import ContactCircle from 'src/components/ContactCircle'
5
5
  import Touchable from 'src/components/Touchable'
6
6
  import PhoneIcon from 'src/icons/Phone'
7
7
  import WalletIcon from 'src/icons/navigator/Wallet'
8
- import {
9
- addressToVerificationStatusSelector,
10
- e164NumberToAddressSelector,
11
- } from 'src/identity/selectors'
12
- import Checkmark from 'src/icons/Checkmark'
13
8
  import {
14
9
  Recipient,
15
- RecipientType,
16
10
  getDisplayDetail,
17
11
  getDisplayName,
18
12
  recipientHasNumber,
19
13
  } from 'src/recipients/recipient'
20
- import { useSelector } from 'src/redux/hooks'
21
14
  import Colors from 'src/styles/colors'
22
15
  import { typeScale } from 'src/styles/fonts'
23
16
  import { Spacing } from 'src/styles/styles'
@@ -29,8 +22,6 @@ interface Props {
29
22
  selected?: boolean
30
23
  }
31
24
 
32
- const ICON_SIZE = 10
33
-
34
25
  function RecipientItem({ recipient, onSelectRecipient, loading, selected }: Props) {
35
26
  const { t } = useTranslation()
36
27
 
@@ -39,17 +30,6 @@ function RecipientItem({ recipient, onSelectRecipient, loading, selected }: Prop
39
30
  onSelectRecipient(recipient)
40
31
  }
41
32
 
42
- const e164NumberToAddress = useSelector(e164NumberToAddressSelector)
43
- const addressToVerificationStatus = useSelector(addressToVerificationStatusSelector)
44
-
45
- // TODO(ACT-980): avoid icon flash when a known contact is clicked
46
- const showAppIcon = useMemo(() => {
47
- if (recipient.recipientType === RecipientType.PhoneNumber) {
48
- return recipient.e164PhoneNumber && !!e164NumberToAddress[recipient.e164PhoneNumber]
49
- }
50
- return recipient.address && addressToVerificationStatus[recipient.address]
51
- }, [e164NumberToAddress, recipient])
52
-
53
33
  return (
54
34
  <Touchable onPress={onPress} testID="RecipientItem">
55
35
  <View style={[styles.row, selected && styles.rowSelected]}>
@@ -62,11 +42,6 @@ function RecipientItem({ recipient, onSelectRecipient, loading, selected }: Prop
62
42
  borderColor={Colors.borderPrimary}
63
43
  DefaultIcon={() => renderDefaultIcon(recipient)} // no need to honor color props here since the color we need match the defaults
64
44
  />
65
- {!!showAppIcon && (
66
- <View style={styles.appIcon} testID="RecipientItem/AppIcon">
67
- <Checkmark color={Colors.contentTertiary} height={ICON_SIZE} width={ICON_SIZE} />
68
- </View>
69
- )}
70
45
  </View>
71
46
  <View style={styles.contentContainer}>
72
47
  <Text numberOfLines={1} ellipsizeMode={'tail'} style={styles.name}>
@@ -121,14 +96,6 @@ const styles = StyleSheet.create({
121
96
  justifyContent: 'center',
122
97
  alignItems: 'center',
123
98
  },
124
- appIcon: {
125
- position: 'absolute',
126
- top: 22,
127
- left: 22,
128
- backgroundColor: Colors.accent,
129
- borderRadius: 100,
130
- padding: 2,
131
- },
132
99
  })
133
100
 
134
101
  export default memo(RecipientItem)
@@ -76,15 +76,16 @@ describe('getRecipientVerificationStatus', () => {
76
76
  { recipient: mockRecipient, type: 'with phone number' },
77
77
  ])('address recipient $type', ({ recipient }) => {
78
78
  it('returns appropriate status', () => {
79
- expect(getRecipientVerificationStatus(recipient, {}, { [recipient.address]: true })).toEqual(
79
+ const key = recipient.address.toLowerCase()
80
+ expect(getRecipientVerificationStatus(recipient, {}, { [key]: 'valora' })).toEqual(
80
81
  RecipientVerificationStatus.VERIFIED
81
82
  )
82
- expect(getRecipientVerificationStatus(recipient, {}, { [recipient.address]: false })).toEqual(
83
+ expect(getRecipientVerificationStatus(recipient, {}, { [key]: null })).toEqual(
83
84
  RecipientVerificationStatus.UNVERIFIED
84
85
  )
85
- expect(
86
- getRecipientVerificationStatus(recipient, {}, { [recipient.address]: undefined })
87
- ).toEqual(RecipientVerificationStatus.UNKNOWN)
86
+ expect(getRecipientVerificationStatus(recipient, {}, { [key]: undefined })).toEqual(
87
+ RecipientVerificationStatus.UNKNOWN
88
+ )
88
89
  expect(getRecipientVerificationStatus(recipient, {}, {})).toEqual(
89
90
  RecipientVerificationStatus.UNKNOWN
90
91
  )
@@ -5,7 +5,7 @@ import { formatShortenedAddress } from 'src/account/utils'
5
5
  import {
6
6
  AddressToDisplayNameType,
7
7
  AddressToE164NumberType,
8
- AddressToVerificationStatus,
8
+ AddressToVerifiedByType,
9
9
  E164NumberToAddressType,
10
10
  } from 'src/identity/reducer'
11
11
  import { RecipientVerificationStatus } from 'src/identity/types'
@@ -168,7 +168,7 @@ export function getRecipientFromAddress(
168
168
  export function getRecipientVerificationStatus(
169
169
  recipient: Recipient,
170
170
  e164NumberToAddress: E164NumberToAddressType,
171
- addressToVerificationStatus: AddressToVerificationStatus
171
+ addressToVerifiedBy: AddressToVerifiedByType
172
172
  ): RecipientVerificationStatus {
173
173
  // phone recipients should always have a number, the extra check is to ensure typing
174
174
  if (recipient.recipientType === RecipientType.PhoneNumber && recipientHasNumber(recipient)) {
@@ -183,18 +183,13 @@ export function getRecipientVerificationStatus(
183
183
 
184
184
  return RecipientVerificationStatus.VERIFIED
185
185
  }
186
- if (recipientHasAddress(recipient) && recipient.address in addressToVerificationStatus) {
187
- switch (addressToVerificationStatus[recipient.address]) {
188
- case true:
189
- return RecipientVerificationStatus.VERIFIED
190
- case false:
191
- return RecipientVerificationStatus.UNVERIFIED
192
- case undefined:
193
- return RecipientVerificationStatus.UNKNOWN
194
- }
195
- } else {
186
+ if (recipientHasAddress(recipient)) {
187
+ const entry = addressToVerifiedBy[recipient.address.toLowerCase()]
188
+ if (entry) return RecipientVerificationStatus.VERIFIED
189
+ if (entry === null) return RecipientVerificationStatus.UNVERIFIED
196
190
  return RecipientVerificationStatus.UNKNOWN
197
191
  }
192
+ return RecipientVerificationStatus.UNKNOWN
198
193
  }
199
194
 
200
195
  type PreparedRecipient = Recipient & {
@@ -0,0 +1,20 @@
1
+ import { addressToVerifiedBySelector } from 'src/identity/selectors'
2
+ import { miniPay, valora } from 'src/images/Images'
3
+ import { useSelector } from 'src/redux/hooks'
4
+
5
+ export const VERIFIERS = {
6
+ valora: { name: 'Valora', icon: valora },
7
+ minipay: { name: 'MiniPay', icon: miniPay },
8
+ } as const
9
+
10
+ export type Verifier = keyof typeof VERIFIERS
11
+
12
+ export function isKnownVerifier(verifier: string | null | undefined): verifier is Verifier {
13
+ return !!verifier && Object.hasOwn(VERIFIERS, verifier)
14
+ }
15
+
16
+ export function useVerifierName(address: string | undefined): string | undefined {
17
+ const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
18
+ const verifier = address ? addressToVerifiedBy[address.toLowerCase()] : undefined
19
+ return isKnownVerifier(verifier) ? VERIFIERS[verifier].name : undefined
20
+ }
@@ -69,6 +69,7 @@ import {
69
69
  v253Schema,
70
70
  v254Schema,
71
71
  v255Schema,
72
+ v256Schema,
72
73
  v28Schema,
73
74
  v2Schema,
74
75
  v35Schema,
@@ -1959,4 +1960,28 @@ describe('Redux persist migrations', () => {
1959
1960
  const migratedSchema = migrations[256](oldSchema)
1960
1961
  expect(migratedSchema.identity.secureSendPhoneNumberMapping).toBeUndefined()
1961
1962
  })
1963
+
1964
+ it('works from 256 to 257', () => {
1965
+ const oldSchema = {
1966
+ ...v256Schema,
1967
+ identity: {
1968
+ ...v256Schema.identity,
1969
+ addressToVerificationStatus: {
1970
+ '0xAAA': true,
1971
+ '0xbbb': false,
1972
+ '0xccc': undefined,
1973
+ },
1974
+ addressToVerifiedBy: {
1975
+ '0xddd': 'minipay',
1976
+ },
1977
+ },
1978
+ }
1979
+ const migratedSchema = migrations[257](oldSchema)
1980
+ expect(migratedSchema.identity.addressToVerificationStatus).toBeUndefined()
1981
+ expect(migratedSchema.identity.addressToVerifiedBy).toStrictEqual({
1982
+ '0xddd': 'minipay',
1983
+ '0xaaa': 'valora', // `true` carried over (and lowercased)
1984
+ // `false` and `undefined` entries are dropped — `false` was ambiguous in the old schema
1985
+ })
1986
+ })
1962
1987
  })
@@ -2081,4 +2081,25 @@ export const migrations = {
2081
2081
  ...state,
2082
2082
  identity: _.omit(state.identity, 'secureSendPhoneNumberMapping'),
2083
2083
  }),
2084
+ 257: (state: any) => {
2085
+ // Carry over previously confirmed verifications (all Valora)
2086
+ const oldMap: Record<string, boolean | undefined> =
2087
+ state.identity?.addressToVerificationStatus ?? {}
2088
+ const carriedOver: Record<string, string> = {}
2089
+ for (const [address, verified] of Object.entries(oldMap)) {
2090
+ if (verified === true) {
2091
+ carriedOver[address.toLowerCase()] = 'valora'
2092
+ }
2093
+ }
2094
+ return {
2095
+ ...state,
2096
+ identity: {
2097
+ ..._.omit(state.identity, 'addressToVerificationStatus'),
2098
+ addressToVerifiedBy: {
2099
+ ...state.identity.addressToVerifiedBy,
2100
+ ...carriedOver,
2101
+ },
2102
+ },
2103
+ }
2104
+ },
2084
2105
  }
@@ -143,7 +143,7 @@ describe('store state', () => {
143
143
  {
144
144
  "_persist": {
145
145
  "rehydrated": true,
146
- "version": 256,
146
+ "version": 257,
147
147
  },
148
148
  "account": {
149
149
  "acceptedTerms": false,
@@ -244,7 +244,6 @@ describe('store state', () => {
244
244
  "identity": {
245
245
  "addressToDisplayName": {},
246
246
  "addressToE164Number": {},
247
- "addressToVerificationStatus": {},
248
247
  "addressToVerifiedBy": {},
249
248
  "askedContactsPermission": false,
250
249
  "e164NumberToAddress": {},
@@ -30,7 +30,7 @@ const persistConfig: PersistConfig<ReducersRootState> = {
30
30
  key: 'root',
31
31
  // default is -1, increment as we make migrations
32
32
  // See https://github.com/valora-xyz/wallet/tree/main/WALLET.md#redux-state-migration
33
- version: 256,
33
+ version: 257,
34
34
  keyPrefix: `reduxStore-`, // the redux-persist default is `persist:` which doesn't work with some file systems.
35
35
  storage: FSStorage(),
36
36
  blacklist: ['networkInfo', 'alert', 'imports', 'keylessBackup', transactionFeedV2Api.reducerPath],