wallet-stack 1.0.0-alpha.133 → 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/package.json +1 -1
- package/src/analytics/Properties.tsx +1 -1
- 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 +0 -17
- package/src/identity/contactMapping.test.ts +78 -19
- package/src/identity/contactMapping.ts +50 -20
- package/src/identity/reducer.ts +7 -21
- package/src/identity/selectors.ts +0 -2
- 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 +25 -0
- package/src/redux/migrations.ts +21 -0
- package/src/redux/store.test.ts +1 -2
- package/src/redux/store.ts +1 -1
- package/src/send/SelectRecipientAddress.tsx +11 -13
- package/src/send/SendConfirmation.test.tsx +3 -3
- package/src/send/SendConfirmation.tsx +3 -3
- package/src/send/SendSelectRecipient.test.tsx +37 -51
- 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
|
@@ -7,10 +7,9 @@ import { SendEvents } from 'src/analytics/Events'
|
|
|
7
7
|
import { SendOrigin } from 'src/analytics/types'
|
|
8
8
|
import { getAppConfig } from 'src/appConfig'
|
|
9
9
|
import { fetchAddressVerification, fetchAddressesAndValidate } from 'src/identity/actions'
|
|
10
|
-
import { RecipientVerificationStatus } from 'src/identity/types'
|
|
11
10
|
import { navigate } from 'src/navigator/NavigationService'
|
|
12
11
|
import { Screens } from 'src/navigator/Screens'
|
|
13
|
-
import { RecipientType
|
|
12
|
+
import { RecipientType } from 'src/recipients/recipient'
|
|
14
13
|
import SendSelectRecipient from 'src/send/SendSelectRecipient'
|
|
15
14
|
import { getDynamicConfigParams } from 'src/statsig'
|
|
16
15
|
import { StatsigDynamicConfigs } from 'src/statsig/types'
|
|
@@ -31,10 +30,6 @@ import {
|
|
|
31
30
|
jest.mock('@react-native-clipboard/clipboard')
|
|
32
31
|
jest.mock('src/utils/IosVersionUtils')
|
|
33
32
|
jest.mock('src/recipients/resolve-id')
|
|
34
|
-
jest.mock('src/recipients/recipient', () => ({
|
|
35
|
-
...(jest.requireActual('src/recipients/recipient') as any),
|
|
36
|
-
getRecipientVerificationStatus: jest.fn(),
|
|
37
|
-
}))
|
|
38
33
|
|
|
39
34
|
jest.mock('react-native-device-info', () => ({ getFontScaleSync: () => 1 }))
|
|
40
35
|
jest.mock('src/statsig')
|
|
@@ -203,11 +198,13 @@ describe('SendSelectRecipient', () => {
|
|
|
203
198
|
expect(getByTestId('SelectRecipient/NoResults')).toBeTruthy()
|
|
204
199
|
})
|
|
205
200
|
it('navigates to send amount when a verified phone recipient is tapped in search results', async () => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
201
|
+
const store = createMockStore({
|
|
202
|
+
...storeWithPhoneVerified,
|
|
203
|
+
identity: {
|
|
204
|
+
e164NumberToAddress: { [mockE164Number2Invite]: [mockAccount3] },
|
|
205
|
+
addressToVerifiedBy: { [mockAccount3]: 'valora' },
|
|
206
|
+
},
|
|
207
|
+
})
|
|
211
208
|
|
|
212
209
|
const { getByTestId } = render(
|
|
213
210
|
<Provider store={store}>
|
|
@@ -237,7 +234,12 @@ describe('SendSelectRecipient', () => {
|
|
|
237
234
|
})
|
|
238
235
|
})
|
|
239
236
|
it('navigates to send amount when an address is tapped and the user phone number is not verified', async () => {
|
|
240
|
-
const store = createMockStore(
|
|
237
|
+
const store = createMockStore({
|
|
238
|
+
...defaultStore,
|
|
239
|
+
identity: {
|
|
240
|
+
addressToVerifiedBy: { [mockAddressRecipient.address.toLowerCase()]: 'valora' },
|
|
241
|
+
},
|
|
242
|
+
})
|
|
241
243
|
|
|
242
244
|
const { getByTestId } = render(
|
|
243
245
|
<Provider store={store}>
|
|
@@ -267,11 +269,12 @@ describe('SendSelectRecipient', () => {
|
|
|
267
269
|
})
|
|
268
270
|
|
|
269
271
|
it('dispatches address verification when an address is tapped and the user phone number is verified', async () => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
272
|
+
const store = createMockStore({
|
|
273
|
+
...storeWithPhoneVerified,
|
|
274
|
+
identity: {
|
|
275
|
+
addressToVerifiedBy: { [mockAccount2.toLowerCase()]: 'valora' },
|
|
276
|
+
},
|
|
277
|
+
})
|
|
275
278
|
|
|
276
279
|
const { getByTestId } = render(
|
|
277
280
|
<Provider store={store}>
|
|
@@ -299,11 +302,12 @@ describe('SendSelectRecipient', () => {
|
|
|
299
302
|
expect(store.getActions()).toEqual([fetchAddressVerification(mockAccount2.toLowerCase())])
|
|
300
303
|
})
|
|
301
304
|
it('does not navigate when an unverified phone recipient is tapped and no share URL is configured', async () => {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
305
|
+
const store = createMockStore({
|
|
306
|
+
...storeWithPhoneVerified,
|
|
307
|
+
identity: {
|
|
308
|
+
e164NumberToAddress: { [mockE164Number2Invite]: null },
|
|
309
|
+
},
|
|
310
|
+
})
|
|
307
311
|
|
|
308
312
|
const { getByTestId } = render(
|
|
309
313
|
<Provider store={store}>
|
|
@@ -340,11 +344,13 @@ describe('SendSelectRecipient', () => {
|
|
|
340
344
|
inviteFriends: { shareUrl },
|
|
341
345
|
},
|
|
342
346
|
})
|
|
343
|
-
jest
|
|
344
|
-
.mocked(getRecipientVerificationStatus)
|
|
345
|
-
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
|
|
346
347
|
|
|
347
|
-
const store = createMockStore(
|
|
348
|
+
const store = createMockStore({
|
|
349
|
+
...storeWithPhoneVerified,
|
|
350
|
+
identity: {
|
|
351
|
+
e164NumberToAddress: { [mockE164Number2Invite]: null },
|
|
352
|
+
},
|
|
353
|
+
})
|
|
348
354
|
|
|
349
355
|
const { getByTestId } = render(
|
|
350
356
|
<Provider store={store}>
|
|
@@ -373,11 +379,11 @@ describe('SendSelectRecipient', () => {
|
|
|
373
379
|
})
|
|
374
380
|
|
|
375
381
|
it('navigates and dispatches address verification when an unknown address is tapped and the user phone number is verified', async () => {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
382
|
+
// addressToVerifiedBy entry is `null` → checked and not verified
|
|
383
|
+
const store = createMockStore({
|
|
384
|
+
...storeWithPhoneVerified,
|
|
385
|
+
identity: { addressToVerifiedBy: { [mockAccount2.toLowerCase()]: null } },
|
|
386
|
+
})
|
|
381
387
|
|
|
382
388
|
const { getByTestId } = render(
|
|
383
389
|
<Provider store={store}>
|
|
@@ -468,10 +474,6 @@ describe('SendSelectRecipient', () => {
|
|
|
468
474
|
})
|
|
469
475
|
|
|
470
476
|
it('navigates to send amount when a verified phone recipient with a single address is tapped', async () => {
|
|
471
|
-
jest
|
|
472
|
-
.mocked(getRecipientVerificationStatus)
|
|
473
|
-
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
474
|
-
|
|
475
477
|
const store = createMockStore({
|
|
476
478
|
...storeWithPhoneVerified,
|
|
477
479
|
identity: {
|
|
@@ -511,10 +513,6 @@ describe('SendSelectRecipient', () => {
|
|
|
511
513
|
})
|
|
512
514
|
})
|
|
513
515
|
it('navigates with isMiniPayRecipient when address is verified by minipay', async () => {
|
|
514
|
-
jest
|
|
515
|
-
.mocked(getRecipientVerificationStatus)
|
|
516
|
-
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
517
|
-
|
|
518
516
|
const store = createMockStore({
|
|
519
517
|
...storeWithPhoneVerified,
|
|
520
518
|
identity: {
|
|
@@ -552,10 +550,6 @@ describe('SendSelectRecipient', () => {
|
|
|
552
550
|
})
|
|
553
551
|
})
|
|
554
552
|
it('navigates to address picker when phone number recipient has multiple verified addresses', async () => {
|
|
555
|
-
jest
|
|
556
|
-
.mocked(getRecipientVerificationStatus)
|
|
557
|
-
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
558
|
-
|
|
559
553
|
const store = createMockStore({
|
|
560
554
|
...storeWithPhoneVerified,
|
|
561
555
|
identity: {
|
|
@@ -598,10 +592,6 @@ describe('SendSelectRecipient', () => {
|
|
|
598
592
|
})
|
|
599
593
|
})
|
|
600
594
|
it('navigates to send enter amount when phone number has multiple raw addresses but only one with a verifier', async () => {
|
|
601
|
-
jest
|
|
602
|
-
.mocked(getRecipientVerificationStatus)
|
|
603
|
-
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
604
|
-
|
|
605
595
|
const store = createMockStore({
|
|
606
596
|
...storeWithPhoneVerified,
|
|
607
597
|
identity: {
|
|
@@ -652,10 +642,6 @@ describe('SendSelectRecipient', () => {
|
|
|
652
642
|
it.each([{ searchAddress: mockAccount2 }, { searchAddress: mockAccount3 }])(
|
|
653
643
|
'navigates to send enter amount with correct address if an address is entered which also maps to a phone number with multiple addresses',
|
|
654
644
|
async ({ searchAddress }) => {
|
|
655
|
-
jest
|
|
656
|
-
.mocked(getRecipientVerificationStatus)
|
|
657
|
-
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
658
|
-
|
|
659
645
|
const store = createMockStore({
|
|
660
646
|
...storeWithPhoneVerified,
|
|
661
647
|
identity: {
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react'
|
|
2
2
|
import { phoneNumberVerifiedSelector } from 'src/app/selectors'
|
|
3
3
|
import { fetchAddressVerification, fetchAddressesAndValidate } from 'src/identity/actions'
|
|
4
|
-
import {
|
|
5
|
-
addressToVerificationStatusSelector,
|
|
6
|
-
e164NumberToAddressSelector,
|
|
7
|
-
} from 'src/identity/selectors'
|
|
4
|
+
import { addressToVerifiedBySelector, e164NumberToAddressSelector } from 'src/identity/selectors'
|
|
8
5
|
import { RecipientVerificationStatus } from 'src/identity/types'
|
|
9
6
|
import { Recipient, RecipientType, getRecipientVerificationStatus } from 'src/recipients/recipient'
|
|
10
7
|
import { useDispatch, useSelector } from 'src/redux/hooks'
|
|
@@ -16,7 +13,7 @@ const useFetchRecipientVerificationStatus = () => {
|
|
|
16
13
|
)
|
|
17
14
|
|
|
18
15
|
const e164NumberToAddress = useSelector(e164NumberToAddressSelector)
|
|
19
|
-
const
|
|
16
|
+
const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
|
|
20
17
|
const phoneNumberVerified = useSelector(phoneNumberVerifiedSelector)
|
|
21
18
|
const dispatch = useDispatch()
|
|
22
19
|
|
|
@@ -36,9 +33,7 @@ const useFetchRecipientVerificationStatus = () => {
|
|
|
36
33
|
) {
|
|
37
34
|
dispatch(fetchAddressesAndValidate(selectedRecipient.e164PhoneNumber))
|
|
38
35
|
} else if (selectedRecipient?.address) {
|
|
39
|
-
if (
|
|
40
|
-
setRecipientVerificationStatus(RecipientVerificationStatus.VERIFIED)
|
|
41
|
-
} else if (phoneNumberVerified) {
|
|
36
|
+
if (phoneNumberVerified) {
|
|
42
37
|
dispatch(fetchAddressVerification(selectedRecipient.address))
|
|
43
38
|
} else {
|
|
44
39
|
setRecipientVerificationStatus(RecipientVerificationStatus.UNVERIFIED)
|
|
@@ -49,12 +44,12 @@ const useFetchRecipientVerificationStatus = () => {
|
|
|
49
44
|
useEffect(() => {
|
|
50
45
|
if (recipient && recipientVerificationStatus === RecipientVerificationStatus.UNKNOWN) {
|
|
51
46
|
// e164NumberToAddress is updated after a successful phone number lookup,
|
|
52
|
-
//
|
|
47
|
+
// addressToVerifiedBy is updated after a successful address lookup
|
|
53
48
|
setRecipientVerificationStatus(
|
|
54
|
-
getRecipientVerificationStatus(recipient, e164NumberToAddress,
|
|
49
|
+
getRecipientVerificationStatus(recipient, e164NumberToAddress, addressToVerifiedBy)
|
|
55
50
|
)
|
|
56
51
|
}
|
|
57
|
-
}, [e164NumberToAddress,
|
|
52
|
+
}, [e164NumberToAddress, addressToVerifiedBy, recipient, recipientVerificationStatus])
|
|
58
53
|
|
|
59
54
|
return {
|
|
60
55
|
recipient,
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import { useTranslation } from 'react-i18next'
|
|
3
3
|
import { LayoutAnimation, StyleSheet, Text, View } from 'react-native'
|
|
4
|
+
import { formatShortenedAddress } from 'src/account/utils'
|
|
4
5
|
import AccountNumber from 'src/components/AccountNumber'
|
|
5
6
|
import Expandable from 'src/components/Expandable'
|
|
6
7
|
import Touchable from 'src/components/Touchable'
|
|
8
|
+
import VerifiedBadge from 'src/icons/VerifiedBadge'
|
|
7
9
|
import { Screens } from 'src/navigator/Screens'
|
|
8
10
|
import { getDisplayName, Recipient, recipientHasNumber } from 'src/recipients/recipient'
|
|
11
|
+
import { useVerifierName } from 'src/recipients/verifier'
|
|
9
12
|
import colors from 'src/styles/colors'
|
|
10
13
|
import { typeScale } from 'src/styles/fonts'
|
|
11
|
-
import {
|
|
14
|
+
import { Spacing } from 'src/styles/styles'
|
|
12
15
|
|
|
13
16
|
interface Props {
|
|
14
17
|
type: 'sent' | 'received' | 'withdrawn'
|
|
@@ -36,11 +39,30 @@ export default function UserSection({
|
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
const displayName = getDisplayName(recipient, t)
|
|
39
|
-
const displayNumber = recipientHasNumber(recipient)
|
|
40
|
-
? getDisplayNumberInternational(recipient.e164PhoneNumber)
|
|
41
|
-
: undefined
|
|
42
42
|
const address = recipient.address || ''
|
|
43
43
|
|
|
44
|
+
const verifierName = useVerifierName(recipient.address)
|
|
45
|
+
|
|
46
|
+
// Show short address in the secondary row when the primary (displayName) isn't already
|
|
47
|
+
// a formatted address — i.e., when the recipient has a name or phone number so the
|
|
48
|
+
// address would add information rather than duplicating the primary.
|
|
49
|
+
const hasIdentity = !!recipient.name || recipientHasNumber(recipient)
|
|
50
|
+
const shortAddress =
|
|
51
|
+
hasIdentity && recipient.address ? formatShortenedAddress(recipient.address) : undefined
|
|
52
|
+
|
|
53
|
+
const secondaryContent = verifierName ? (
|
|
54
|
+
<View style={styles.secondaryContent}>
|
|
55
|
+
{!!shortAddress && <Text style={styles.secondaryText}>{shortAddress}</Text>}
|
|
56
|
+
<VerifiedBadge
|
|
57
|
+
color={colors.contentSecondary}
|
|
58
|
+
testID={testID ? `${testID}/VerifierBadge` : undefined}
|
|
59
|
+
/>
|
|
60
|
+
<Text style={styles.secondaryText}>{verifierName}</Text>
|
|
61
|
+
</View>
|
|
62
|
+
) : shortAddress ? (
|
|
63
|
+
<Text style={styles.secondaryText}>{shortAddress}</Text>
|
|
64
|
+
) : null
|
|
65
|
+
|
|
44
66
|
const sectionLabel = {
|
|
45
67
|
received: t('receivedFrom'),
|
|
46
68
|
sent: t('sentTo'),
|
|
@@ -54,16 +76,14 @@ export default function UserSection({
|
|
|
54
76
|
<Text style={styles.sectionLabel}>{sectionLabel}</Text>
|
|
55
77
|
<Touchable onPress={toggleExpanded} disabled={!expandable}>
|
|
56
78
|
<>
|
|
57
|
-
<Expandable isExpandable={expandable && !
|
|
79
|
+
<Expandable isExpandable={expandable && !secondaryContent} isExpanded={expanded}>
|
|
58
80
|
<Text style={styles.username} testID={`${testID}/name`}>
|
|
59
81
|
{displayName}
|
|
60
82
|
</Text>
|
|
61
83
|
</Expandable>
|
|
62
|
-
{!!
|
|
63
|
-
<Expandable isExpandable={expandable
|
|
64
|
-
<
|
|
65
|
-
{displayNumber}
|
|
66
|
-
</Text>
|
|
84
|
+
{!!secondaryContent && (
|
|
85
|
+
<Expandable isExpandable={expandable} isExpanded={expanded}>
|
|
86
|
+
<View testID={`${testID}/address`}>{secondaryContent}</View>
|
|
67
87
|
</Expandable>
|
|
68
88
|
)}
|
|
69
89
|
</>
|
|
@@ -104,10 +124,16 @@ const styles = StyleSheet.create({
|
|
|
104
124
|
username: {
|
|
105
125
|
...typeScale.bodyMedium,
|
|
106
126
|
},
|
|
107
|
-
|
|
127
|
+
secondaryText: {
|
|
108
128
|
...typeScale.bodySmall,
|
|
109
129
|
color: colors.contentSecondary,
|
|
110
130
|
},
|
|
131
|
+
secondaryContent: {
|
|
132
|
+
flexDirection: 'row',
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
gap: Spacing.Tiny4,
|
|
135
|
+
flexShrink: 1,
|
|
136
|
+
},
|
|
111
137
|
avatarContainer: {
|
|
112
138
|
flex: 1,
|
|
113
139
|
justifyContent: 'center',
|
|
@@ -47,7 +47,6 @@ import {
|
|
|
47
47
|
mockClaimRewardTransaction,
|
|
48
48
|
mockCusdAddress,
|
|
49
49
|
mockCusdTokenId,
|
|
50
|
-
mockDisplayNumber2,
|
|
51
50
|
mockE164Number2,
|
|
52
51
|
mockEarnClaimRewardTransaction,
|
|
53
52
|
mockEarnDepositTransaction,
|
|
@@ -326,8 +325,8 @@ describe('TransactionDetailsScreen', () => {
|
|
|
326
325
|
const nameComponent = getByTestId('TransferSent/name')
|
|
327
326
|
expect(getElementText(nameComponent)).toEqual(mockName)
|
|
328
327
|
|
|
329
|
-
const
|
|
330
|
-
expect(getElementText(
|
|
328
|
+
const addressComponent = getByTestId('TransferSent/address')
|
|
329
|
+
expect(getElementText(addressComponent)).toEqual('0x8C3b...ca4E')
|
|
331
330
|
|
|
332
331
|
expect(getByTestId('TransactionDetails/FeeRowItem')).toHaveTextContent('0.01 CELO', {
|
|
333
332
|
exact: false,
|
|
@@ -359,8 +358,8 @@ describe('TransactionDetailsScreen', () => {
|
|
|
359
358
|
const nameComponent = getByTestId('TransferReceived/name')
|
|
360
359
|
expect(getElementText(nameComponent)).toEqual(mockName)
|
|
361
360
|
|
|
362
|
-
const
|
|
363
|
-
expect(getElementText(
|
|
361
|
+
const addressComponent = getByTestId('TransferReceived/address')
|
|
362
|
+
expect(getElementText(addressComponent)).toEqual('0x8C3b...ca4E')
|
|
364
363
|
|
|
365
364
|
const totalComponent = getByTestId('TotalLineItem/Total')
|
|
366
365
|
expect(getElementText(totalComponent)).toEqual('€4.00')
|
|
@@ -381,7 +380,8 @@ describe('TransactionDetailsScreen', () => {
|
|
|
381
380
|
const nameComponent = getByTestId('RewardReceived/name')
|
|
382
381
|
expect(getElementText(nameComponent)).toEqual('feedItemRewardReceivedTitle')
|
|
383
382
|
|
|
384
|
-
|
|
383
|
+
// Rewards senders don't have a known verifier, so the verifier composite isn't shown.
|
|
384
|
+
expect(queryByTestId('RewardReceived/VerifierBadge')).toBeNull()
|
|
385
385
|
|
|
386
386
|
const totalComponent = getByTestId('TotalLineItem/Total')
|
|
387
387
|
expect(getElementText(totalComponent)).toEqual('€4.00')
|
|
@@ -69,17 +69,6 @@ export function getDisplayPhoneNumber(phoneNumber: string, defaultCountryCode: s
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export function getDisplayNumberInternational(e164PhoneNumber: string) {
|
|
73
|
-
const countryCode = getCountryCode(e164PhoneNumber)
|
|
74
|
-
const phoneDetails = parsePhoneNumber(e164PhoneNumber, (countryCode || '').toString())
|
|
75
|
-
if (phoneDetails) {
|
|
76
|
-
return phoneDetails.displayNumberInternational
|
|
77
|
-
} else {
|
|
78
|
-
// Fallback to input instead of showing nothing for invalid numbers
|
|
79
|
-
return e164PhoneNumber
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
72
|
export function isE164NumberStrict(phoneNumber: string) {
|
|
84
73
|
try {
|
|
85
74
|
const parsedPhoneNumber = phoneUtil.parse(phoneNumber)
|