wallet-stack 1.0.0-alpha.132 → 1.0.0-alpha.134

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.
Files changed (64) hide show
  1. package/locales/base/translation.json +5 -0
  2. package/package.json +1 -2
  3. package/src/analytics/Events.tsx +3 -10
  4. package/src/analytics/Properties.tsx +9 -25
  5. package/src/analytics/docs.ts +11 -8
  6. package/src/app/ErrorMessages.ts +0 -7
  7. package/src/components/ReviewTransaction.test.tsx +49 -8
  8. package/src/components/ReviewTransaction.tsx +58 -12
  9. package/src/icons/VerifiedBadge.tsx +27 -0
  10. package/src/identity/actions.ts +1 -114
  11. package/src/identity/contactMapping.test.ts +80 -46
  12. package/src/identity/contactMapping.ts +46 -102
  13. package/src/identity/reducer.ts +7 -98
  14. package/src/identity/saga.ts +2 -85
  15. package/src/identity/selectors.ts +0 -4
  16. package/src/images/Images.ts +2 -0
  17. package/src/images/assets/minipay.png +0 -0
  18. package/src/images/assets/minipay@1.5x.png +0 -0
  19. package/src/images/assets/minipay@2x.png +0 -0
  20. package/src/images/assets/minipay@3x.png +0 -0
  21. package/src/images/assets/minipay@4x.png +0 -0
  22. package/src/images/assets/valora.png +0 -0
  23. package/src/images/assets/valora@1.5x.png +0 -0
  24. package/src/images/assets/valora@2x.png +0 -0
  25. package/src/images/assets/valora@3x.png +0 -0
  26. package/src/images/assets/valora@4x.png +0 -0
  27. package/src/index.d.ts +0 -1
  28. package/src/navigator/Navigator.tsx +4 -14
  29. package/src/navigator/Screens.tsx +1 -2
  30. package/src/navigator/types.tsx +4 -6
  31. package/src/qrcode/utils.test.tsx +4 -96
  32. package/src/qrcode/utils.ts +5 -114
  33. package/src/recipients/RecipientItemV2.test.tsx +11 -72
  34. package/src/recipients/RecipientItemV2.tsx +1 -34
  35. package/src/recipients/recipient.test.ts +6 -5
  36. package/src/recipients/recipient.ts +7 -12
  37. package/src/recipients/verifier.ts +20 -0
  38. package/src/redux/migrations.test.ts +38 -0
  39. package/src/redux/migrations.ts +25 -0
  40. package/src/redux/store.test.ts +1 -3
  41. package/src/redux/store.ts +1 -1
  42. package/src/send/SelectRecipientAddress.test.tsx +146 -0
  43. package/src/send/SelectRecipientAddress.tsx +164 -0
  44. package/src/send/SendConfirmation.test.tsx +3 -3
  45. package/src/send/SendConfirmation.tsx +3 -3
  46. package/src/send/SendSelectRecipient.test.tsx +53 -138
  47. package/src/send/SendSelectRecipient.tsx +12 -27
  48. package/src/send/actions.ts +0 -26
  49. package/src/send/saga.ts +1 -6
  50. package/src/send/useFetchRecipientVerificationStatus.ts +6 -11
  51. package/src/transactions/UserSection.tsx +37 -11
  52. package/src/transactions/feed/TransactionDetailsScreen.test.tsx +6 -6
  53. package/src/utils/phoneNumbers.ts +0 -11
  54. package/src/components/AccountNumberCard.tsx +0 -23
  55. package/src/components/ErrorMessageInline.tsx +0 -78
  56. package/src/components/SingleDigitInput.tsx +0 -53
  57. package/src/icons/HamburgerCard.tsx +0 -55
  58. package/src/identity/saga.test.ts +0 -103
  59. package/src/identity/secureSend.ts +0 -171
  60. package/src/send/ValidateRecipientAccount.test.tsx +0 -182
  61. package/src/send/ValidateRecipientAccount.tsx +0 -392
  62. package/src/send/ValidateRecipientIntro.test.tsx +0 -61
  63. package/src/send/ValidateRecipientIntro.tsx +0 -136
  64. package/src/send/__snapshots__/ValidateRecipientAccount.test.tsx.snap +0 -777
@@ -6,17 +6,15 @@ import { call, select } from 'redux-saga/effects'
6
6
  import { setUserContactDetails } from 'src/account/actions'
7
7
  import { defaultCountryCodeSelector, e164NumberSelector } from 'src/account/selectors'
8
8
  import { showError, showErrorOrFallback } from 'src/alert/actions'
9
- import { IdentityEvents } from 'src/analytics/Events'
10
9
  import AppAnalytics from 'src/analytics/AppAnalytics'
10
+ import { IdentityEvents } from 'src/analytics/Events'
11
11
  import { ErrorMessages } from 'src/app/ErrorMessages'
12
12
  import { phoneNumberVerifiedSelector } from 'src/app/selectors'
13
13
  import {
14
14
  Actions,
15
- addressVerificationStatusReceived,
16
15
  contactsSaved,
17
16
  fetchAddressVerification,
18
17
  fetchAddressesAndValidate,
19
- requireSecureSend,
20
18
  updateE164PhoneNumberAddresses,
21
19
  } from 'src/identity/actions'
22
20
  import {
@@ -25,12 +23,11 @@ import {
25
23
  fetchAddressesAndValidateSaga,
26
24
  saveContacts,
27
25
  } from 'src/identity/contactMapping'
28
- import { AddressValidationType } from 'src/identity/reducer'
29
26
  import {
30
- addressToVerificationStatusSelector,
27
+ addressToE164NumberSelector,
28
+ addressToVerifiedBySelector,
31
29
  e164NumberToAddressSelector,
32
30
  lastSavedContactsHashSelector,
33
- secureSendPhoneNumberMappingSelector,
34
31
  } from 'src/identity/selectors'
35
32
  import { retrieveSignedMessage } from 'src/pincode/authentication'
36
33
  import { contactsToRecipients } from 'src/recipients/recipient'
@@ -97,19 +94,21 @@ describe('Fetch Addresses Saga', () => {
97
94
  mockFetch.resetMocks()
98
95
  })
99
96
 
97
+ const emptyMappingProviders: [any, any][] = [
98
+ [select(e164NumberToAddressSelector), {}],
99
+ [select(addressToE164NumberSelector), {}],
100
+ [select(addressToVerifiedBySelector), {}],
101
+ ]
102
+
100
103
  it('fetches and caches addresses correctly', async () => {
101
- const mockE164NumberToAddress = {
102
- [mockE164Number]: [mockAccount.toLowerCase()],
103
- }
104
104
  const updatedAccount = '0xAbC'
105
105
  mockFetch.mockResponseOnce(JSON.stringify({ data: { addresses: [updatedAccount] } }))
106
106
 
107
107
  await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
108
108
  .provide([
109
- [select(e164NumberToAddressSelector), mockE164NumberToAddress],
109
+ ...emptyMappingProviders,
110
110
  [select(walletAddressSelector), '0xxyz'],
111
111
  [call(retrieveSignedMessage), 'some signed message'],
112
- [select(secureSendPhoneNumberMappingSelector), {}],
113
112
  ])
114
113
  .put(updateE164PhoneNumberAddresses({ [mockE164Number]: undefined }, {}))
115
114
  .put(
@@ -135,18 +134,14 @@ describe('Fetch Addresses Saga', () => {
135
134
  })
136
135
 
137
136
  it('fetches and caches multiple addresses correctly', async () => {
138
- const mockE164NumberToAddress = {
139
- [mockE164Number]: [mockAccount.toLowerCase()],
140
- }
141
137
  const updatedAccounts = ['0xAbC', '0xdef']
142
138
  mockFetch.mockResponseOnce(JSON.stringify({ data: { addresses: updatedAccounts } }))
143
139
 
144
140
  await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
145
141
  .provide([
146
- [select(e164NumberToAddressSelector), mockE164NumberToAddress],
147
- [select(walletAddressSelector), mockAccount],
142
+ ...emptyMappingProviders,
143
+ [select(walletAddressSelector), '0xxyz'],
148
144
  [call(retrieveSignedMessage), 'some signed message'],
149
- [select(secureSendPhoneNumberMappingSelector), {}],
150
145
  ])
151
146
  .put(updateE164PhoneNumberAddresses({ [mockE164Number]: undefined }, {}))
152
147
  .put(
@@ -156,14 +151,10 @@ describe('Fetch Addresses Saga', () => {
156
151
  {}
157
152
  )
158
153
  )
159
- .put(requireSecureSend(mockE164Number, AddressValidationType.PARTIAL))
160
154
  .run()
161
155
  })
162
156
 
163
157
  it('uses verifiedAddresses as source of truth when present', async () => {
164
- const mockE164NumberToAddress = {
165
- [mockE164Number]: [mockAccount.toLowerCase()],
166
- }
167
158
  // addresses only contains DB-verified addresses (backward compat),
168
159
  // verifiedAddresses contains all (DB + SC) and is the source of truth
169
160
  mockFetch.mockResponseOnce(
@@ -180,10 +171,9 @@ describe('Fetch Addresses Saga', () => {
180
171
 
181
172
  await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
182
173
  .provide([
183
- [select(e164NumberToAddressSelector), mockE164NumberToAddress],
184
- [select(walletAddressSelector), mockAccount],
174
+ ...emptyMappingProviders,
175
+ [select(walletAddressSelector), '0xxyz'],
185
176
  [call(retrieveSignedMessage), 'some signed message'],
186
- [select(secureSendPhoneNumberMappingSelector), {}],
187
177
  ])
188
178
  .put(updateE164PhoneNumberAddresses({ [mockE164Number]: undefined }, {}))
189
179
  .put(
@@ -193,20 +183,48 @@ describe('Fetch Addresses Saga', () => {
193
183
  { '0xabc': 'valora', '0xdef': 'minipay' }
194
184
  )
195
185
  )
196
- .put(requireSecureSend(mockE164Number, AddressValidationType.PARTIAL))
186
+ .run()
187
+ })
188
+
189
+ it('prunes stale address mappings no longer returned by the fresh lookup', async () => {
190
+ mockFetch.mockResponseOnce(
191
+ JSON.stringify({
192
+ data: {
193
+ addresses: ['0xkept'],
194
+ verifiedAddresses: [{ address: '0xkept', verifiedBy: 'valora' }],
195
+ },
196
+ })
197
+ )
198
+
199
+ await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
200
+ .provide([
201
+ [select(e164NumberToAddressSelector), { [mockE164Number]: ['0xstale', '0xkept'] }],
202
+ [
203
+ select(addressToE164NumberSelector),
204
+ { '0xstale': mockE164Number, '0xkept': mockE164Number },
205
+ ],
206
+ [select(addressToVerifiedBySelector), { '0xstale': 'valora', '0xkept': 'valora' }],
207
+ [select(walletAddressSelector), '0xxyz'],
208
+ [call(retrieveSignedMessage), 'some signed message'],
209
+ ])
210
+ .put(updateE164PhoneNumberAddresses({ [mockE164Number]: undefined }, {}))
211
+ .put(
212
+ updateE164PhoneNumberAddresses(
213
+ { [mockE164Number]: ['0xkept'] },
214
+ { '0xstale': null, '0xkept': mockE164Number },
215
+ { '0xstale': null, '0xkept': 'valora' }
216
+ )
217
+ )
197
218
  .run()
198
219
  })
199
220
 
200
221
  it('handles lookup errors correctly', async () => {
201
- const mockE164NumberToAddress = {
202
- [mockE164Number]: [mockAccount.toLowerCase()],
203
- }
204
222
  mockFetch.mockReject()
205
223
 
206
224
  await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
207
225
  .provide([
208
- [select(e164NumberToAddressSelector), mockE164NumberToAddress],
209
- [select(walletAddressSelector), mockAccount],
226
+ ...emptyMappingProviders,
227
+ [select(walletAddressSelector), '0xxyz'],
210
228
  [call(retrieveSignedMessage), 'some signed message'],
211
229
  ])
212
230
  .put(showErrorOrFallback(expect.anything(), ErrorMessages.ADDRESS_LOOKUP_FAILURE))
@@ -220,48 +238,64 @@ describe('Fetch Address Verification Saga', () => {
220
238
  mockFetch.resetMocks()
221
239
  })
222
240
 
223
- it('fetches and stores verified address', async () => {
224
- mockFetch.mockResponseOnce(JSON.stringify({ data: { addressVerified: true } }))
241
+ it('records the `verifiedBy` value returned by the backend', async () => {
242
+ mockFetch.mockResponseOnce(
243
+ JSON.stringify({ data: { addressVerified: true, verifiedBy: 'minipay' } })
244
+ )
225
245
 
226
246
  await expectSaga(fetchAddressVerificationSaga, fetchAddressVerification(mockAccount))
227
247
  .provide([
228
- [select(addressToVerificationStatusSelector), {}],
229
248
  [select(walletAddressSelector), '0xxyz'],
230
249
  [call(retrieveSignedMessage), 'some signed message'],
231
250
  ])
232
- .put(addressVerificationStatusReceived(mockAccount, true))
251
+ .put(updateE164PhoneNumberAddresses({}, {}, { [mockAccount.toLowerCase()]: 'minipay' }))
233
252
  .run()
234
253
 
235
254
  expect(mockFetch).toHaveBeenCalledTimes(1)
236
255
  expect(mockFetch).toHaveBeenCalledWith(
237
- `${networkConfig.checkAddressVerifiedUrl}?address=${mockAccount}&clientPlatform=android&clientVersion=0.0.1`,
238
- {
256
+ `${networkConfig.checkAddressVerifiedUrl}?address=${mockAccount.toLowerCase()}&clientPlatform=android&clientVersion=0.0.1`,
257
+ expect.objectContaining({
239
258
  method: 'GET',
240
- headers: {
241
- 'Content-Type': 'application/json',
259
+ headers: expect.objectContaining({
242
260
  authorization: `${networkConfig.authHeaderIssuer} 0xxyz:some signed message`,
243
- },
244
- signal: expect.any(AbortSignal),
245
- }
261
+ }),
262
+ })
246
263
  )
247
264
  })
248
265
 
249
- it('skips fetching if address already known', async () => {
266
+ it('falls back to `valora` when the backend confirms the address without a `verifiedBy` field', async () => {
267
+ mockFetch.mockResponseOnce(JSON.stringify({ data: { addressVerified: true } }))
268
+
250
269
  await expectSaga(fetchAddressVerificationSaga, fetchAddressVerification(mockAccount))
251
- .provide([[select(addressToVerificationStatusSelector), { [mockAccount]: true }]])
270
+ .provide([
271
+ [select(walletAddressSelector), '0xxyz'],
272
+ [call(retrieveSignedMessage), 'some signed message'],
273
+ ])
274
+ .put(updateE164PhoneNumberAddresses({}, {}, { [mockAccount.toLowerCase()]: 'valora' }))
252
275
  .run()
276
+ })
253
277
 
254
- expect(mockFetch).toHaveBeenCalledTimes(0)
278
+ it('records `null` (checked, not verified) when the backend returns false', async () => {
279
+ mockFetch.mockResponseOnce(JSON.stringify({ data: { addressVerified: false } }))
280
+
281
+ await expectSaga(fetchAddressVerificationSaga, fetchAddressVerification(mockAccount))
282
+ .provide([
283
+ [select(walletAddressSelector), '0xxyz'],
284
+ [call(retrieveSignedMessage), 'some signed message'],
285
+ ])
286
+ .put(updateE164PhoneNumberAddresses({}, {}, { [mockAccount.toLowerCase()]: null }))
287
+ .run()
255
288
  })
256
289
 
257
- it('handles errors gracefully', async () => {
290
+ it('does not touch `addressToVerifiedBy` on network errors — the check is inconclusive, not negative', async () => {
258
291
  mockFetch.mockReject()
259
292
  await expectSaga(fetchAddressVerificationSaga, fetchAddressVerification(mockAccount))
260
293
  .provide([
261
- [select(addressToVerificationStatusSelector), {}],
262
294
  [select(walletAddressSelector), '0xxyz'],
263
295
  [call(retrieveSignedMessage), 'some signed message'],
264
296
  ])
297
+ .not.put.actionType(Actions.UPDATE_E164_PHONE_NUMBER_ADDRESSES)
298
+ .put(showErrorOrFallback(expect.anything(), ErrorMessages.ADDRESS_LOOKUP_FAILURE))
265
299
  .run()
266
300
  expect(AppAnalytics.track).toHaveBeenCalledWith(IdentityEvents.address_lookup_error, {
267
301
  error: 'Unable to fetch verification status for this address',
@@ -11,26 +11,21 @@ import {
11
11
  Actions,
12
12
  FetchAddressVerificationAction,
13
13
  FetchAddressesAndValidateAction,
14
- addressVerificationStatusReceived,
15
14
  contactsSaved,
16
- endFetchingAddresses,
17
15
  endImportContacts,
18
- requireSecureSend,
19
16
  updateE164PhoneNumberAddresses,
20
17
  updateImportContactsProgress,
21
18
  } from 'src/identity/actions'
22
19
  import {
23
20
  AddressToE164NumberType,
24
- AddressValidationType,
21
+ AddressToVerifiedByType,
25
22
  E164NumberToAddressType,
26
- SecureSendPhoneNumberMapping,
27
23
  } from 'src/identity/reducer'
28
- import { checkIfValidationRequired } from 'src/identity/secureSend'
29
24
  import {
30
- addressToVerificationStatusSelector,
25
+ addressToE164NumberSelector,
26
+ addressToVerifiedBySelector,
31
27
  e164NumberToAddressSelector,
32
28
  lastSavedContactsHashSelector,
33
- secureSendPhoneNumberMappingSelector,
34
29
  } from 'src/identity/selectors'
35
30
  import { ImportContactsStatus } from 'src/identity/types'
36
31
  import { retrieveSignedMessage } from 'src/pincode/authentication'
@@ -144,17 +139,17 @@ function* updateUserContact(e164NumberToRecipients: NumberToRecipient) {
144
139
  yield* put(setUserContactDetails(userRecipient.contactId, userRecipient.thumbnailPath || null))
145
140
  }
146
141
 
147
- export function* fetchAddressesAndValidateSaga({
148
- e164Number,
149
- requesterAddress,
150
- }: FetchAddressesAndValidateAction) {
142
+ export function* fetchAddressesAndValidateSaga({ e164Number }: FetchAddressesAndValidateAction) {
151
143
  AppAnalytics.track(IdentityEvents.phone_number_lookup_start)
152
144
  try {
153
145
  Logger.debug(TAG + '@fetchAddressesAndValidate', `Fetching addresses for number`)
154
- const oldE164NumberToAddress: E164NumberToAddressType = yield* select(
155
- e164NumberToAddressSelector
156
- )
157
- const oldAddresses = oldE164NumberToAddress[e164Number] || []
146
+
147
+ // Snapshot the previous mappings before we clear them so we can prune stale entries
148
+ // after the fresh response arrives (see the pruning block below).
149
+ const prevE164NumberToAddress = yield* select(e164NumberToAddressSelector)
150
+ const prevAddressToE164Number = yield* select(addressToE164NumberSelector)
151
+ const prevAddressToVerifiedBy = yield* select(addressToVerifiedBySelector)
152
+ const prevAddresses = prevE164NumberToAddress[e164Number] ?? []
158
153
 
159
154
  // Clear existing entries for those numbers so our mapping consumers know new status is pending.
160
155
  yield* put(updateE164PhoneNumberAddresses({ [e164Number]: undefined }, {}))
@@ -165,10 +160,26 @@ export function* fetchAddressesAndValidateSaga({
165
160
  // it includes addresses verified by both CPV and SocialConnect.
166
161
  // The `addresses` field is used for backward compatibility.
167
162
  const walletAddresses = verifiedAddresses ? verifiedAddresses.map((v) => v.address) : addresses
163
+ const walletAddressSet = new Set(walletAddresses)
168
164
 
169
165
  const e164NumberToAddressUpdates: E164NumberToAddressType = {}
170
166
  const addressToE164NumberUpdates: AddressToE164NumberType = {}
171
- const addressToVerifiedByUpdates: Record<string, string> = {}
167
+ const addressToVerifiedByUpdates: AddressToVerifiedByType = {}
168
+
169
+ // Prune addresses previously associated with this phone number but no longer present
170
+ // in the fresh response.
171
+ for (const prevAddress of prevAddresses) {
172
+ if (!walletAddressSet.has(prevAddress)) {
173
+ // Clear the reverse mapping for this number.
174
+ if (prevAddressToE164Number[prevAddress] === e164Number) {
175
+ addressToE164NumberUpdates[prevAddress] = null
176
+ }
177
+ // Clear verifier info.
178
+ if (prevAddress in prevAddressToVerifiedBy) {
179
+ addressToVerifiedByUpdates[prevAddress] = null
180
+ }
181
+ }
182
+ }
172
183
 
173
184
  if (verifiedAddresses) {
174
185
  for (const { address, verifiedBy } of verifiedAddresses) {
@@ -186,28 +197,6 @@ export function* fetchAddressesAndValidateSaga({
186
197
  walletAddresses.map((a) => (addressToE164NumberUpdates[a] = e164Number))
187
198
  }
188
199
 
189
- const userAddress = yield* select(walletAddressSelector)
190
- if (!userAddress) {
191
- throw new Error('Wallet address not set')
192
- }
193
- const secureSendPossibleAddresses = [...walletAddresses]
194
- const secureSendPhoneNumberMapping = yield* select(secureSendPhoneNumberMappingSelector)
195
- // If fetch is being done as part of a payment request from an unverified address,
196
- // the unverified address should be considered in the Secure Send check
197
- if (requesterAddress && !secureSendPossibleAddresses.includes(requesterAddress)) {
198
- secureSendPossibleAddresses.push(requesterAddress)
199
- }
200
-
201
- const addressValidationType = checkIfValidationRequired(
202
- oldAddresses,
203
- secureSendPossibleAddresses,
204
- userAddress,
205
- secureSendPhoneNumberMapping,
206
- e164Number
207
- )
208
- if (addressValidationType !== AddressValidationType.NONE) {
209
- yield* put(requireSecureSend(e164Number, addressValidationType))
210
- }
211
200
  yield* put(
212
201
  updateE164PhoneNumberAddresses(
213
202
  e164NumberToAddressUpdates,
@@ -215,13 +204,11 @@ export function* fetchAddressesAndValidateSaga({
215
204
  addressToVerifiedByUpdates
216
205
  )
217
206
  )
218
- yield* put(endFetchingAddresses(e164Number, true))
219
207
  AppAnalytics.track(IdentityEvents.phone_number_lookup_complete)
220
208
  } catch (err) {
221
209
  const error = ensureError(err)
222
210
  Logger.debug(TAG + '@fetchAddressesAndValidate', `Error fetching addresses`, error)
223
211
  yield* put(showErrorOrFallback(error, ErrorMessages.ADDRESS_LOOKUP_FAILURE))
224
- yield* put(endFetchingAddresses(e164Number, false))
225
212
  AppAnalytics.track(IdentityEvents.phone_number_lookup_error, {
226
213
  error: error.message,
227
214
  })
@@ -229,14 +216,20 @@ export function* fetchAddressesAndValidateSaga({
229
216
  }
230
217
 
231
218
  export function* fetchAddressVerificationSaga({ address }: FetchAddressVerificationAction) {
219
+ const normalizedAddress = address.toLowerCase()
232
220
  try {
233
- const addressToVerificationStatus = yield* select(addressToVerificationStatusSelector)
234
- if (!(address in addressToVerificationStatus && addressToVerificationStatus[address])) {
235
- AppAnalytics.track(IdentityEvents.address_lookup_start)
236
- const addressVerified = yield* call(fetchAddressVerification, address)
237
- yield* put(addressVerificationStatusReceived(address, addressVerified))
238
- AppAnalytics.track(IdentityEvents.address_lookup_complete)
239
- }
221
+ AppAnalytics.track(IdentityEvents.address_lookup_start)
222
+ const { addressVerified, verifiedBy } = yield* call(fetchAddressVerification, normalizedAddress)
223
+ // Older backend responses omit `verifiedBy` and only signal Valora-verified addresses,
224
+ // so fall back to 'valora' when verification is confirmed without a verifier.
225
+ yield* put(
226
+ updateE164PhoneNumberAddresses(
227
+ {},
228
+ {},
229
+ { [normalizedAddress]: addressVerified ? (verifiedBy ?? 'valora') : null }
230
+ )
231
+ )
232
+ AppAnalytics.track(IdentityEvents.address_lookup_complete)
240
233
  } catch (err) {
241
234
  const error = ensureError(err)
242
235
  Logger.debug(
@@ -244,13 +237,8 @@ export function* fetchAddressVerificationSaga({ address }: FetchAddressVerificat
244
237
  `Error fetching address verification`,
245
238
  error
246
239
  )
247
- AppAnalytics.track(IdentityEvents.address_lookup_error, {
248
- error: error.message,
249
- })
250
- // Setting this address to "false" does not mean that the address
251
- // if definitely unverified; we set it to false to indicate that
252
- // the request is finished, and possibly unverified.
253
- yield* put(addressVerificationStatusReceived(address, false))
240
+ yield* put(showErrorOrFallback(error, ErrorMessages.ADDRESS_LOOKUP_FAILURE))
241
+ AppAnalytics.track(IdentityEvents.address_lookup_error, { error: error.message })
254
242
  }
255
243
  }
256
244
 
@@ -332,59 +320,15 @@ function* fetchAddressVerification(address: string) {
332
320
  )
333
321
  }
334
322
 
335
- const { data }: { data: { addressVerified: boolean } } = yield* call([response, 'json'])
336
- return data.addressVerified
323
+ const { data }: { data: { addressVerified: boolean; verifiedBy?: string | null } } =
324
+ yield* call([response, 'json'])
325
+ return { addressVerified: data.addressVerified, verifiedBy: data.verifiedBy }
337
326
  } catch (error) {
338
327
  Logger.warn(`${TAG}/fetchAddressVerification`, 'Unable to look up address', error)
339
328
  throw new Error('Unable to fetch verification status for this address')
340
329
  }
341
330
  }
342
331
 
343
- // Only use with multiple addresses if user has
344
- // gone through SecureSend
345
- export function getAddressFromPhoneNumber(
346
- e164Number: string,
347
- e164NumberToAddress: E164NumberToAddressType,
348
- secureSendPhoneNumberMapping: SecureSendPhoneNumberMapping,
349
- requesterAddress?: string
350
- ): string | null | undefined {
351
- const addresses = e164NumberToAddress[e164Number]
352
-
353
- // If there are no verified addresses for the number,
354
- // use the requester's given address
355
- if (!addresses && requesterAddress) {
356
- return requesterAddress
357
- }
358
-
359
- // If address is null (unverified) or undefined (in the process
360
- // of being updated) then just return that falsy value
361
- if (!addresses) {
362
- return addresses
363
- }
364
-
365
- // If there are multiple addresses, need to determine which to use
366
- if (addresses.length > 1) {
367
- // Check if the user has gone through Secure Send and validated a
368
- // recipient address
369
- const validatedAddress = secureSendPhoneNumberMapping[e164Number]
370
- ? secureSendPhoneNumberMapping[e164Number].address
371
- : undefined
372
-
373
- // If they have not, they shouldn't have been able to
374
- // get to this point
375
- if (!validatedAddress) {
376
- throw new Error(
377
- 'Multiple addresses but none were validated. Should have routed through Secure Send.'
378
- )
379
- }
380
-
381
- return validatedAddress
382
- }
383
-
384
- // Normal case when there is only one address in the mapping
385
- return addresses[0]
386
- }
387
-
388
332
  export function* saveContacts() {
389
333
  try {
390
334
  const saveContactsGate = getFeatureGate(StatsigFeatureGates.SAVE_CONTACTS)
@@ -1,4 +1,3 @@
1
- import dotProp from 'dot-prop-immutable'
2
1
  import { RehydrateAction } from 'redux-persist'
3
2
  import { Actions as AccountActions, ClearStoredAccountAction } from 'src/account/actions'
4
3
  import { ActionTypes, Actions } from 'src/identity/actions'
@@ -32,30 +31,11 @@ export interface ImportContactProgress {
32
31
  total: number
33
32
  }
34
33
 
35
- export enum AddressValidationType {
36
- FULL = 'full',
37
- PARTIAL = 'partial',
38
- NONE = 'none',
39
- }
40
-
41
- export interface SecureSendPhoneNumberMapping {
42
- [e164Number: string]: SecureSendDetails
43
- }
44
-
45
- export interface SecureSendDetails {
46
- address?: string
47
- addressValidationType: AddressValidationType
48
- isFetchingAddresses?: boolean
49
- lastFetchSuccessful?: boolean
50
- validationSuccessful?: boolean
51
- }
52
-
53
- export interface AddressToVerificationStatus {
54
- [address: string]: boolean | undefined
55
- }
56
-
57
34
  export interface AddressToVerifiedByType {
58
- [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
59
39
  }
60
40
 
61
41
  interface State {
@@ -67,10 +47,6 @@ interface State {
67
47
  // Has the user already been asked for contacts permission
68
48
  askedContactsPermission: boolean
69
49
  importContactsProgress: ImportContactProgress
70
- // Contacts found during the matchmaking process
71
- secureSendPhoneNumberMapping: SecureSendPhoneNumberMapping
72
- // Mapping of address to verification status; undefined entries represent a loading state
73
- addressToVerificationStatus: AddressToVerificationStatus
74
50
  // Mapping of address to the entity that verified it (e.g. "valora", "minipay")
75
51
  addressToVerifiedBy: AddressToVerifiedByType
76
52
  lastSavedContactsHash: string | null
@@ -87,8 +63,6 @@ const initialState: State = {
87
63
  current: 0,
88
64
  total: 0,
89
65
  },
90
- secureSendPhoneNumberMapping: {},
91
- addressToVerificationStatus: {},
92
66
  addressToVerifiedBy: {},
93
67
  lastSavedContactsHash: null,
94
68
  shouldRefreshStoredPasswordHash: false,
@@ -159,83 +133,18 @@ export const reducer = (
159
133
  status: success ? ImportContactsStatus.Done : ImportContactsStatus.Failed,
160
134
  },
161
135
  }
162
- case Actions.VALIDATE_RECIPIENT_ADDRESS_SUCCESS:
163
- return {
164
- ...state,
165
- // Overwrite the previous mapping when a new address is validated
166
- secureSendPhoneNumberMapping: dotProp.set(
167
- state.secureSendPhoneNumberMapping,
168
- `${action.e164Number}`,
169
- {
170
- address: action.validatedAddress,
171
- addressValidationType: AddressValidationType.NONE,
172
- validationSuccessful: true,
173
- }
174
- ),
175
- }
176
- case Actions.VALIDATE_RECIPIENT_ADDRESS_RESET:
177
- return {
178
- ...state,
179
- secureSendPhoneNumberMapping: dotProp.set(
180
- state.secureSendPhoneNumberMapping,
181
- `${action.e164Number}.validationSuccessful`,
182
- false
183
- ),
184
- }
185
- case Actions.REQUIRE_SECURE_SEND:
186
- return {
187
- ...state,
188
- // Erase the previous mapping when new validation is required
189
- secureSendPhoneNumberMapping: dotProp.set(
190
- state.secureSendPhoneNumberMapping,
191
- `${action.e164Number}`,
192
- {
193
- address: undefined,
194
- addressValidationType: action.addressValidationType,
195
- }
196
- ),
197
- }
198
- case Actions.FETCH_ADDRESSES_AND_VALIDATION_STATUS:
199
- return {
200
- ...state,
201
- secureSendPhoneNumberMapping: dotProp.set(
202
- state.secureSendPhoneNumberMapping,
203
- `${action.e164Number}.isFetchingAddresses`,
204
- true
205
- ),
206
- }
207
- case Actions.END_FETCHING_ADDRESSES:
208
- return {
209
- ...state,
210
- secureSendPhoneNumberMapping: dotProp.merge(
211
- state.secureSendPhoneNumberMapping,
212
- `${action.e164Number}`,
213
- { isFetchingAddresses: false, lastFetchSuccessful: action.lastFetchSuccessful }
214
- ),
215
- }
216
136
  case AccountActions.CLEAR_STORED_ACCOUNT:
217
137
  return {
218
138
  ...initialState,
219
139
  addressToE164Number: state.addressToE164Number,
220
140
  e164NumberToAddress: state.e164NumberToAddress,
221
- secureSendPhoneNumberMapping: state.secureSendPhoneNumberMapping,
222
141
  }
223
142
  case Actions.FETCH_ADDRESS_VERIFICATION_STATUS:
224
- // If the current status is false or does not exist, we set it to undefined
225
- // to mark it as being in a loading state.
226
143
  return {
227
144
  ...state,
228
- addressToVerificationStatus: {
229
- ...state.addressToVerificationStatus,
230
- [action.address]: state.addressToVerificationStatus[action.address] || undefined,
231
- },
232
- }
233
- case Actions.ADDRESS_VERIFICATION_STATUS_RECEIVED:
234
- return {
235
- ...state,
236
- addressToVerificationStatus: {
237
- ...state.addressToVerificationStatus,
238
- [action.address]: action.addressVerified,
145
+ addressToVerifiedBy: {
146
+ ...state.addressToVerifiedBy,
147
+ [action.address]: undefined,
239
148
  },
240
149
  }
241
150
  case Actions.CONTACTS_SAVED: