wallet-stack 1.0.0-alpha.131 → 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 +11 -0
- package/package.json +2 -3
- 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 +1 -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 +3 -0
- package/src/images/assets/invite-modal.png +0 -0
- package/src/images/assets/invite-modal@1.5x.png +0 -0
- package/src/images/assets/invite-modal@2x.png +0 -0
- package/src/images/assets/invite-modal@3x.png +0 -0
- package/src/images/assets/invite-modal@4x.png +0 -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 +10 -14
- package/src/navigator/Screens.tsx +2 -2
- package/src/navigator/types.tsx +5 -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/SendConfirmation.test.tsx +28 -0
- package/src/send/SendConfirmation.tsx +18 -1
- package/src/send/SendInvite.test.tsx +107 -0
- package/src/send/SendInvite.tsx +99 -0
- package/src/send/SendSelectRecipient.test.tsx +44 -223
- package/src/send/SendSelectRecipient.tsx +41 -149
- 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
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
v251Schema,
|
|
69
69
|
v253Schema,
|
|
70
70
|
v254Schema,
|
|
71
|
+
v255Schema,
|
|
71
72
|
v28Schema,
|
|
72
73
|
v2Schema,
|
|
73
74
|
v35Schema,
|
|
@@ -1946,4 +1947,16 @@ describe('Redux persist migrations', () => {
|
|
|
1946
1947
|
const migratedSchema = migrations[255](oldSchema)
|
|
1947
1948
|
expect(migratedSchema.identity.addressToVerifiedBy).toStrictEqual({})
|
|
1948
1949
|
})
|
|
1950
|
+
|
|
1951
|
+
it('works from 255 to 256', () => {
|
|
1952
|
+
const oldSchema = {
|
|
1953
|
+
...v255Schema,
|
|
1954
|
+
identity: {
|
|
1955
|
+
...v255Schema.identity,
|
|
1956
|
+
secureSendPhoneNumberMapping: { '+14155550000': {} },
|
|
1957
|
+
},
|
|
1958
|
+
}
|
|
1959
|
+
const migratedSchema = migrations[256](oldSchema)
|
|
1960
|
+
expect(migratedSchema.identity.secureSendPhoneNumberMapping).toBeUndefined()
|
|
1961
|
+
})
|
|
1949
1962
|
})
|
package/src/redux/migrations.ts
CHANGED
package/src/redux/store.test.ts
CHANGED
|
@@ -143,7 +143,7 @@ describe('store state', () => {
|
|
|
143
143
|
{
|
|
144
144
|
"_persist": {
|
|
145
145
|
"rehydrated": true,
|
|
146
|
-
"version":
|
|
146
|
+
"version": 256,
|
|
147
147
|
},
|
|
148
148
|
"account": {
|
|
149
149
|
"acceptedTerms": false,
|
|
@@ -254,7 +254,6 @@ describe('store state', () => {
|
|
|
254
254
|
"total": 0,
|
|
255
255
|
},
|
|
256
256
|
"lastSavedContactsHash": null,
|
|
257
|
-
"secureSendPhoneNumberMapping": {},
|
|
258
257
|
"shouldRefreshStoredPasswordHash": true,
|
|
259
258
|
},
|
|
260
259
|
"imports": {
|
package/src/redux/store.ts
CHANGED
|
@@ -30,7 +30,7 @@ const persistConfig: PersistConfig<ReducersRootState> = {
|
|
|
30
30
|
key: 'root',
|
|
31
31
|
// default is -1, increment as we make migrations
|
|
32
32
|
// See https://github.com/valora-xyz/wallet/tree/main/WALLET.md#redux-state-migration
|
|
33
|
-
version:
|
|
33
|
+
version: 256,
|
|
34
34
|
keyPrefix: `reduxStore-`, // the redux-persist default is `persist:` which doesn't work with some file systems.
|
|
35
35
|
storage: FSStorage(),
|
|
36
36
|
blacklist: ['networkInfo', 'alert', 'imports', 'keylessBackup', transactionFeedV2Api.reducerPath],
|
|
@@ -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
|
|
@@ -171,6 +171,34 @@ describe('SendConfirmation', () => {
|
|
|
171
171
|
expect(getByTestId('ConfirmButton')).toHaveTextContent('send', { exact: false })
|
|
172
172
|
})
|
|
173
173
|
|
|
174
|
+
it('shows the unknown address warning when the recipient address is not a known app user', () => {
|
|
175
|
+
const { getByTestId } = renderScreen(mockSendConfirmationProps, {
|
|
176
|
+
identity: {
|
|
177
|
+
addressToVerificationStatus: { [mockAccount]: false },
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
expect(getByTestId('UnknownAddressInfo')).toBeTruthy()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('does not show the unknown address warning when the recipient address is verified', () => {
|
|
185
|
+
const { queryByTestId } = renderScreen(mockSendConfirmationProps, {
|
|
186
|
+
identity: {
|
|
187
|
+
addressToVerificationStatus: { [mockAccount]: true },
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
expect(queryByTestId('UnknownAddressInfo')).toBeNull()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('does not show the unknown address warning when the address has not been looked up yet', () => {
|
|
195
|
+
const { queryByTestId } = renderScreen(mockSendConfirmationProps, {
|
|
196
|
+
identity: { addressToVerificationStatus: {} },
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
expect(queryByTestId('UnknownAddressInfo')).toBeNull()
|
|
200
|
+
})
|
|
201
|
+
|
|
174
202
|
it('does not prepare a transaction on load by default', () => {
|
|
175
203
|
renderScreen(mockSendConfirmationProps)
|
|
176
204
|
expect(mockUsePrepareSendTransactionsOutput.clearPreparedTransactions).not.toHaveBeenCalled()
|
|
@@ -10,6 +10,7 @@ import BackButton from 'src/components/BackButton'
|
|
|
10
10
|
import type { BottomSheetModalRefType } from 'src/components/BottomSheet'
|
|
11
11
|
import Button, { BtnSizes } from 'src/components/Button'
|
|
12
12
|
import InfoBottomSheet, { InfoBottomSheetContentBlock } from 'src/components/InfoBottomSheet'
|
|
13
|
+
import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification'
|
|
13
14
|
import {
|
|
14
15
|
buildAmounts,
|
|
15
16
|
ReviewContent,
|
|
@@ -23,11 +24,13 @@ import {
|
|
|
23
24
|
} from 'src/components/ReviewTransaction'
|
|
24
25
|
import { formatValueToDisplay } from 'src/components/TokenDisplay'
|
|
25
26
|
import TokenIcon from 'src/components/TokenIcon'
|
|
27
|
+
import { addressToVerificationStatusSelector } from 'src/identity/selectors'
|
|
26
28
|
import { LocalCurrencySymbol } from 'src/localCurrency/consts'
|
|
27
29
|
import { getLocalCurrencyCode, getLocalCurrencySymbol } from 'src/localCurrency/selectors'
|
|
28
30
|
import { noHeader } from 'src/navigator/Headers'
|
|
29
31
|
import { Screens } from 'src/navigator/Screens'
|
|
30
32
|
import { StackParamList } from 'src/navigator/types'
|
|
33
|
+
import { RecipientType } from 'src/recipients/recipient'
|
|
31
34
|
import { useDispatch, useSelector } from 'src/redux/hooks'
|
|
32
35
|
import { sendPayment } from 'src/send/actions'
|
|
33
36
|
import { isSendingSelector } from 'src/send/selectors'
|
|
@@ -36,11 +39,11 @@ import { NETWORK_NAMES } from 'src/shared/conts'
|
|
|
36
39
|
import { useAmountAsUsd, useTokenInfo, useTokenToLocalAmount } from 'src/tokens/hooks'
|
|
37
40
|
import { feeCurrenciesSelector } from 'src/tokens/selectors'
|
|
38
41
|
import Logger from 'src/utils/Logger'
|
|
39
|
-
import { getFeeCurrencyAndAmounts } from 'src/viem/prepareTransactions'
|
|
40
42
|
import {
|
|
41
43
|
getPreparedTransactionsPossible,
|
|
42
44
|
getSerializablePreparedTransaction,
|
|
43
45
|
} from 'src/viem/preparedTransactionSerialization'
|
|
46
|
+
import { getFeeCurrencyAndAmounts } from 'src/viem/prepareTransactions'
|
|
44
47
|
import { walletAddressSelector } from 'src/web3/selectors'
|
|
45
48
|
|
|
46
49
|
type Props = NativeStackScreenProps<StackParamList, Screens.SendConfirmation>
|
|
@@ -88,6 +91,12 @@ export default function SendConfirmation({ route: { params } }: Props) {
|
|
|
88
91
|
const localAmount = useTokenToLocalAmount(tokenAmount, tokenId)
|
|
89
92
|
const usdAmount = useAmountAsUsd(tokenAmount, tokenId)
|
|
90
93
|
const walletAddress = useSelector(walletAddressSelector)
|
|
94
|
+
const addressToVerificationStatus = useSelector(addressToVerificationStatusSelector)
|
|
95
|
+
const showUnknownAddressInfo =
|
|
96
|
+
recipient.recipientType === RecipientType.Address &&
|
|
97
|
+
!!recipient.address &&
|
|
98
|
+
addressToVerificationStatus[recipient.address] === false
|
|
99
|
+
|
|
91
100
|
const feeCurrencies = useSelector((state) => feeCurrenciesSelector(state, tokenInfo!.networkId))
|
|
92
101
|
const {
|
|
93
102
|
maxFeeAmount,
|
|
@@ -233,6 +242,14 @@ export default function SendConfirmation({ route: { params } }: Props) {
|
|
|
233
242
|
</ReviewContent>
|
|
234
243
|
|
|
235
244
|
<ReviewFooter>
|
|
245
|
+
{showUnknownAddressInfo && (
|
|
246
|
+
<InLineNotification
|
|
247
|
+
variant={NotificationVariant.Info}
|
|
248
|
+
description={t('sendSelectRecipient.unknownAddressInfo')}
|
|
249
|
+
testID="UnknownAddressInfo"
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
252
|
+
|
|
236
253
|
<Button
|
|
237
254
|
testID="ConfirmButton"
|
|
238
255
|
text={t('send')}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { act, fireEvent, render } from '@testing-library/react-native'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import Share from 'react-native-share'
|
|
4
|
+
import { Provider } from 'react-redux'
|
|
5
|
+
import { showError } from 'src/alert/actions'
|
|
6
|
+
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
7
|
+
import { SendEvents } from 'src/analytics/Events'
|
|
8
|
+
import { ErrorMessages } from 'src/app/ErrorMessages'
|
|
9
|
+
import { Screens } from 'src/navigator/Screens'
|
|
10
|
+
import { RecipientType } from 'src/recipients/recipient'
|
|
11
|
+
import SendInvite from 'src/send/SendInvite'
|
|
12
|
+
import { createMockStore, getMockStackScreenProps } from 'test/utils'
|
|
13
|
+
import { mockInvitableRecipient3 } from 'test/values'
|
|
14
|
+
|
|
15
|
+
const shareUrl = 'https://example.test/invite'
|
|
16
|
+
|
|
17
|
+
const mockScreenProps = () =>
|
|
18
|
+
getMockStackScreenProps(Screens.SendInvite, {
|
|
19
|
+
recipient: mockInvitableRecipient3,
|
|
20
|
+
shareUrl,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('SendInvite', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('opens the share sheet, tracks the press analytic, and stays on the screen after the sheet closes', async () => {
|
|
29
|
+
jest
|
|
30
|
+
.mocked(Share.open)
|
|
31
|
+
.mockResolvedValueOnce({ success: true, dismissedAction: false, message: '' })
|
|
32
|
+
|
|
33
|
+
const store = createMockStore({})
|
|
34
|
+
const { getByTestId } = render(
|
|
35
|
+
<Provider store={store}>
|
|
36
|
+
<SendInvite {...mockScreenProps()} />
|
|
37
|
+
</Provider>
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
await act(async () => {
|
|
41
|
+
fireEvent.press(getByTestId('SendInvite/ShareButton'))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_invite_press, {
|
|
45
|
+
recipientType: RecipientType.PhoneNumber,
|
|
46
|
+
})
|
|
47
|
+
expect(Share.open).toHaveBeenCalledWith(
|
|
48
|
+
expect.objectContaining({
|
|
49
|
+
message: expect.stringContaining(shareUrl),
|
|
50
|
+
url: shareUrl,
|
|
51
|
+
failOnCancel: false,
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('dispatches an error toast when the share sheet fails', async () => {
|
|
57
|
+
jest.mocked(Share.open).mockRejectedValueOnce(new Error('no share providers'))
|
|
58
|
+
|
|
59
|
+
const store = createMockStore({})
|
|
60
|
+
const { getByTestId } = render(
|
|
61
|
+
<Provider store={store}>
|
|
62
|
+
<SendInvite {...mockScreenProps()} />
|
|
63
|
+
</Provider>
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
await act(async () => {
|
|
67
|
+
fireEvent.press(getByTestId('SendInvite/ShareButton'))
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(store.getActions()).toContainEqual(showError(ErrorMessages.SHARE_INVITE_FAILED))
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('renders the contact name in the title when the recipient has one', () => {
|
|
74
|
+
const store = createMockStore({})
|
|
75
|
+
const { getByText } = render(
|
|
76
|
+
<Provider store={store}>
|
|
77
|
+
<SendInvite {...mockScreenProps()} />
|
|
78
|
+
</Provider>
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
getByText(`sendInvite.title, {"contact":"${mockInvitableRecipient3.name}"}`)
|
|
83
|
+
).toBeTruthy()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('falls back to the phone number in the title when the recipient has no name', () => {
|
|
87
|
+
const namelessRecipient = {
|
|
88
|
+
displayNumber: mockInvitableRecipient3.displayNumber,
|
|
89
|
+
e164PhoneNumber: mockInvitableRecipient3.e164PhoneNumber,
|
|
90
|
+
recipientType: mockInvitableRecipient3.recipientType,
|
|
91
|
+
}
|
|
92
|
+
const screenProps = getMockStackScreenProps(Screens.SendInvite, {
|
|
93
|
+
recipient: namelessRecipient,
|
|
94
|
+
shareUrl,
|
|
95
|
+
})
|
|
96
|
+
const store = createMockStore({})
|
|
97
|
+
const { getByText } = render(
|
|
98
|
+
<Provider store={store}>
|
|
99
|
+
<SendInvite {...screenProps} />
|
|
100
|
+
</Provider>
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
expect(
|
|
104
|
+
getByText(`sendInvite.title, {"contact":"${mockInvitableRecipient3.displayNumber}"}`)
|
|
105
|
+
).toBeTruthy()
|
|
106
|
+
})
|
|
107
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { 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 Share from 'react-native-share'
|
|
7
|
+
import { showError } from 'src/alert/actions'
|
|
8
|
+
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
9
|
+
import { SendEvents } from 'src/analytics/Events'
|
|
10
|
+
import { ErrorMessages } from 'src/app/ErrorMessages'
|
|
11
|
+
import Button, { BtnSizes } from 'src/components/Button'
|
|
12
|
+
import { inviteModal } from 'src/images/Images'
|
|
13
|
+
import { headerWithBackButton } from 'src/navigator/Headers'
|
|
14
|
+
import { Screens } from 'src/navigator/Screens'
|
|
15
|
+
import { StackParamList } from 'src/navigator/types'
|
|
16
|
+
import { getDisplayName } from 'src/recipients/recipient'
|
|
17
|
+
import { useDispatch } from 'src/redux/hooks'
|
|
18
|
+
import { typeScale } from 'src/styles/fonts'
|
|
19
|
+
import { Spacing } from 'src/styles/styles'
|
|
20
|
+
import Logger from 'src/utils/Logger'
|
|
21
|
+
|
|
22
|
+
type Props = NativeStackScreenProps<StackParamList, Screens.SendInvite>
|
|
23
|
+
|
|
24
|
+
function SendInvite({ route }: Props) {
|
|
25
|
+
const { t } = useTranslation()
|
|
26
|
+
const dispatch = useDispatch()
|
|
27
|
+
const { recipient, shareUrl } = route.params
|
|
28
|
+
const contact = getDisplayName(recipient, t)
|
|
29
|
+
|
|
30
|
+
const handleShareInvite = async () => {
|
|
31
|
+
AppAnalytics.track(SendEvents.send_select_recipient_invite_press, {
|
|
32
|
+
recipientType: recipient.recipientType,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await Share.open({
|
|
37
|
+
message: t('inviteWithSmsMessage.shareMessage', { shareUrl }),
|
|
38
|
+
url: shareUrl,
|
|
39
|
+
failOnCancel: false,
|
|
40
|
+
})
|
|
41
|
+
} catch (error) {
|
|
42
|
+
Logger.error('SendInvite', 'Share sheet failed', error)
|
|
43
|
+
dispatch(showError(ErrorMessages.SHARE_INVITE_FAILED))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
49
|
+
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
50
|
+
<View style={styles.imageContainer}>
|
|
51
|
+
<Image source={inviteModal} resizeMode="contain" />
|
|
52
|
+
</View>
|
|
53
|
+
<Text style={styles.title}>{t('sendInvite.title', { contact })}</Text>
|
|
54
|
+
<Text style={styles.body}>{t('sendInvite.body')}</Text>
|
|
55
|
+
</ScrollView>
|
|
56
|
+
<Button
|
|
57
|
+
testID="SendInvite/ShareButton"
|
|
58
|
+
style={styles.button}
|
|
59
|
+
onPress={handleShareInvite}
|
|
60
|
+
text={t('sendInvite.cta')}
|
|
61
|
+
size={BtnSizes.FULL}
|
|
62
|
+
/>
|
|
63
|
+
</SafeAreaView>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
SendInvite.navigationOptions = headerWithBackButton
|
|
68
|
+
|
|
69
|
+
const styles = StyleSheet.create({
|
|
70
|
+
container: {
|
|
71
|
+
flex: 1,
|
|
72
|
+
justifyContent: 'space-between',
|
|
73
|
+
},
|
|
74
|
+
scrollContent: {
|
|
75
|
+
padding: Spacing.Thick24,
|
|
76
|
+
},
|
|
77
|
+
imageContainer: {
|
|
78
|
+
alignItems: 'center',
|
|
79
|
+
paddingTop: Spacing.Thick24,
|
|
80
|
+
paddingBottom: Spacing.Regular16,
|
|
81
|
+
},
|
|
82
|
+
title: {
|
|
83
|
+
...typeScale.titleMedium,
|
|
84
|
+
textAlign: 'center',
|
|
85
|
+
marginTop: Spacing.Large32,
|
|
86
|
+
paddingHorizontal: Spacing.Regular16,
|
|
87
|
+
},
|
|
88
|
+
body: {
|
|
89
|
+
...typeScale.bodyMedium,
|
|
90
|
+
textAlign: 'center',
|
|
91
|
+
marginTop: Spacing.Regular16,
|
|
92
|
+
paddingHorizontal: Spacing.Regular16,
|
|
93
|
+
},
|
|
94
|
+
button: {
|
|
95
|
+
padding: Spacing.Thick24,
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
export default SendInvite
|