wallet-stack 1.0.0-alpha.131 → 1.0.0-alpha.132
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 +6 -0
- package/package.json +2 -2
- package/src/app/ErrorMessages.ts +1 -0
- package/src/images/Images.ts +1 -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/navigator/Navigator.tsx +6 -0
- package/src/navigator/Screens.tsx +1 -0
- package/src/navigator/types.tsx +1 -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 +30 -138
- package/src/send/SendSelectRecipient.tsx +29 -122
|
@@ -2229,6 +2229,12 @@
|
|
|
2229
2229
|
"invite": "Invite"
|
|
2230
2230
|
}
|
|
2231
2231
|
},
|
|
2232
|
+
"sendInvite": {
|
|
2233
|
+
"title": "Invite {{contact}} to {{appName}}",
|
|
2234
|
+
"body": "Send them a link to {{appName}} so they can get a wallet and start receiving crypto.",
|
|
2235
|
+
"cta": "Share invite",
|
|
2236
|
+
"errorRetry": "Couldn't open the share sheet. Please try again."
|
|
2237
|
+
},
|
|
2232
2238
|
"transactionStatus": {
|
|
2233
2239
|
"transactionIsCompleted": "Completed",
|
|
2234
2240
|
"transactionIsPending": "Pending",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallet-stack",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.132",
|
|
4
4
|
"author": "Valora Inc",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"provenance": true
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=24.15.0"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"tsconfig.json",
|
package/src/app/ErrorMessages.ts
CHANGED
|
@@ -37,4 +37,5 @@ export enum ErrorMessages {
|
|
|
37
37
|
KYC_TRY_AGAIN_FAILED = 'fiatConnectKycStatusScreen.tryAgainFailed',
|
|
38
38
|
HOOKS_INVALID_PREVIEW_API_URL = 'hooksPreview.invalidApiUrl',
|
|
39
39
|
SHORTCUT_CLAIM_REWARD_FAILED = 'dappShortcuts.claimRewardFailure',
|
|
40
|
+
SHARE_INVITE_FAILED = 'sendInvite.errorRetry',
|
|
40
41
|
}
|
package/src/images/Images.ts
CHANGED
|
@@ -14,6 +14,7 @@ export const celoEducation4 = require('src/images/assets/celo-education-4.png')
|
|
|
14
14
|
export const email = require('src/images/assets/email.png')
|
|
15
15
|
export const fiatExchange = require('src/images/assets/fiat-exchange.png')
|
|
16
16
|
export const getVerified = require('src/images/assets/get-verified.png')
|
|
17
|
+
export const inviteModal = require('src/images/assets/invite-modal.png')
|
|
17
18
|
export const learnCelo = require('src/images/assets/learn-celo.png')
|
|
18
19
|
export const pointsCardBackground = require('src/images/assets/points-card-background.png')
|
|
19
20
|
export const pointsIllustration = require('src/images/assets/points-illustration.png')
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -104,6 +104,7 @@ import { RootState } from 'src/redux/reducers'
|
|
|
104
104
|
import { store } from 'src/redux/store'
|
|
105
105
|
import SendConfirmation, { sendConfirmationScreenNavOptions } from 'src/send/SendConfirmation'
|
|
106
106
|
import SendEnterAmount from 'src/send/SendEnterAmount'
|
|
107
|
+
import SendInvite from 'src/send/SendInvite'
|
|
107
108
|
import SendSelectRecipient from 'src/send/SendSelectRecipient'
|
|
108
109
|
import ValidateRecipientAccount, {
|
|
109
110
|
validateRecipientAccountScreenNavOptions,
|
|
@@ -256,6 +257,11 @@ const sendScreens = (Navigator: typeof Stack) => (
|
|
|
256
257
|
component={SendEnterAmount}
|
|
257
258
|
options={noHeader}
|
|
258
259
|
/>
|
|
260
|
+
<Navigator.Screen
|
|
261
|
+
name={Screens.SendInvite}
|
|
262
|
+
component={SendInvite}
|
|
263
|
+
options={SendInvite.navigationOptions}
|
|
264
|
+
/>
|
|
259
265
|
</>
|
|
260
266
|
)
|
|
261
267
|
|
|
@@ -75,6 +75,7 @@ export enum Screens {
|
|
|
75
75
|
SelectCountry = 'SelectCountry',
|
|
76
76
|
SelectLocalCurrency = 'SelectLocalCurrency',
|
|
77
77
|
SelectProvider = 'SelectProvider',
|
|
78
|
+
SendInvite = 'SendInvite',
|
|
78
79
|
SendSelectRecipient = 'SendSelectRecipient',
|
|
79
80
|
SendConfirmation = 'SendConfirmation',
|
|
80
81
|
SendEnterAmount = 'SendEnterAmount',
|
package/src/navigator/types.tsx
CHANGED
|
@@ -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
|
|
@@ -2,7 +2,6 @@ import Clipboard from '@react-native-clipboard/clipboard'
|
|
|
2
2
|
import { act, fireEvent, render, waitFor } from '@testing-library/react-native'
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Provider } from 'react-redux'
|
|
5
|
-
import Share from 'react-native-share'
|
|
6
5
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
7
6
|
import { SendEvents } from 'src/analytics/Events'
|
|
8
7
|
import { SendOrigin } from 'src/analytics/types'
|
|
@@ -204,7 +203,7 @@ describe('SendSelectRecipient', () => {
|
|
|
204
203
|
})
|
|
205
204
|
expect(getByTestId('SelectRecipient/NoResults')).toBeTruthy()
|
|
206
205
|
})
|
|
207
|
-
it('navigates to send amount when
|
|
206
|
+
it('navigates to send amount when a verified phone recipient is tapped in search results', async () => {
|
|
208
207
|
jest
|
|
209
208
|
.mocked(getRecipientVerificationStatus)
|
|
210
209
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -225,15 +224,6 @@ describe('SendSelectRecipient', () => {
|
|
|
225
224
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
226
225
|
})
|
|
227
226
|
|
|
228
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
229
|
-
expect(getByTestId('SendOrInviteButton')).toHaveTextContent(
|
|
230
|
-
'sendSelectRecipient.buttons.send',
|
|
231
|
-
{ exact: false }
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
await act(() => {
|
|
235
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
236
|
-
})
|
|
237
227
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
238
228
|
recipientType: RecipientType.PhoneNumber,
|
|
239
229
|
})
|
|
@@ -247,7 +237,7 @@ describe('SendSelectRecipient', () => {
|
|
|
247
237
|
isMiniPayRecipient: false,
|
|
248
238
|
})
|
|
249
239
|
})
|
|
250
|
-
it('navigates to send amount when address
|
|
240
|
+
it('navigates to send amount when an address is tapped and the user phone number is not verified', async () => {
|
|
251
241
|
const store = createMockStore(defaultStore)
|
|
252
242
|
|
|
253
243
|
const { getByTestId } = render(
|
|
@@ -264,15 +254,6 @@ describe('SendSelectRecipient', () => {
|
|
|
264
254
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
265
255
|
})
|
|
266
256
|
|
|
267
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
268
|
-
expect(getByTestId('SendOrInviteButton')).toHaveTextContent(
|
|
269
|
-
'sendSelectRecipient.buttons.send',
|
|
270
|
-
{ exact: false }
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
await act(() => {
|
|
274
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
275
|
-
})
|
|
276
257
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
277
258
|
recipientType: RecipientType.Address,
|
|
278
259
|
})
|
|
@@ -286,14 +267,14 @@ describe('SendSelectRecipient', () => {
|
|
|
286
267
|
})
|
|
287
268
|
})
|
|
288
269
|
|
|
289
|
-
it('
|
|
270
|
+
it('dispatches address verification when an address is tapped and the user phone number is verified', async () => {
|
|
290
271
|
jest
|
|
291
272
|
.mocked(getRecipientVerificationStatus)
|
|
292
273
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
293
274
|
|
|
294
275
|
const store = createMockStore(storeWithPhoneVerified)
|
|
295
276
|
|
|
296
|
-
const { getByTestId
|
|
277
|
+
const { getByTestId } = render(
|
|
297
278
|
<Provider store={store}>
|
|
298
279
|
<SendSelectRecipient {...mockScreenProps({})} />
|
|
299
280
|
</Provider>
|
|
@@ -317,17 +298,15 @@ describe('SendSelectRecipient', () => {
|
|
|
317
298
|
})
|
|
318
299
|
|
|
319
300
|
expect(store.getActions()).toEqual([fetchAddressVerification(mockAccount2.toLowerCase())])
|
|
320
|
-
expect(queryByTestId('UnknownAddressInfo')).toBeFalsy()
|
|
321
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
322
301
|
})
|
|
323
|
-
it('does not
|
|
302
|
+
it('does not navigate when an unverified phone recipient is tapped and no share URL is configured', async () => {
|
|
324
303
|
jest
|
|
325
304
|
.mocked(getRecipientVerificationStatus)
|
|
326
305
|
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
|
|
327
306
|
|
|
328
307
|
const store = createMockStore(storeWithPhoneVerified)
|
|
329
308
|
|
|
330
|
-
const { getByTestId
|
|
309
|
+
const { getByTestId } = render(
|
|
331
310
|
<Provider store={store}>
|
|
332
311
|
<SendSelectRecipient {...mockScreenProps({})} />
|
|
333
312
|
</Provider>
|
|
@@ -348,11 +327,10 @@ describe('SendSelectRecipient', () => {
|
|
|
348
327
|
})
|
|
349
328
|
|
|
350
329
|
expect(store.getActions()).toEqual([fetchAddressesAndValidate(mockE164Number2Invite)])
|
|
351
|
-
expect(
|
|
352
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
330
|
+
expect(navigate).not.toHaveBeenCalled()
|
|
353
331
|
})
|
|
354
332
|
|
|
355
|
-
it('
|
|
333
|
+
it('navigates to the invite screen when an unverified phone number is tapped and share URL is configured', async () => {
|
|
356
334
|
const shareUrl = 'https://example.test/invite'
|
|
357
335
|
jest.mocked(getAppConfig).mockReturnValue({
|
|
358
336
|
displayName: 'Test App',
|
|
@@ -367,20 +345,6 @@ describe('SendSelectRecipient', () => {
|
|
|
367
345
|
.mocked(getRecipientVerificationStatus)
|
|
368
346
|
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
|
|
369
347
|
|
|
370
|
-
let resolveShare!: (value: {
|
|
371
|
-
success: boolean
|
|
372
|
-
dismissedAction: boolean
|
|
373
|
-
message: string
|
|
374
|
-
}) => void
|
|
375
|
-
const sharePromise = new Promise<{
|
|
376
|
-
success: boolean
|
|
377
|
-
dismissedAction: boolean
|
|
378
|
-
message: string
|
|
379
|
-
}>((resolve) => {
|
|
380
|
-
resolveShare = resolve
|
|
381
|
-
})
|
|
382
|
-
jest.mocked(Share.open).mockReturnValueOnce(sharePromise)
|
|
383
|
-
|
|
384
348
|
const store = createMockStore(storeWithPhoneVerified)
|
|
385
349
|
|
|
386
350
|
const { getByTestId } = render(
|
|
@@ -397,33 +361,19 @@ describe('SendSelectRecipient', () => {
|
|
|
397
361
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
398
362
|
})
|
|
399
363
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
364
|
+
expect(navigate).toHaveBeenCalledWith(Screens.SendInvite, {
|
|
365
|
+
recipient: expect.objectContaining({
|
|
366
|
+
e164PhoneNumber: mockE164Number2Invite,
|
|
367
|
+
recipientType: RecipientType.PhoneNumber,
|
|
368
|
+
}),
|
|
369
|
+
shareUrl,
|
|
405
370
|
})
|
|
406
371
|
|
|
407
|
-
//
|
|
408
|
-
expect(
|
|
409
|
-
recipientType: RecipientType.PhoneNumber,
|
|
410
|
-
})
|
|
411
|
-
expect(Share.open).toHaveBeenCalledWith(
|
|
412
|
-
expect.objectContaining({
|
|
413
|
-
message: expect.stringContaining(shareUrl),
|
|
414
|
-
url: shareUrl,
|
|
415
|
-
failOnCancel: false,
|
|
416
|
-
})
|
|
417
|
-
)
|
|
418
|
-
expect(navigate).not.toHaveBeenCalled()
|
|
419
|
-
|
|
420
|
-
await act(async () => {
|
|
421
|
-
resolveShare({ success: true, dismissedAction: false, message: '' })
|
|
422
|
-
await sharePromise
|
|
423
|
-
})
|
|
372
|
+
// Search text is preserved so the user can return to the same picker state.
|
|
373
|
+
expect(searchInput.props.value).toBe(mockE164Number2Invite)
|
|
424
374
|
})
|
|
425
375
|
|
|
426
|
-
it('
|
|
376
|
+
it('navigates and dispatches address verification when an unknown address is tapped and the user phone number is verified', async () => {
|
|
427
377
|
jest
|
|
428
378
|
.mocked(getRecipientVerificationStatus)
|
|
429
379
|
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
|
|
@@ -455,10 +405,12 @@ describe('SendSelectRecipient', () => {
|
|
|
455
405
|
})
|
|
456
406
|
|
|
457
407
|
expect(store.getActions()).toEqual([fetchAddressVerification(mockAccount2.toLowerCase())])
|
|
458
|
-
expect(
|
|
459
|
-
|
|
408
|
+
expect(navigate).toHaveBeenCalledWith(
|
|
409
|
+
Screens.SendEnterAmount,
|
|
410
|
+
expect.objectContaining({ origin: SendOrigin.AppSendFlow })
|
|
411
|
+
)
|
|
460
412
|
})
|
|
461
|
-
it('
|
|
413
|
+
it('skips verification request for an address when the user phone number is not verified', async () => {
|
|
462
414
|
const store = createMockStore(defaultStore)
|
|
463
415
|
|
|
464
416
|
const { getByTestId } = render(
|
|
@@ -486,43 +438,11 @@ describe('SendSelectRecipient', () => {
|
|
|
486
438
|
})
|
|
487
439
|
|
|
488
440
|
expect(store.getActions()).toEqual([])
|
|
489
|
-
|
|
490
|
-
expect(
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
jest
|
|
494
|
-
.mocked(getRecipientVerificationStatus)
|
|
495
|
-
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
|
|
496
|
-
|
|
497
|
-
const store = createMockStore(storeWithPhoneVerified)
|
|
498
|
-
|
|
499
|
-
const { getByTestId } = render(
|
|
500
|
-
<Provider store={store}>
|
|
501
|
-
<SendSelectRecipient {...mockScreenProps({})} />
|
|
502
|
-
</Provider>
|
|
441
|
+
// Navigation still proceeds — status is treated as UNVERIFIED locally.
|
|
442
|
+
expect(navigate).toHaveBeenCalledWith(
|
|
443
|
+
Screens.SendEnterAmount,
|
|
444
|
+
expect.objectContaining({ origin: SendOrigin.AppSendFlow })
|
|
503
445
|
)
|
|
504
|
-
await waitFor(() => {
|
|
505
|
-
expect(getByTestId('SendSelectRecipientSearchInput')).toBeTruthy()
|
|
506
|
-
})
|
|
507
|
-
const searchInput = getByTestId('SendSelectRecipientSearchInput')
|
|
508
|
-
|
|
509
|
-
await act(() => {
|
|
510
|
-
fireEvent.changeText(searchInput, mockAccount)
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
expect(getByTestId('RecipientItem')).toHaveTextContent(mockRecipient.name, { exact: false })
|
|
514
|
-
expect(getByTestId('RecipientItem')).toHaveTextContent(mockRecipient.displayNumber, {
|
|
515
|
-
exact: false,
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
await act(() => {
|
|
519
|
-
fireEvent.press(getByTestId('RecipientItem'))
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
expect(store.getActions()).toEqual([fetchAddressVerification(mockAccount)])
|
|
523
|
-
expect(getByTestId('UnknownAddressInfo')).toBeTruthy()
|
|
524
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
525
|
-
expect(getByTestId('SendOrInviteButton')).toHaveTextContent('send', { exact: false })
|
|
526
446
|
})
|
|
527
447
|
it('shows paste button if clipboard has address content', async () => {
|
|
528
448
|
const store = createMockStore(defaultStore)
|
|
@@ -548,7 +468,7 @@ describe('SendSelectRecipient', () => {
|
|
|
548
468
|
await expect(pasteButtonAfterPress).rejects.toThrow()
|
|
549
469
|
})
|
|
550
470
|
|
|
551
|
-
it('navigates to send amount when phone
|
|
471
|
+
it('navigates to send amount when a verified phone recipient with a single address is tapped', async () => {
|
|
552
472
|
jest
|
|
553
473
|
.mocked(getRecipientVerificationStatus)
|
|
554
474
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -577,11 +497,6 @@ describe('SendSelectRecipient', () => {
|
|
|
577
497
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
578
498
|
})
|
|
579
499
|
|
|
580
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
581
|
-
|
|
582
|
-
await act(() => {
|
|
583
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
584
|
-
})
|
|
585
500
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
586
501
|
recipientType: RecipientType.PhoneNumber,
|
|
587
502
|
})
|
|
@@ -599,7 +514,7 @@ describe('SendSelectRecipient', () => {
|
|
|
599
514
|
isMiniPayRecipient: false,
|
|
600
515
|
})
|
|
601
516
|
})
|
|
602
|
-
it('navigates
|
|
517
|
+
it('navigates with isMiniPayRecipient when address is verified by minipay', async () => {
|
|
603
518
|
jest
|
|
604
519
|
.mocked(getRecipientVerificationStatus)
|
|
605
520
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -628,9 +543,6 @@ describe('SendSelectRecipient', () => {
|
|
|
628
543
|
await act(() => {
|
|
629
544
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
630
545
|
})
|
|
631
|
-
await act(() => {
|
|
632
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
633
|
-
})
|
|
634
546
|
|
|
635
547
|
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
|
|
636
548
|
isFromScan: false,
|
|
@@ -646,7 +558,7 @@ describe('SendSelectRecipient', () => {
|
|
|
646
558
|
isMiniPayRecipient: true,
|
|
647
559
|
})
|
|
648
560
|
})
|
|
649
|
-
it('navigates to secure send flow when phone
|
|
561
|
+
it('navigates to secure send flow when phone recipient has multiple addresses, first time seeing it', async () => {
|
|
650
562
|
jest
|
|
651
563
|
.mocked(getRecipientVerificationStatus)
|
|
652
564
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -681,11 +593,6 @@ describe('SendSelectRecipient', () => {
|
|
|
681
593
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
682
594
|
})
|
|
683
595
|
|
|
684
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
685
|
-
|
|
686
|
-
await act(() => {
|
|
687
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
688
|
-
})
|
|
689
596
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
690
597
|
recipientType: RecipientType.PhoneNumber,
|
|
691
598
|
})
|
|
@@ -696,7 +603,7 @@ describe('SendSelectRecipient', () => {
|
|
|
696
603
|
origin: SendOrigin.AppSendFlow,
|
|
697
604
|
})
|
|
698
605
|
})
|
|
699
|
-
it('navigates to send
|
|
606
|
+
it('navigates to send amount when phone recipient has multiple addresses and secure send was already done', async () => {
|
|
700
607
|
jest
|
|
701
608
|
.mocked(getRecipientVerificationStatus)
|
|
702
609
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -734,11 +641,6 @@ describe('SendSelectRecipient', () => {
|
|
|
734
641
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
735
642
|
})
|
|
736
643
|
|
|
737
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
738
|
-
|
|
739
|
-
await act(() => {
|
|
740
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
741
|
-
})
|
|
742
644
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
743
645
|
recipientType: RecipientType.PhoneNumber,
|
|
744
646
|
})
|
|
@@ -795,11 +697,6 @@ describe('SendSelectRecipient', () => {
|
|
|
795
697
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
796
698
|
})
|
|
797
699
|
|
|
798
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
799
|
-
|
|
800
|
-
await act(() => {
|
|
801
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
802
|
-
})
|
|
803
700
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
804
701
|
recipientType: RecipientType.Address,
|
|
805
702
|
})
|
|
@@ -861,11 +758,6 @@ describe('SendSelectRecipient', () => {
|
|
|
861
758
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
862
759
|
})
|
|
863
760
|
|
|
864
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
865
|
-
|
|
866
|
-
await act(() => {
|
|
867
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
868
|
-
})
|
|
869
761
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
870
762
|
recipientType: RecipientType.Address,
|
|
871
763
|
})
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
2
|
-
import React, { useState } from 'react'
|
|
2
|
+
import React, { useEffect, useState } from 'react'
|
|
3
3
|
import { useTranslation } from 'react-i18next'
|
|
4
4
|
import { StyleSheet, Text, View } from 'react-native'
|
|
5
5
|
import { getFontScaleSync } from 'react-native-device-info'
|
|
6
6
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
7
|
-
import Share from 'react-native-share'
|
|
8
7
|
import { isAddressFormat } from 'src/account/utils'
|
|
9
8
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
10
9
|
import { SendEvents } from 'src/analytics/Events'
|
|
11
10
|
import { SendOrigin } from 'src/analytics/types'
|
|
12
11
|
import { getAppConfig } from 'src/appConfig'
|
|
13
12
|
import BackButton from 'src/components/BackButton'
|
|
14
|
-
import Button, { BtnSizes } from 'src/components/Button'
|
|
15
|
-
import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification'
|
|
16
13
|
import KeyboardAwareScrollView from 'src/components/KeyboardAwareScrollView'
|
|
17
14
|
import CustomHeader from 'src/components/header/CustomHeader'
|
|
18
15
|
import CircledIcon from 'src/icons/CircledIcon'
|
|
@@ -42,7 +39,6 @@ import colors from 'src/styles/colors'
|
|
|
42
39
|
import { typeScale } from 'src/styles/fonts'
|
|
43
40
|
import { Spacing } from 'src/styles/styles'
|
|
44
41
|
import variables from 'src/styles/variables'
|
|
45
|
-
import Logger from 'src/utils/Logger'
|
|
46
42
|
|
|
47
43
|
type Props = NativeStackScreenProps<StackParamList, Screens.SendSelectRecipient>
|
|
48
44
|
|
|
@@ -167,45 +163,6 @@ const getStartedStyles = StyleSheet.create({
|
|
|
167
163
|
},
|
|
168
164
|
})
|
|
169
165
|
|
|
170
|
-
function SendOrInviteButton({
|
|
171
|
-
recipient,
|
|
172
|
-
recipientVerificationStatus,
|
|
173
|
-
shareUrl,
|
|
174
|
-
onPress,
|
|
175
|
-
}: {
|
|
176
|
-
recipient: Recipient | null
|
|
177
|
-
recipientVerificationStatus: RecipientVerificationStatus
|
|
178
|
-
shareUrl: string | null
|
|
179
|
-
onPress: (shouldInviteRecipient: boolean) => void
|
|
180
|
-
}) {
|
|
181
|
-
const { t } = useTranslation()
|
|
182
|
-
|
|
183
|
-
const sendOrInviteButtonDisabled =
|
|
184
|
-
(!!recipient && recipientVerificationStatus === RecipientVerificationStatus.UNKNOWN) ||
|
|
185
|
-
// If the phone number is present and unverified and no share URL is found, disable the send/invite button
|
|
186
|
-
(recipient?.recipientType === RecipientType.PhoneNumber &&
|
|
187
|
-
recipientVerificationStatus === RecipientVerificationStatus.UNVERIFIED &&
|
|
188
|
-
!shareUrl)
|
|
189
|
-
|
|
190
|
-
const shouldInviteRecipient =
|
|
191
|
-
!sendOrInviteButtonDisabled &&
|
|
192
|
-
recipient?.recipientType === RecipientType.PhoneNumber &&
|
|
193
|
-
recipientVerificationStatus === RecipientVerificationStatus.UNVERIFIED
|
|
194
|
-
return (
|
|
195
|
-
<Button
|
|
196
|
-
testID="SendOrInviteButton"
|
|
197
|
-
style={styles.sendOrInviteButton}
|
|
198
|
-
onPress={() => onPress(shouldInviteRecipient)}
|
|
199
|
-
disabled={sendOrInviteButtonDisabled}
|
|
200
|
-
text={
|
|
201
|
-
shouldInviteRecipient
|
|
202
|
-
? t('sendSelectRecipient.buttons.invite')
|
|
203
|
-
: t('sendSelectRecipient.buttons.send')
|
|
204
|
-
}
|
|
205
|
-
size={BtnSizes.FULL}
|
|
206
|
-
/>
|
|
207
|
-
)
|
|
208
|
-
}
|
|
209
166
|
enum SelectRecipientView {
|
|
210
167
|
Recent = 'Recent',
|
|
211
168
|
Contacts = 'Contacts',
|
|
@@ -222,19 +179,14 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
222
179
|
const forceTokenId = route.params?.forceTokenId
|
|
223
180
|
const defaultTokenIdOverride = route.params?.defaultTokenIdOverride
|
|
224
181
|
|
|
225
|
-
const [showSendOrInviteButton, setShowSendOrInviteButton] = useState(false)
|
|
226
|
-
|
|
227
182
|
const [showSearchResults, setShowSearchResults] = useState(false)
|
|
228
183
|
|
|
229
184
|
const [activeView, setActiveView] = useState(SelectRecipientView.Recent)
|
|
230
185
|
|
|
231
186
|
const onSearch = (searchQuery: string) => {
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// where the button appears but is bound to a recipient that is
|
|
235
|
-
// not present on the page.
|
|
187
|
+
// Clear any in-flight selection so a stale recipient can't auto-navigate
|
|
188
|
+
// once the user starts typing a different query.
|
|
236
189
|
unsetSelectedRecipient()
|
|
237
|
-
setShowSendOrInviteButton(false)
|
|
238
190
|
setShowSearchResults(!!searchQuery)
|
|
239
191
|
}
|
|
240
192
|
const { contactRecipients, recentRecipients } = useSendRecipients()
|
|
@@ -243,17 +195,31 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
243
195
|
const { recipientVerificationStatus, recipient, setSelectedRecipient, unsetSelectedRecipient } =
|
|
244
196
|
useFetchRecipientVerificationStatus()
|
|
245
197
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
recipient
|
|
250
|
-
|
|
251
|
-
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
// Auto-navigate once verification resolves. The picker stays mounted so the
|
|
200
|
+
// user's search text and selection are preserved when they come back.
|
|
201
|
+
if (!recipient || recipientVerificationStatus === RecipientVerificationStatus.UNKNOWN) {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
252
204
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
205
|
+
const isUnverifiedPhone =
|
|
206
|
+
recipient.recipientType === RecipientType.PhoneNumber &&
|
|
207
|
+
recipientVerificationStatus === RecipientVerificationStatus.UNVERIFIED
|
|
208
|
+
|
|
209
|
+
if (isUnverifiedPhone) {
|
|
210
|
+
if (shareUrl) {
|
|
211
|
+
navigate(Screens.SendInvite, { recipient, shareUrl })
|
|
212
|
+
}
|
|
213
|
+
// Without shareUrl there's no invite flow and no send flow for an
|
|
214
|
+
// unverified phone — stay on the picker so the user can pick someone else.
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
AppAnalytics.track(SendEvents.send_select_recipient_send_press, {
|
|
219
|
+
recipientType: recipient.recipientType,
|
|
220
|
+
})
|
|
221
|
+
nextScreen(recipient)
|
|
222
|
+
}, [recipient, recipientVerificationStatus, shareUrl])
|
|
257
223
|
|
|
258
224
|
const onContactsPermissionGranted = () => {
|
|
259
225
|
dispatch(importContacts())
|
|
@@ -268,7 +234,6 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
268
234
|
AppAnalytics.track(SendEvents.send_select_recipient_recent_press, {
|
|
269
235
|
recipientType: recentRecipient.recipientType,
|
|
270
236
|
})
|
|
271
|
-
setSelectedRecipient(recentRecipient)
|
|
272
237
|
nextScreen(recentRecipient)
|
|
273
238
|
}
|
|
274
239
|
|
|
@@ -320,40 +285,6 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
320
285
|
})
|
|
321
286
|
}
|
|
322
287
|
|
|
323
|
-
const onPressSendOrInvite = async (shouldInviteRecipient: boolean) => {
|
|
324
|
-
if (!recipient) return
|
|
325
|
-
|
|
326
|
-
// Invites
|
|
327
|
-
if (shouldInviteRecipient) {
|
|
328
|
-
if (!shareUrl) {
|
|
329
|
-
Logger.warn('SendSelectRecipient', 'No share URL found for invite')
|
|
330
|
-
return
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
AppAnalytics.track(SendEvents.send_select_recipient_invite_press, {
|
|
334
|
-
recipientType: recipient.recipientType,
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
await Share.open({
|
|
339
|
-
message: t('inviteWithSmsMessage.shareMessage', { shareUrl }),
|
|
340
|
-
url: shareUrl,
|
|
341
|
-
failOnCancel: false,
|
|
342
|
-
})
|
|
343
|
-
} catch (error) {
|
|
344
|
-
Logger.warn('SendSelectRecipient', 'Share sheet failed', error)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Sends
|
|
351
|
-
AppAnalytics.track(SendEvents.send_select_recipient_send_press, {
|
|
352
|
-
recipientType: recipient.recipientType,
|
|
353
|
-
})
|
|
354
|
-
nextScreen(recipient)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
288
|
const renderSearchResults = () => {
|
|
358
289
|
if (mergedRecipients.length) {
|
|
359
290
|
return (
|
|
@@ -362,7 +293,7 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
362
293
|
<RecipientPicker
|
|
363
294
|
testID={'SelectRecipient/AllRecipientsPicker'}
|
|
364
295
|
recipients={mergedRecipients}
|
|
365
|
-
onSelectRecipient={
|
|
296
|
+
onSelectRecipient={setSelectedRecipient}
|
|
366
297
|
selectedRecipient={recipient}
|
|
367
298
|
isSelectedRecipientLoading={
|
|
368
299
|
!!recipient && recipientVerificationStatus === RecipientVerificationStatus.UNKNOWN
|
|
@@ -409,7 +340,7 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
409
340
|
<RecipientPicker
|
|
410
341
|
testID={'SelectRecipient/ContactRecipientPicker'}
|
|
411
342
|
recipients={contactRecipients}
|
|
412
|
-
onSelectRecipient={
|
|
343
|
+
onSelectRecipient={setSelectedRecipient}
|
|
413
344
|
selectedRecipient={recipient}
|
|
414
345
|
isSelectedRecipientLoading={
|
|
415
346
|
!!recipient && recipientVerificationStatus === RecipientVerificationStatus.UNKNOWN
|
|
@@ -439,22 +370,6 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
439
370
|
</>
|
|
440
371
|
)}
|
|
441
372
|
</KeyboardAwareScrollView>
|
|
442
|
-
{showUnknownAddressInfo && (
|
|
443
|
-
<InLineNotification
|
|
444
|
-
variant={NotificationVariant.Info}
|
|
445
|
-
description={t('sendSelectRecipient.unknownAddressInfo')}
|
|
446
|
-
testID="UnknownAddressInfo"
|
|
447
|
-
style={styles.unknownAddressInfo}
|
|
448
|
-
/>
|
|
449
|
-
)}
|
|
450
|
-
{showSendOrInviteButton && (
|
|
451
|
-
<SendOrInviteButton
|
|
452
|
-
recipient={recipient}
|
|
453
|
-
recipientVerificationStatus={recipientVerificationStatus}
|
|
454
|
-
onPress={onPressSendOrInvite}
|
|
455
|
-
shareUrl={shareUrl}
|
|
456
|
-
/>
|
|
457
|
-
)}
|
|
458
373
|
</SafeAreaView>
|
|
459
374
|
)
|
|
460
375
|
}
|
|
@@ -496,14 +411,6 @@ const styles = StyleSheet.create({
|
|
|
496
411
|
padding: Spacing.Regular16,
|
|
497
412
|
textAlign: 'center',
|
|
498
413
|
},
|
|
499
|
-
unknownAddressInfo: {
|
|
500
|
-
margin: Spacing.Regular16,
|
|
501
|
-
marginBottom: variables.contentPadding,
|
|
502
|
-
},
|
|
503
|
-
sendOrInviteButton: {
|
|
504
|
-
margin: Spacing.Regular16,
|
|
505
|
-
marginTop: variables.contentPadding,
|
|
506
|
-
},
|
|
507
414
|
})
|
|
508
415
|
|
|
509
416
|
export default SendSelectRecipient
|