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.
- package/locales/base/translation.json +5 -0
- package/package.json +1 -2
- package/src/analytics/Events.tsx +3 -10
- package/src/analytics/Properties.tsx +9 -25
- package/src/analytics/docs.ts +11 -8
- package/src/app/ErrorMessages.ts +0 -7
- package/src/components/ReviewTransaction.test.tsx +49 -8
- package/src/components/ReviewTransaction.tsx +58 -12
- package/src/icons/VerifiedBadge.tsx +27 -0
- package/src/identity/actions.ts +1 -114
- package/src/identity/contactMapping.test.ts +80 -46
- package/src/identity/contactMapping.ts +46 -102
- package/src/identity/reducer.ts +7 -98
- package/src/identity/saga.ts +2 -85
- package/src/identity/selectors.ts +0 -4
- package/src/images/Images.ts +2 -0
- package/src/images/assets/minipay.png +0 -0
- package/src/images/assets/minipay@1.5x.png +0 -0
- package/src/images/assets/minipay@2x.png +0 -0
- package/src/images/assets/minipay@3x.png +0 -0
- package/src/images/assets/minipay@4x.png +0 -0
- package/src/images/assets/valora.png +0 -0
- package/src/images/assets/valora@1.5x.png +0 -0
- package/src/images/assets/valora@2x.png +0 -0
- package/src/images/assets/valora@3x.png +0 -0
- package/src/images/assets/valora@4x.png +0 -0
- package/src/index.d.ts +0 -1
- package/src/navigator/Navigator.tsx +4 -14
- package/src/navigator/Screens.tsx +1 -2
- package/src/navigator/types.tsx +4 -6
- package/src/qrcode/utils.test.tsx +4 -96
- package/src/qrcode/utils.ts +5 -114
- package/src/recipients/RecipientItemV2.test.tsx +11 -72
- package/src/recipients/RecipientItemV2.tsx +1 -34
- package/src/recipients/recipient.test.ts +6 -5
- package/src/recipients/recipient.ts +7 -12
- package/src/recipients/verifier.ts +20 -0
- package/src/redux/migrations.test.ts +38 -0
- package/src/redux/migrations.ts +25 -0
- package/src/redux/store.test.ts +1 -3
- package/src/redux/store.ts +1 -1
- package/src/send/SelectRecipientAddress.test.tsx +146 -0
- package/src/send/SelectRecipientAddress.tsx +164 -0
- package/src/send/SendConfirmation.test.tsx +3 -3
- package/src/send/SendConfirmation.tsx +3 -3
- package/src/send/SendSelectRecipient.test.tsx +53 -138
- package/src/send/SendSelectRecipient.tsx +12 -27
- package/src/send/actions.ts +0 -26
- package/src/send/saga.ts +1 -6
- package/src/send/useFetchRecipientVerificationStatus.ts +6 -11
- package/src/transactions/UserSection.tsx +37 -11
- package/src/transactions/feed/TransactionDetailsScreen.test.tsx +6 -6
- package/src/utils/phoneNumbers.ts +0 -11
- package/src/components/AccountNumberCard.tsx +0 -23
- package/src/components/ErrorMessageInline.tsx +0 -78
- package/src/components/SingleDigitInput.tsx +0 -53
- package/src/icons/HamburgerCard.tsx +0 -55
- package/src/identity/saga.test.ts +0 -103
- package/src/identity/secureSend.ts +0 -171
- package/src/send/ValidateRecipientAccount.test.tsx +0 -182
- package/src/send/ValidateRecipientAccount.tsx +0 -392
- package/src/send/ValidateRecipientIntro.test.tsx +0 -61
- package/src/send/ValidateRecipientIntro.tsx +0 -136
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
[select(walletAddressSelector),
|
|
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
|
-
|
|
184
|
-
[select(walletAddressSelector),
|
|
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
|
-
.
|
|
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
|
-
|
|
209
|
-
[select(walletAddressSelector),
|
|
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('
|
|
224
|
-
mockFetch.mockResponseOnce(
|
|
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(
|
|
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
|
-
|
|
245
|
-
}
|
|
261
|
+
}),
|
|
262
|
+
})
|
|
246
263
|
)
|
|
247
264
|
})
|
|
248
265
|
|
|
249
|
-
it('
|
|
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([
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
21
|
+
AddressToVerifiedByType,
|
|
25
22
|
E164NumberToAddressType,
|
|
26
|
-
SecureSendPhoneNumberMapping,
|
|
27
23
|
} from 'src/identity/reducer'
|
|
28
|
-
import { checkIfValidationRequired } from 'src/identity/secureSend'
|
|
29
24
|
import {
|
|
30
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
const
|
|
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:
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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 } } =
|
|
336
|
-
|
|
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)
|
package/src/identity/reducer.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
229
|
-
...state.
|
|
230
|
-
[action.address]:
|
|
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:
|