wallet-stack 1.0.0-alpha.132 → 1.0.0-alpha.133
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/identity/actions.ts +1 -97
- package/src/identity/contactMapping.test.ts +3 -28
- package/src/identity/contactMapping.ts +2 -88
- package/src/identity/reducer.ts +0 -77
- package/src/identity/saga.ts +2 -85
- package/src/identity/selectors.ts +0 -2
- 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/redux/migrations.test.ts +13 -0
- package/src/redux/migrations.ts +4 -0
- package/src/redux/store.test.ts +1 -2
- package/src/redux/store.ts +1 -1
- package/src/send/SelectRecipientAddress.test.tsx +146 -0
- package/src/send/SelectRecipientAddress.tsx +166 -0
- package/src/send/SendSelectRecipient.test.tsx +16 -87
- package/src/send/SendSelectRecipient.tsx +12 -27
- package/src/send/actions.ts +0 -26
- package/src/send/saga.ts +1 -6
- 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
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/react-native'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Provider } from 'react-redux'
|
|
4
|
+
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
5
|
+
import { SendEvents } from 'src/analytics/Events'
|
|
6
|
+
import { SendOrigin } from 'src/analytics/types'
|
|
7
|
+
import { navigate } from 'src/navigator/NavigationService'
|
|
8
|
+
import { Screens } from 'src/navigator/Screens'
|
|
9
|
+
import { RecipientType } from 'src/recipients/recipient'
|
|
10
|
+
import SelectRecipientAddress from 'src/send/SelectRecipientAddress'
|
|
11
|
+
import { createMockStore, getMockStackScreenProps } from 'test/utils'
|
|
12
|
+
import { mockAccount2, mockAccount3, mockE164Number3 } from 'test/values'
|
|
13
|
+
|
|
14
|
+
const mockRecipient = {
|
|
15
|
+
e164PhoneNumber: mockE164Number3,
|
|
16
|
+
displayNumber: '(415) 555-0123',
|
|
17
|
+
recipientType: RecipientType.PhoneNumber,
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
function renderScreen(storeOverrides: any) {
|
|
21
|
+
const store = createMockStore({
|
|
22
|
+
identity: {
|
|
23
|
+
e164NumberToAddress: {
|
|
24
|
+
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
|
|
25
|
+
},
|
|
26
|
+
addressToVerifiedBy: {
|
|
27
|
+
[mockAccount2.toLowerCase()]: 'valora',
|
|
28
|
+
[mockAccount3.toLowerCase()]: 'minipay',
|
|
29
|
+
},
|
|
30
|
+
...storeOverrides?.identity,
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const props = getMockStackScreenProps(Screens.SelectRecipientAddress, {
|
|
35
|
+
recipient: mockRecipient,
|
|
36
|
+
origin: SendOrigin.AppSendFlow,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
return render(
|
|
40
|
+
<Provider store={store}>
|
|
41
|
+
<SelectRecipientAddress {...props} />
|
|
42
|
+
</Provider>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('SelectRecipientAddress', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
jest.clearAllMocks()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('renders a row per verified address with its verifier label', () => {
|
|
52
|
+
const { getByText, getByTestId } = renderScreen({})
|
|
53
|
+
|
|
54
|
+
expect(getByTestId(`SelectRecipientAddress/Row/${mockAccount2.toLowerCase()}`)).toBeTruthy()
|
|
55
|
+
expect(getByTestId(`SelectRecipientAddress/Row/${mockAccount3.toLowerCase()}`)).toBeTruthy()
|
|
56
|
+
expect(getByText('Valora')).toBeTruthy()
|
|
57
|
+
expect(getByText('MiniPay')).toBeTruthy()
|
|
58
|
+
expect(
|
|
59
|
+
getByText(`selectRecipientAddress.explanation, {"name":"${mockRecipient.displayNumber}"}`)
|
|
60
|
+
).toBeTruthy()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('tracks the screen-open event with the number of verified addresses', () => {
|
|
64
|
+
renderScreen({})
|
|
65
|
+
|
|
66
|
+
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_address_open, {
|
|
67
|
+
addressCount: 2,
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('filters out addresses without a verifier entry', () => {
|
|
72
|
+
const { queryByTestId } = renderScreen({
|
|
73
|
+
identity: {
|
|
74
|
+
e164NumberToAddress: {
|
|
75
|
+
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
|
|
76
|
+
},
|
|
77
|
+
addressToVerifiedBy: {
|
|
78
|
+
[mockAccount2.toLowerCase()]: 'valora',
|
|
79
|
+
// mockAccount3 intentionally omitted
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
expect(queryByTestId(`SelectRecipientAddress/Row/${mockAccount2.toLowerCase()}`)).toBeTruthy()
|
|
85
|
+
expect(queryByTestId(`SelectRecipientAddress/Row/${mockAccount3.toLowerCase()}`)).toBeNull()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('filters out addresses with an unknown verifier', () => {
|
|
89
|
+
const { queryByTestId } = renderScreen({
|
|
90
|
+
identity: {
|
|
91
|
+
e164NumberToAddress: {
|
|
92
|
+
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
|
|
93
|
+
},
|
|
94
|
+
addressToVerifiedBy: {
|
|
95
|
+
[mockAccount2.toLowerCase()]: 'valora',
|
|
96
|
+
[mockAccount3.toLowerCase()]: 'somethingNew',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
expect(queryByTestId(`SelectRecipientAddress/Row/${mockAccount2.toLowerCase()}`)).toBeTruthy()
|
|
102
|
+
expect(queryByTestId(`SelectRecipientAddress/Row/${mockAccount3.toLowerCase()}`)).toBeNull()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('navigates to SendEnterAmount on Valora row tap with isMiniPayRecipient=false', () => {
|
|
106
|
+
const { getByTestId } = renderScreen({})
|
|
107
|
+
fireEvent.press(getByTestId(`SelectRecipientAddress/Row/${mockAccount2.toLowerCase()}`))
|
|
108
|
+
|
|
109
|
+
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
|
|
110
|
+
isFromScan: false,
|
|
111
|
+
defaultTokenIdOverride: undefined,
|
|
112
|
+
forceTokenId: undefined,
|
|
113
|
+
recipient: {
|
|
114
|
+
...mockRecipient,
|
|
115
|
+
address: mockAccount2.toLowerCase(),
|
|
116
|
+
},
|
|
117
|
+
origin: SendOrigin.AppSendFlow,
|
|
118
|
+
isMiniPayRecipient: false,
|
|
119
|
+
})
|
|
120
|
+
expect(AppAnalytics.track).toHaveBeenCalledWith(
|
|
121
|
+
SendEvents.send_select_recipient_address_select,
|
|
122
|
+
{ verifier: 'valora' }
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('navigates to SendEnterAmount on MiniPay row tap with isMiniPayRecipient=true', () => {
|
|
127
|
+
const { getByTestId } = renderScreen({})
|
|
128
|
+
fireEvent.press(getByTestId(`SelectRecipientAddress/Row/${mockAccount3.toLowerCase()}`))
|
|
129
|
+
|
|
130
|
+
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
|
|
131
|
+
isFromScan: false,
|
|
132
|
+
defaultTokenIdOverride: undefined,
|
|
133
|
+
forceTokenId: undefined,
|
|
134
|
+
recipient: {
|
|
135
|
+
...mockRecipient,
|
|
136
|
+
address: mockAccount3.toLowerCase(),
|
|
137
|
+
},
|
|
138
|
+
origin: SendOrigin.AppSendFlow,
|
|
139
|
+
isMiniPayRecipient: true,
|
|
140
|
+
})
|
|
141
|
+
expect(AppAnalytics.track).toHaveBeenCalledWith(
|
|
142
|
+
SendEvents.send_select_recipient_address_select,
|
|
143
|
+
{ verifier: 'minipay' }
|
|
144
|
+
)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
2
|
+
import React, { useEffect } from 'react'
|
|
3
|
+
import { Trans, useTranslation } from 'react-i18next'
|
|
4
|
+
import { Image, ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
5
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
6
|
+
import { formatShortenedAddress } from 'src/account/utils'
|
|
7
|
+
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
8
|
+
import { SendEvents } from 'src/analytics/Events'
|
|
9
|
+
import BackButton from 'src/components/BackButton'
|
|
10
|
+
import Touchable from 'src/components/Touchable'
|
|
11
|
+
import CustomHeader from 'src/components/header/CustomHeader'
|
|
12
|
+
import { addressToVerifiedBySelector, e164NumberToAddressSelector } from 'src/identity/selectors'
|
|
13
|
+
import { miniPay, valora } from 'src/images/Images'
|
|
14
|
+
import { noHeader } from 'src/navigator/Headers'
|
|
15
|
+
import { navigate } from 'src/navigator/NavigationService'
|
|
16
|
+
import { Screens } from 'src/navigator/Screens'
|
|
17
|
+
import { StackParamList } from 'src/navigator/types'
|
|
18
|
+
import { getDisplayName } from 'src/recipients/recipient'
|
|
19
|
+
import { useSelector } from 'src/redux/hooks'
|
|
20
|
+
import Colors from 'src/styles/colors'
|
|
21
|
+
import { typeScale } from 'src/styles/fonts'
|
|
22
|
+
import { Spacing } from 'src/styles/styles'
|
|
23
|
+
|
|
24
|
+
type Props = NativeStackScreenProps<StackParamList, Screens.SelectRecipientAddress>
|
|
25
|
+
|
|
26
|
+
const VERIFIERS = {
|
|
27
|
+
valora: { name: 'Valora', icon: valora },
|
|
28
|
+
minipay: { name: 'MiniPay', icon: miniPay },
|
|
29
|
+
} as const
|
|
30
|
+
|
|
31
|
+
export type Verifier = keyof typeof VERIFIERS
|
|
32
|
+
|
|
33
|
+
const ICON_SIZE = 40
|
|
34
|
+
|
|
35
|
+
function isKnownVerifier(verifier: string | undefined): verifier is Verifier {
|
|
36
|
+
return !!verifier && Object.hasOwn(VERIFIERS, verifier)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function VerifierIcon({ verifier }: { verifier: Verifier }) {
|
|
40
|
+
return <Image source={VERIFIERS[verifier].icon} style={styles.icon} resizeMode="contain" />
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function SelectRecipientAddress({ route }: Props) {
|
|
44
|
+
const { t } = useTranslation()
|
|
45
|
+
const { recipient, origin, forceTokenId, defaultTokenIdOverride } = route.params
|
|
46
|
+
|
|
47
|
+
const e164NumberToAddress = useSelector(e164NumberToAddressSelector)
|
|
48
|
+
const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
|
|
49
|
+
|
|
50
|
+
const addresses = e164NumberToAddress[recipient.e164PhoneNumber] || []
|
|
51
|
+
const verifiedEntries = addresses
|
|
52
|
+
.map((address) => ({ address, verifier: addressToVerifiedBy[address] }))
|
|
53
|
+
.filter((entry): entry is { address: string; verifier: Verifier } =>
|
|
54
|
+
isKnownVerifier(entry.verifier)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
AppAnalytics.track(SendEvents.send_select_recipient_address_open, {
|
|
59
|
+
addressCount: verifiedEntries.length,
|
|
60
|
+
})
|
|
61
|
+
}, [])
|
|
62
|
+
|
|
63
|
+
const onSelectAddress = (address: string, verifier: Verifier) => {
|
|
64
|
+
AppAnalytics.track(SendEvents.send_select_recipient_address_select, {
|
|
65
|
+
verifier,
|
|
66
|
+
})
|
|
67
|
+
navigate(Screens.SendEnterAmount, {
|
|
68
|
+
isFromScan: false,
|
|
69
|
+
defaultTokenIdOverride,
|
|
70
|
+
forceTokenId,
|
|
71
|
+
recipient: {
|
|
72
|
+
...recipient,
|
|
73
|
+
address,
|
|
74
|
+
},
|
|
75
|
+
origin,
|
|
76
|
+
isMiniPayRecipient: verifier === 'minipay',
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<SafeAreaView style={styles.container} edges={['top']}>
|
|
82
|
+
<CustomHeader
|
|
83
|
+
style={styles.customHeader}
|
|
84
|
+
left={<BackButton eventName={SendEvents.send_select_recipient_address_back} />}
|
|
85
|
+
/>
|
|
86
|
+
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
87
|
+
<Text style={styles.title}>{t('selectRecipientAddress.header')}</Text>
|
|
88
|
+
<Text style={styles.explanation}>
|
|
89
|
+
<Trans
|
|
90
|
+
i18nKey="selectRecipientAddress.explanation"
|
|
91
|
+
tOptions={{ name: getDisplayName(recipient, t) }}
|
|
92
|
+
>
|
|
93
|
+
<Text style={styles.explanationName} />
|
|
94
|
+
</Trans>
|
|
95
|
+
</Text>
|
|
96
|
+
{verifiedEntries.map(({ address, verifier }) => (
|
|
97
|
+
<Touchable
|
|
98
|
+
key={address}
|
|
99
|
+
onPress={() => onSelectAddress(address, verifier)}
|
|
100
|
+
testID={`SelectRecipientAddress/Row/${address}`}
|
|
101
|
+
>
|
|
102
|
+
<View style={styles.row}>
|
|
103
|
+
<VerifierIcon verifier={verifier} />
|
|
104
|
+
<View style={styles.rowContent}>
|
|
105
|
+
<Text style={styles.address}>{formatShortenedAddress(address)}</Text>
|
|
106
|
+
<Text style={styles.verifier}>{VERIFIERS[verifier].name}</Text>
|
|
107
|
+
</View>
|
|
108
|
+
</View>
|
|
109
|
+
</Touchable>
|
|
110
|
+
))}
|
|
111
|
+
</ScrollView>
|
|
112
|
+
</SafeAreaView>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
container: {
|
|
118
|
+
flex: 1,
|
|
119
|
+
},
|
|
120
|
+
customHeader: {
|
|
121
|
+
paddingHorizontal: Spacing.Thick24,
|
|
122
|
+
},
|
|
123
|
+
scrollContent: {
|
|
124
|
+
paddingBottom: Spacing.Smallest8,
|
|
125
|
+
},
|
|
126
|
+
title: {
|
|
127
|
+
...typeScale.titleMedium,
|
|
128
|
+
paddingHorizontal: Spacing.Regular16,
|
|
129
|
+
paddingTop: Spacing.Thick24,
|
|
130
|
+
marginBottom: Spacing.Smallest8,
|
|
131
|
+
},
|
|
132
|
+
explanation: {
|
|
133
|
+
...typeScale.bodySmall,
|
|
134
|
+
color: Colors.contentSecondary,
|
|
135
|
+
paddingHorizontal: Spacing.Regular16,
|
|
136
|
+
paddingBottom: Spacing.Regular16,
|
|
137
|
+
},
|
|
138
|
+
explanationName: {
|
|
139
|
+
fontWeight: 'bold',
|
|
140
|
+
},
|
|
141
|
+
icon: {
|
|
142
|
+
width: ICON_SIZE,
|
|
143
|
+
height: ICON_SIZE,
|
|
144
|
+
},
|
|
145
|
+
row: {
|
|
146
|
+
flexDirection: 'row',
|
|
147
|
+
alignItems: 'center',
|
|
148
|
+
paddingHorizontal: Spacing.Regular16,
|
|
149
|
+
paddingVertical: Spacing.Regular16,
|
|
150
|
+
},
|
|
151
|
+
rowContent: {
|
|
152
|
+
flex: 1,
|
|
153
|
+
marginLeft: Spacing.Small12,
|
|
154
|
+
},
|
|
155
|
+
address: {
|
|
156
|
+
...typeScale.labelMedium,
|
|
157
|
+
},
|
|
158
|
+
verifier: {
|
|
159
|
+
...typeScale.bodySmall,
|
|
160
|
+
color: Colors.contentSecondary,
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
SelectRecipientAddress.navigationOptions = noHeader
|
|
165
|
+
|
|
166
|
+
export default SelectRecipientAddress
|
|
@@ -7,7 +7,6 @@ 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 { AddressValidationType } from 'src/identity/reducer'
|
|
11
10
|
import { RecipientVerificationStatus } from 'src/identity/types'
|
|
12
11
|
import { navigate } from 'src/navigator/NavigationService'
|
|
13
12
|
import { Screens } from 'src/navigator/Screens'
|
|
@@ -476,9 +475,6 @@ describe('SendSelectRecipient', () => {
|
|
|
476
475
|
const store = createMockStore({
|
|
477
476
|
...storeWithPhoneVerified,
|
|
478
477
|
identity: {
|
|
479
|
-
secureSendPhoneNumberMapping: {
|
|
480
|
-
[mockE164Number3]: { addressValidationType: AddressValidationType.NONE },
|
|
481
|
-
},
|
|
482
478
|
e164NumberToAddress: { [mockE164Number3]: [mockAccount3] },
|
|
483
479
|
},
|
|
484
480
|
})
|
|
@@ -522,9 +518,6 @@ describe('SendSelectRecipient', () => {
|
|
|
522
518
|
const store = createMockStore({
|
|
523
519
|
...storeWithPhoneVerified,
|
|
524
520
|
identity: {
|
|
525
|
-
secureSendPhoneNumberMapping: {
|
|
526
|
-
[mockE164Number3]: { addressValidationType: AddressValidationType.NONE },
|
|
527
|
-
},
|
|
528
521
|
e164NumberToAddress: { [mockE164Number3]: [mockAccount3] },
|
|
529
522
|
addressToVerifiedBy: { [mockAccount3]: 'minipay' },
|
|
530
523
|
},
|
|
@@ -558,7 +551,7 @@ describe('SendSelectRecipient', () => {
|
|
|
558
551
|
isMiniPayRecipient: true,
|
|
559
552
|
})
|
|
560
553
|
})
|
|
561
|
-
it('navigates to
|
|
554
|
+
it('navigates to address picker when phone number recipient has multiple verified addresses', async () => {
|
|
562
555
|
jest
|
|
563
556
|
.mocked(getRecipientVerificationStatus)
|
|
564
557
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -566,9 +559,6 @@ describe('SendSelectRecipient', () => {
|
|
|
566
559
|
const store = createMockStore({
|
|
567
560
|
...storeWithPhoneVerified,
|
|
568
561
|
identity: {
|
|
569
|
-
secureSendPhoneNumberMapping: {
|
|
570
|
-
[mockE164Number3]: { addressValidationType: AddressValidationType.PARTIAL },
|
|
571
|
-
},
|
|
572
562
|
e164NumberToAddress: {
|
|
573
563
|
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
|
|
574
564
|
},
|
|
@@ -576,6 +566,10 @@ describe('SendSelectRecipient', () => {
|
|
|
576
566
|
[mockAccount2.toLowerCase()]: mockE164Number3,
|
|
577
567
|
[mockAccount3.toLowerCase()]: mockE164Number3,
|
|
578
568
|
},
|
|
569
|
+
addressToVerifiedBy: {
|
|
570
|
+
[mockAccount2.toLowerCase()]: 'valora',
|
|
571
|
+
[mockAccount3.toLowerCase()]: 'minipay',
|
|
572
|
+
},
|
|
579
573
|
},
|
|
580
574
|
})
|
|
581
575
|
|
|
@@ -596,14 +590,14 @@ describe('SendSelectRecipient', () => {
|
|
|
596
590
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
597
591
|
recipientType: RecipientType.PhoneNumber,
|
|
598
592
|
})
|
|
599
|
-
expect(navigate).toHaveBeenCalledWith(Screens.
|
|
593
|
+
expect(navigate).toHaveBeenCalledWith(Screens.SelectRecipientAddress, {
|
|
600
594
|
defaultTokenIdOverride: undefined,
|
|
601
595
|
forceTokenId: undefined,
|
|
602
596
|
recipient: expect.any(Object),
|
|
603
597
|
origin: SendOrigin.AppSendFlow,
|
|
604
598
|
})
|
|
605
599
|
})
|
|
606
|
-
it('navigates to send amount when phone
|
|
600
|
+
it('navigates to send enter amount when phone number has multiple raw addresses but only one with a verifier', async () => {
|
|
607
601
|
jest
|
|
608
602
|
.mocked(getRecipientVerificationStatus)
|
|
609
603
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -611,12 +605,6 @@ describe('SendSelectRecipient', () => {
|
|
|
611
605
|
const store = createMockStore({
|
|
612
606
|
...storeWithPhoneVerified,
|
|
613
607
|
identity: {
|
|
614
|
-
secureSendPhoneNumberMapping: {
|
|
615
|
-
[mockE164Number3]: {
|
|
616
|
-
addressValidationType: AddressValidationType.NONE,
|
|
617
|
-
address: mockAccount3,
|
|
618
|
-
},
|
|
619
|
-
},
|
|
620
608
|
e164NumberToAddress: {
|
|
621
609
|
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
|
|
622
610
|
},
|
|
@@ -624,6 +612,9 @@ describe('SendSelectRecipient', () => {
|
|
|
624
612
|
[mockAccount2.toLowerCase()]: mockE164Number3,
|
|
625
613
|
[mockAccount3.toLowerCase()]: mockE164Number3,
|
|
626
614
|
},
|
|
615
|
+
addressToVerifiedBy: {
|
|
616
|
+
[mockAccount3.toLowerCase()]: 'valora',
|
|
617
|
+
},
|
|
627
618
|
},
|
|
628
619
|
})
|
|
629
620
|
|
|
@@ -649,7 +640,7 @@ describe('SendSelectRecipient', () => {
|
|
|
649
640
|
defaultTokenIdOverride: undefined,
|
|
650
641
|
forceTokenId: undefined,
|
|
651
642
|
recipient: {
|
|
652
|
-
address: mockAccount3,
|
|
643
|
+
address: mockAccount3.toLowerCase(),
|
|
653
644
|
displayNumber: '(415) 555-0123',
|
|
654
645
|
e164PhoneNumber: mockE164Number3,
|
|
655
646
|
recipientType: 'PhoneNumber',
|
|
@@ -659,7 +650,7 @@ describe('SendSelectRecipient', () => {
|
|
|
659
650
|
})
|
|
660
651
|
})
|
|
661
652
|
it.each([{ searchAddress: mockAccount2 }, { searchAddress: mockAccount3 }])(
|
|
662
|
-
'navigates to send enter amount with correct address if
|
|
653
|
+
'navigates to send enter amount with correct address if an address is entered which also maps to a phone number with multiple addresses',
|
|
663
654
|
async ({ searchAddress }) => {
|
|
664
655
|
jest
|
|
665
656
|
.mocked(getRecipientVerificationStatus)
|
|
@@ -668,11 +659,6 @@ describe('SendSelectRecipient', () => {
|
|
|
668
659
|
const store = createMockStore({
|
|
669
660
|
...storeWithPhoneVerified,
|
|
670
661
|
identity: {
|
|
671
|
-
secureSendPhoneNumberMapping: {
|
|
672
|
-
[mockE164Number3]: {
|
|
673
|
-
addressValidationType: AddressValidationType.PARTIAL,
|
|
674
|
-
},
|
|
675
|
-
},
|
|
676
662
|
e164NumberToAddress: {
|
|
677
663
|
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
|
|
678
664
|
},
|
|
@@ -680,66 +666,9 @@ describe('SendSelectRecipient', () => {
|
|
|
680
666
|
[mockAccount2.toLowerCase()]: mockE164Number3,
|
|
681
667
|
[mockAccount3.toLowerCase()]: mockE164Number3,
|
|
682
668
|
},
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
const { getByTestId } = render(
|
|
687
|
-
<Provider store={store}>
|
|
688
|
-
<SendSelectRecipient {...mockScreenProps({})} />
|
|
689
|
-
</Provider>
|
|
690
|
-
)
|
|
691
|
-
const searchInput = getByTestId('SendSelectRecipientSearchInput')
|
|
692
|
-
|
|
693
|
-
await act(() => {
|
|
694
|
-
fireEvent.changeText(searchInput, searchAddress)
|
|
695
|
-
})
|
|
696
|
-
await act(() => {
|
|
697
|
-
fireEvent.press(getByTestId('RecipientItem'))
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
701
|
-
recipientType: RecipientType.Address,
|
|
702
|
-
})
|
|
703
|
-
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
|
|
704
|
-
isFromScan: false,
|
|
705
|
-
defaultTokenIdOverride: undefined,
|
|
706
|
-
forceTokenId: undefined,
|
|
707
|
-
recipient: {
|
|
708
|
-
address: searchAddress.toLowerCase(),
|
|
709
|
-
e164PhoneNumber: mockE164Number3,
|
|
710
|
-
recipientType: RecipientType.Address,
|
|
711
|
-
contactId: undefined,
|
|
712
|
-
displayNumber: undefined,
|
|
713
|
-
name: undefined,
|
|
714
|
-
thumbnailPath: undefined,
|
|
715
|
-
},
|
|
716
|
-
origin: SendOrigin.AppSendFlow,
|
|
717
|
-
isMiniPayRecipient: false,
|
|
718
|
-
})
|
|
719
|
-
}
|
|
720
|
-
)
|
|
721
|
-
it.each([{ searchAddress: mockAccount2 }, { searchAddress: mockAccount3 }])(
|
|
722
|
-
'navigates to send enter amount with correct address if a an address is entered which also has a phone number with secure send done with different address',
|
|
723
|
-
async ({ searchAddress }) => {
|
|
724
|
-
jest
|
|
725
|
-
.mocked(getRecipientVerificationStatus)
|
|
726
|
-
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
727
|
-
|
|
728
|
-
const store = createMockStore({
|
|
729
|
-
...storeWithPhoneVerified,
|
|
730
|
-
identity: {
|
|
731
|
-
secureSendPhoneNumberMapping: {
|
|
732
|
-
[mockE164Number3]: {
|
|
733
|
-
addressValidationType: AddressValidationType.NONE,
|
|
734
|
-
address: mockAccount3,
|
|
735
|
-
},
|
|
736
|
-
},
|
|
737
|
-
e164NumberToAddress: {
|
|
738
|
-
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
|
|
739
|
-
},
|
|
740
|
-
addressToE164Number: {
|
|
741
|
-
[mockAccount2.toLowerCase()]: mockE164Number3,
|
|
742
|
-
[mockAccount3.toLowerCase()]: mockE164Number3,
|
|
669
|
+
addressToVerifiedBy: {
|
|
670
|
+
[mockAccount2.toLowerCase()]: 'valora',
|
|
671
|
+
[mockAccount3.toLowerCase()]: 'minipay',
|
|
743
672
|
},
|
|
744
673
|
},
|
|
745
674
|
})
|
|
@@ -775,7 +704,7 @@ describe('SendSelectRecipient', () => {
|
|
|
775
704
|
thumbnailPath: undefined,
|
|
776
705
|
},
|
|
777
706
|
origin: SendOrigin.AppSendFlow,
|
|
778
|
-
isMiniPayRecipient:
|
|
707
|
+
isMiniPayRecipient: searchAddress.toLowerCase() === mockAccount3.toLowerCase(),
|
|
779
708
|
})
|
|
780
709
|
}
|
|
781
710
|
)
|
|
@@ -14,14 +14,7 @@ import KeyboardAwareScrollView from 'src/components/KeyboardAwareScrollView'
|
|
|
14
14
|
import CustomHeader from 'src/components/header/CustomHeader'
|
|
15
15
|
import CircledIcon from 'src/icons/CircledIcon'
|
|
16
16
|
import { importContacts } from 'src/identity/actions'
|
|
17
|
-
import {
|
|
18
|
-
import { AddressValidationType } from 'src/identity/reducer'
|
|
19
|
-
import { getAddressValidationType } from 'src/identity/secureSend'
|
|
20
|
-
import {
|
|
21
|
-
addressToVerifiedBySelector,
|
|
22
|
-
e164NumberToAddressSelector,
|
|
23
|
-
secureSendPhoneNumberMappingSelector,
|
|
24
|
-
} from 'src/identity/selectors'
|
|
17
|
+
import { addressToVerifiedBySelector, e164NumberToAddressSelector } from 'src/identity/selectors'
|
|
25
18
|
import { RecipientVerificationStatus } from 'src/identity/types'
|
|
26
19
|
import { noHeader } from 'src/navigator/Headers'
|
|
27
20
|
import { navigate } from 'src/navigator/NavigationService'
|
|
@@ -171,7 +164,6 @@ enum SelectRecipientView {
|
|
|
171
164
|
function SendSelectRecipient({ route }: Props) {
|
|
172
165
|
const { t } = useTranslation()
|
|
173
166
|
const dispatch = useDispatch()
|
|
174
|
-
const secureSendPhoneNumberMapping = useSelector(secureSendPhoneNumberMappingSelector)
|
|
175
167
|
const e164NumberToAddress = useSelector(e164NumberToAddressSelector)
|
|
176
168
|
const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
|
|
177
169
|
const shareUrl = getAppConfig().experimental?.inviteFriends?.shareUrl ?? null
|
|
@@ -241,35 +233,28 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
241
233
|
// use the address from the recipient object
|
|
242
234
|
let address: string | null | undefined = selectedRecipient.address
|
|
243
235
|
|
|
244
|
-
// if not present there must be a phone number, route through
|
|
245
|
-
//
|
|
236
|
+
// if not present there must be a phone number, route through the address picker
|
|
237
|
+
// when multiple verified addresses exist, otherwise go directly to amount entry
|
|
246
238
|
if (!address && recipientHasNumber(selectedRecipient)) {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
navigate(Screens.ValidateRecipientIntro, {
|
|
253
|
-
defaultTokenIdOverride,
|
|
239
|
+
const phoneAddresses = e164NumberToAddress[selectedRecipient.e164PhoneNumber] ?? []
|
|
240
|
+
const verifiedAddresses = phoneAddresses.filter((a) => !!addressToVerifiedBy[a])
|
|
241
|
+
|
|
242
|
+
if (verifiedAddresses.length > 1) {
|
|
243
|
+
navigate(Screens.SelectRecipientAddress, {
|
|
254
244
|
forceTokenId,
|
|
245
|
+
defaultTokenIdOverride,
|
|
255
246
|
recipient: selectedRecipient,
|
|
256
247
|
origin: SendOrigin.AppSendFlow,
|
|
257
248
|
})
|
|
258
249
|
return
|
|
259
250
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
e164NumberToAddress,
|
|
263
|
-
secureSendPhoneNumberMapping,
|
|
264
|
-
undefined
|
|
265
|
-
)
|
|
251
|
+
|
|
252
|
+
address = verifiedAddresses[0] ?? phoneAddresses[0]
|
|
266
253
|
}
|
|
267
254
|
|
|
268
255
|
if (!address) {
|
|
269
256
|
// this should never happen
|
|
270
|
-
throw new Error(
|
|
271
|
-
'No address found, this should never happen. Should have routed to invite or secure send.'
|
|
272
|
-
)
|
|
257
|
+
throw new Error('No address found, this should never happen. Should have routed to invite.')
|
|
273
258
|
}
|
|
274
259
|
|
|
275
260
|
navigate(Screens.SendEnterAmount, {
|
package/src/send/actions.ts
CHANGED
|
@@ -9,7 +9,6 @@ export type SVG = typeof Svg
|
|
|
9
9
|
|
|
10
10
|
export enum Actions {
|
|
11
11
|
BARCODE_DETECTED = 'SEND/BARCODE_DETECTED',
|
|
12
|
-
BARCODE_DETECTED_SECURE_SEND = 'SEND/BARCODE_DETECTED_SECURE_SEND',
|
|
13
12
|
QRCODE_SHARE = 'SEND/QRCODE_SHARE',
|
|
14
13
|
SEND_PAYMENT = 'SEND/SEND_PAYMENT',
|
|
15
14
|
SEND_PAYMENT_SUCCESS = 'SEND/SEND_PAYMENT_SUCCESS',
|
|
@@ -23,15 +22,6 @@ export interface HandleQRCodeDetectedAction {
|
|
|
23
22
|
defaultTokenIdOverride?: string
|
|
24
23
|
}
|
|
25
24
|
|
|
26
|
-
export interface HandleQRCodeDetectedSecureSendAction {
|
|
27
|
-
type: Actions.BARCODE_DETECTED_SECURE_SEND
|
|
28
|
-
qrCode: QrCode
|
|
29
|
-
requesterAddress?: string
|
|
30
|
-
recipient: Recipient
|
|
31
|
-
forceTokenId?: boolean
|
|
32
|
-
defaultTokenIdOverride?: string
|
|
33
|
-
}
|
|
34
|
-
|
|
35
25
|
export interface ShareQRCodeAction {
|
|
36
26
|
type: Actions.QRCODE_SHARE
|
|
37
27
|
qrCodeSvg: SVG
|
|
@@ -64,7 +54,6 @@ export interface UpdateLastUsedCurrencyAction {
|
|
|
64
54
|
|
|
65
55
|
export type ActionTypes =
|
|
66
56
|
| HandleQRCodeDetectedAction
|
|
67
|
-
| HandleQRCodeDetectedSecureSendAction
|
|
68
57
|
| ShareQRCodeAction
|
|
69
58
|
| SendPaymentAction
|
|
70
59
|
| SendPaymentSuccessAction
|
|
@@ -83,21 +72,6 @@ export const handleQRCodeDetected = ({
|
|
|
83
72
|
defaultTokenIdOverride,
|
|
84
73
|
})
|
|
85
74
|
|
|
86
|
-
export const handleQRCodeDetectedSecureSend = (
|
|
87
|
-
qrCode: QrCode,
|
|
88
|
-
recipient: Recipient,
|
|
89
|
-
requesterAddress?: string,
|
|
90
|
-
forceTokenId?: boolean,
|
|
91
|
-
defaultTokenIdOverride?: string
|
|
92
|
-
): HandleQRCodeDetectedSecureSendAction => ({
|
|
93
|
-
type: Actions.BARCODE_DETECTED_SECURE_SEND,
|
|
94
|
-
qrCode,
|
|
95
|
-
requesterAddress,
|
|
96
|
-
recipient,
|
|
97
|
-
forceTokenId,
|
|
98
|
-
defaultTokenIdOverride,
|
|
99
|
-
})
|
|
100
|
-
|
|
101
75
|
export const shareQRCode = (qrCodeSvg: SVG): ShareQRCodeAction => ({
|
|
102
76
|
type: Actions.QRCODE_SHARE,
|
|
103
77
|
qrCodeSvg,
|
package/src/send/saga.ts
CHANGED
|
@@ -3,7 +3,7 @@ import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
|
3
3
|
import { CeloExchangeEvents, SendEvents } from 'src/analytics/Events'
|
|
4
4
|
import { ErrorMessages } from 'src/app/ErrorMessages'
|
|
5
5
|
import { navigateBack, navigateInitialTab } from 'src/navigator/NavigationService'
|
|
6
|
-
import { handleQRCodeDefault,
|
|
6
|
+
import { handleQRCodeDefault, shareSVGImage } from 'src/qrcode/utils'
|
|
7
7
|
import {
|
|
8
8
|
Actions,
|
|
9
9
|
SendPaymentAction,
|
|
@@ -164,12 +164,7 @@ function* watchQrCodeDetections() {
|
|
|
164
164
|
yield* takeEvery(Actions.BARCODE_DETECTED, safely(handleQRCodeDefault))
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
function* watchQrCodeDetectionsSecureSend() {
|
|
168
|
-
yield* takeEvery(Actions.BARCODE_DETECTED_SECURE_SEND, safely(handleQRCodeSecureSend))
|
|
169
|
-
}
|
|
170
|
-
|
|
171
167
|
export function* sendSaga() {
|
|
172
|
-
yield* spawn(watchQrCodeDetectionsSecureSend)
|
|
173
168
|
yield* spawn(watchQrCodeDetections)
|
|
174
169
|
yield* spawn(watchQrCodeShare)
|
|
175
170
|
yield* spawn(watchSendPayment)
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { StyleSheet, ViewStyle } from 'react-native'
|
|
3
|
-
import AccountNumber from 'src/components/AccountNumber'
|
|
4
|
-
import Card from 'src/components/Card'
|
|
5
|
-
|
|
6
|
-
interface Props {
|
|
7
|
-
address: string
|
|
8
|
-
style?: ViewStyle
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export default function AccountNumberCard({ address, style }: Props) {
|
|
12
|
-
return (
|
|
13
|
-
<Card style={[styles.container, style]} rounded={true}>
|
|
14
|
-
<AccountNumber address={address} touchDisabled={true} />
|
|
15
|
-
</Card>
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const styles = StyleSheet.create({
|
|
20
|
-
container: {
|
|
21
|
-
justifyContent: 'center',
|
|
22
|
-
},
|
|
23
|
-
})
|