wallet-stack 1.0.0-alpha.130 → 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 +8 -1
- 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/SendEnterAmount.test.tsx +3 -3
- package/src/send/SendInvite.test.tsx +107 -0
- package/src/send/SendInvite.tsx +99 -0
- package/src/send/SendSelectRecipient.test.tsx +60 -96
- package/src/send/SendSelectRecipient.tsx +29 -120
- package/src/send/useSendFilterChips.ts +4 -1
|
@@ -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",
|
|
@@ -2269,7 +2275,8 @@
|
|
|
2269
2275
|
"insufficientBalanceWarning": {
|
|
2270
2276
|
"title": "You need more {{tokenSymbol}}",
|
|
2271
2277
|
"description": "Insufficient {{tokenSymbol}} balance. Try a smaller amount or choose a different asset."
|
|
2272
|
-
}
|
|
2278
|
+
},
|
|
2279
|
+
"miniPayFilterChip": "MiniPay supported"
|
|
2273
2280
|
},
|
|
2274
2281
|
"jumpstartIntro": {
|
|
2275
2282
|
"title": "Share crypto like a text",
|
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')}
|
|
@@ -258,7 +258,7 @@ describe('SendEnterAmount', () => {
|
|
|
258
258
|
</Provider>
|
|
259
259
|
)
|
|
260
260
|
|
|
261
|
-
expect(getByText('
|
|
261
|
+
expect(getByText('sendEnterAmountScreen.miniPayFilterChip')).toBeTruthy()
|
|
262
262
|
|
|
263
263
|
const tokenBottomSheet = getAllByTestId('TokenBottomSheet')[0]
|
|
264
264
|
const tokens = within(tokenBottomSheet).getAllByTestId('TokenBalanceItem')
|
|
@@ -291,7 +291,7 @@ describe('SendEnterAmount', () => {
|
|
|
291
291
|
</Provider>
|
|
292
292
|
)
|
|
293
293
|
|
|
294
|
-
fireEvent.press(getByText('
|
|
294
|
+
fireEvent.press(getByText('sendEnterAmountScreen.miniPayFilterChip'))
|
|
295
295
|
|
|
296
296
|
const tokenBottomSheet = getAllByTestId('TokenBottomSheet')[0]
|
|
297
297
|
const tokens = within(tokenBottomSheet).getAllByTestId('TokenBalanceItem')
|
|
@@ -305,7 +305,7 @@ describe('SendEnterAmount', () => {
|
|
|
305
305
|
</Provider>
|
|
306
306
|
)
|
|
307
307
|
|
|
308
|
-
expect(queryByText('
|
|
308
|
+
expect(queryByText('sendEnterAmountScreen.miniPayFilterChip')).toBeFalsy()
|
|
309
309
|
})
|
|
310
310
|
|
|
311
311
|
it('should include isMiniPayRecipient in send_amount_continue analytics', async () => {
|
|
@@ -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
|
|
@@ -203,7 +203,7 @@ describe('SendSelectRecipient', () => {
|
|
|
203
203
|
})
|
|
204
204
|
expect(getByTestId('SelectRecipient/NoResults')).toBeTruthy()
|
|
205
205
|
})
|
|
206
|
-
it('navigates to send amount when
|
|
206
|
+
it('navigates to send amount when a verified phone recipient is tapped in search results', async () => {
|
|
207
207
|
jest
|
|
208
208
|
.mocked(getRecipientVerificationStatus)
|
|
209
209
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -224,15 +224,6 @@ describe('SendSelectRecipient', () => {
|
|
|
224
224
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
225
225
|
})
|
|
226
226
|
|
|
227
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
228
|
-
expect(getByTestId('SendOrInviteButton')).toHaveTextContent(
|
|
229
|
-
'sendSelectRecipient.buttons.send',
|
|
230
|
-
{ exact: false }
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
await act(() => {
|
|
234
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
235
|
-
})
|
|
236
227
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
237
228
|
recipientType: RecipientType.PhoneNumber,
|
|
238
229
|
})
|
|
@@ -246,7 +237,7 @@ describe('SendSelectRecipient', () => {
|
|
|
246
237
|
isMiniPayRecipient: false,
|
|
247
238
|
})
|
|
248
239
|
})
|
|
249
|
-
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 () => {
|
|
250
241
|
const store = createMockStore(defaultStore)
|
|
251
242
|
|
|
252
243
|
const { getByTestId } = render(
|
|
@@ -263,15 +254,6 @@ describe('SendSelectRecipient', () => {
|
|
|
263
254
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
264
255
|
})
|
|
265
256
|
|
|
266
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
267
|
-
expect(getByTestId('SendOrInviteButton')).toHaveTextContent(
|
|
268
|
-
'sendSelectRecipient.buttons.send',
|
|
269
|
-
{ exact: false }
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
await act(() => {
|
|
273
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
274
|
-
})
|
|
275
257
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
276
258
|
recipientType: RecipientType.Address,
|
|
277
259
|
})
|
|
@@ -285,14 +267,14 @@ describe('SendSelectRecipient', () => {
|
|
|
285
267
|
})
|
|
286
268
|
})
|
|
287
269
|
|
|
288
|
-
it('
|
|
270
|
+
it('dispatches address verification when an address is tapped and the user phone number is verified', async () => {
|
|
289
271
|
jest
|
|
290
272
|
.mocked(getRecipientVerificationStatus)
|
|
291
273
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
292
274
|
|
|
293
275
|
const store = createMockStore(storeWithPhoneVerified)
|
|
294
276
|
|
|
295
|
-
const { getByTestId
|
|
277
|
+
const { getByTestId } = render(
|
|
296
278
|
<Provider store={store}>
|
|
297
279
|
<SendSelectRecipient {...mockScreenProps({})} />
|
|
298
280
|
</Provider>
|
|
@@ -316,17 +298,15 @@ describe('SendSelectRecipient', () => {
|
|
|
316
298
|
})
|
|
317
299
|
|
|
318
300
|
expect(store.getActions()).toEqual([fetchAddressVerification(mockAccount2.toLowerCase())])
|
|
319
|
-
expect(queryByTestId('UnknownAddressInfo')).toBeFalsy()
|
|
320
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
321
301
|
})
|
|
322
|
-
it('does not
|
|
302
|
+
it('does not navigate when an unverified phone recipient is tapped and no share URL is configured', async () => {
|
|
323
303
|
jest
|
|
324
304
|
.mocked(getRecipientVerificationStatus)
|
|
325
305
|
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
|
|
326
306
|
|
|
327
307
|
const store = createMockStore(storeWithPhoneVerified)
|
|
328
308
|
|
|
329
|
-
const { getByTestId
|
|
309
|
+
const { getByTestId } = render(
|
|
330
310
|
<Provider store={store}>
|
|
331
311
|
<SendSelectRecipient {...mockScreenProps({})} />
|
|
332
312
|
</Provider>
|
|
@@ -347,11 +327,20 @@ describe('SendSelectRecipient', () => {
|
|
|
347
327
|
})
|
|
348
328
|
|
|
349
329
|
expect(store.getActions()).toEqual([fetchAddressesAndValidate(mockE164Number2Invite)])
|
|
350
|
-
expect(
|
|
351
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
330
|
+
expect(navigate).not.toHaveBeenCalled()
|
|
352
331
|
})
|
|
353
332
|
|
|
354
|
-
it('
|
|
333
|
+
it('navigates to the invite screen when an unverified phone number is tapped and share URL is configured', async () => {
|
|
334
|
+
const shareUrl = 'https://example.test/invite'
|
|
335
|
+
jest.mocked(getAppConfig).mockReturnValue({
|
|
336
|
+
displayName: 'Test App',
|
|
337
|
+
deepLinkUrlScheme: 'testapp',
|
|
338
|
+
registryName: 'test',
|
|
339
|
+
experimental: {
|
|
340
|
+
phoneNumberVerification: true,
|
|
341
|
+
inviteFriends: { shareUrl },
|
|
342
|
+
},
|
|
343
|
+
})
|
|
355
344
|
jest
|
|
356
345
|
.mocked(getRecipientVerificationStatus)
|
|
357
346
|
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
|
|
@@ -363,31 +352,33 @@ describe('SendSelectRecipient', () => {
|
|
|
363
352
|
<SendSelectRecipient {...mockScreenProps({})} />
|
|
364
353
|
</Provider>
|
|
365
354
|
)
|
|
366
|
-
await waitFor(() => {
|
|
367
|
-
expect(getByTestId('SendSelectRecipientSearchInput')).toBeTruthy()
|
|
368
|
-
})
|
|
369
|
-
const searchInput = getByTestId('SendSelectRecipientSearchInput')
|
|
370
355
|
|
|
356
|
+
const searchInput = getByTestId('SendSelectRecipientSearchInput')
|
|
371
357
|
await act(() => {
|
|
372
|
-
fireEvent.changeText(searchInput,
|
|
358
|
+
fireEvent.changeText(searchInput, mockE164Number2Invite)
|
|
373
359
|
})
|
|
374
|
-
|
|
375
|
-
// ensure its an address recipient (not an address that's tied to a contact)
|
|
376
|
-
expect(getByTestId('RecipientItem')).toHaveTextContent(
|
|
377
|
-
'feedItemAddress, {"address":"0x1ff4...bc42"}',
|
|
378
|
-
{ exact: false }
|
|
379
|
-
)
|
|
380
|
-
|
|
381
360
|
await act(() => {
|
|
382
361
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
383
362
|
})
|
|
384
363
|
|
|
385
|
-
expect(
|
|
386
|
-
|
|
387
|
-
|
|
364
|
+
expect(navigate).toHaveBeenCalledWith(Screens.SendInvite, {
|
|
365
|
+
recipient: expect.objectContaining({
|
|
366
|
+
e164PhoneNumber: mockE164Number2Invite,
|
|
367
|
+
recipientType: RecipientType.PhoneNumber,
|
|
368
|
+
}),
|
|
369
|
+
shareUrl,
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
// Search text is preserved so the user can return to the same picker state.
|
|
373
|
+
expect(searchInput.props.value).toBe(mockE164Number2Invite)
|
|
388
374
|
})
|
|
389
|
-
|
|
390
|
-
|
|
375
|
+
|
|
376
|
+
it('navigates and dispatches address verification when an unknown address is tapped and the user phone number is verified', async () => {
|
|
377
|
+
jest
|
|
378
|
+
.mocked(getRecipientVerificationStatus)
|
|
379
|
+
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
|
|
380
|
+
|
|
381
|
+
const store = createMockStore(storeWithPhoneVerified)
|
|
391
382
|
|
|
392
383
|
const { getByTestId } = render(
|
|
393
384
|
<Provider store={store}>
|
|
@@ -413,16 +404,14 @@ describe('SendSelectRecipient', () => {
|
|
|
413
404
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
414
405
|
})
|
|
415
406
|
|
|
416
|
-
expect(store.getActions()).toEqual([])
|
|
417
|
-
expect(
|
|
418
|
-
|
|
407
|
+
expect(store.getActions()).toEqual([fetchAddressVerification(mockAccount2.toLowerCase())])
|
|
408
|
+
expect(navigate).toHaveBeenCalledWith(
|
|
409
|
+
Screens.SendEnterAmount,
|
|
410
|
+
expect.objectContaining({ origin: SendOrigin.AppSendFlow })
|
|
411
|
+
)
|
|
419
412
|
})
|
|
420
|
-
it('
|
|
421
|
-
|
|
422
|
-
.mocked(getRecipientVerificationStatus)
|
|
423
|
-
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
|
|
424
|
-
|
|
425
|
-
const store = createMockStore(storeWithPhoneVerified)
|
|
413
|
+
it('skips verification request for an address when the user phone number is not verified', async () => {
|
|
414
|
+
const store = createMockStore(defaultStore)
|
|
426
415
|
|
|
427
416
|
const { getByTestId } = render(
|
|
428
417
|
<Provider store={store}>
|
|
@@ -435,22 +424,25 @@ describe('SendSelectRecipient', () => {
|
|
|
435
424
|
const searchInput = getByTestId('SendSelectRecipientSearchInput')
|
|
436
425
|
|
|
437
426
|
await act(() => {
|
|
438
|
-
fireEvent.changeText(searchInput,
|
|
427
|
+
fireEvent.changeText(searchInput, mockAccount2)
|
|
439
428
|
})
|
|
440
429
|
|
|
441
|
-
|
|
442
|
-
expect(getByTestId('RecipientItem')).toHaveTextContent(
|
|
443
|
-
|
|
444
|
-
|
|
430
|
+
// ensure its an address recipient (not an address that's tied to a contact)
|
|
431
|
+
expect(getByTestId('RecipientItem')).toHaveTextContent(
|
|
432
|
+
'feedItemAddress, {"address":"0x1ff4...bc42"}',
|
|
433
|
+
{ exact: false }
|
|
434
|
+
)
|
|
445
435
|
|
|
446
436
|
await act(() => {
|
|
447
437
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
448
438
|
})
|
|
449
439
|
|
|
450
|
-
expect(store.getActions()).toEqual([
|
|
451
|
-
|
|
452
|
-
expect(
|
|
453
|
-
|
|
440
|
+
expect(store.getActions()).toEqual([])
|
|
441
|
+
// Navigation still proceeds — status is treated as UNVERIFIED locally.
|
|
442
|
+
expect(navigate).toHaveBeenCalledWith(
|
|
443
|
+
Screens.SendEnterAmount,
|
|
444
|
+
expect.objectContaining({ origin: SendOrigin.AppSendFlow })
|
|
445
|
+
)
|
|
454
446
|
})
|
|
455
447
|
it('shows paste button if clipboard has address content', async () => {
|
|
456
448
|
const store = createMockStore(defaultStore)
|
|
@@ -476,7 +468,7 @@ describe('SendSelectRecipient', () => {
|
|
|
476
468
|
await expect(pasteButtonAfterPress).rejects.toThrow()
|
|
477
469
|
})
|
|
478
470
|
|
|
479
|
-
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 () => {
|
|
480
472
|
jest
|
|
481
473
|
.mocked(getRecipientVerificationStatus)
|
|
482
474
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -505,11 +497,6 @@ describe('SendSelectRecipient', () => {
|
|
|
505
497
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
506
498
|
})
|
|
507
499
|
|
|
508
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
509
|
-
|
|
510
|
-
await act(() => {
|
|
511
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
512
|
-
})
|
|
513
500
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
514
501
|
recipientType: RecipientType.PhoneNumber,
|
|
515
502
|
})
|
|
@@ -527,7 +514,7 @@ describe('SendSelectRecipient', () => {
|
|
|
527
514
|
isMiniPayRecipient: false,
|
|
528
515
|
})
|
|
529
516
|
})
|
|
530
|
-
it('navigates
|
|
517
|
+
it('navigates with isMiniPayRecipient when address is verified by minipay', async () => {
|
|
531
518
|
jest
|
|
532
519
|
.mocked(getRecipientVerificationStatus)
|
|
533
520
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -556,9 +543,6 @@ describe('SendSelectRecipient', () => {
|
|
|
556
543
|
await act(() => {
|
|
557
544
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
558
545
|
})
|
|
559
|
-
await act(() => {
|
|
560
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
561
|
-
})
|
|
562
546
|
|
|
563
547
|
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
|
|
564
548
|
isFromScan: false,
|
|
@@ -574,7 +558,7 @@ describe('SendSelectRecipient', () => {
|
|
|
574
558
|
isMiniPayRecipient: true,
|
|
575
559
|
})
|
|
576
560
|
})
|
|
577
|
-
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 () => {
|
|
578
562
|
jest
|
|
579
563
|
.mocked(getRecipientVerificationStatus)
|
|
580
564
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -609,11 +593,6 @@ describe('SendSelectRecipient', () => {
|
|
|
609
593
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
610
594
|
})
|
|
611
595
|
|
|
612
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
613
|
-
|
|
614
|
-
await act(() => {
|
|
615
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
616
|
-
})
|
|
617
596
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
618
597
|
recipientType: RecipientType.PhoneNumber,
|
|
619
598
|
})
|
|
@@ -624,7 +603,7 @@ describe('SendSelectRecipient', () => {
|
|
|
624
603
|
origin: SendOrigin.AppSendFlow,
|
|
625
604
|
})
|
|
626
605
|
})
|
|
627
|
-
it('navigates to send
|
|
606
|
+
it('navigates to send amount when phone recipient has multiple addresses and secure send was already done', async () => {
|
|
628
607
|
jest
|
|
629
608
|
.mocked(getRecipientVerificationStatus)
|
|
630
609
|
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
@@ -662,11 +641,6 @@ describe('SendSelectRecipient', () => {
|
|
|
662
641
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
663
642
|
})
|
|
664
643
|
|
|
665
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
666
|
-
|
|
667
|
-
await act(() => {
|
|
668
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
669
|
-
})
|
|
670
644
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
671
645
|
recipientType: RecipientType.PhoneNumber,
|
|
672
646
|
})
|
|
@@ -723,11 +697,6 @@ describe('SendSelectRecipient', () => {
|
|
|
723
697
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
724
698
|
})
|
|
725
699
|
|
|
726
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
727
|
-
|
|
728
|
-
await act(() => {
|
|
729
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
730
|
-
})
|
|
731
700
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
732
701
|
recipientType: RecipientType.Address,
|
|
733
702
|
})
|
|
@@ -789,11 +758,6 @@ describe('SendSelectRecipient', () => {
|
|
|
789
758
|
fireEvent.press(getByTestId('RecipientItem'))
|
|
790
759
|
})
|
|
791
760
|
|
|
792
|
-
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
|
|
793
|
-
|
|
794
|
-
await act(() => {
|
|
795
|
-
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
796
|
-
})
|
|
797
761
|
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
|
|
798
762
|
recipientType: RecipientType.Address,
|
|
799
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, { ShareSingleOptions, Social } 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,38 +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 with SMS')
|
|
330
|
-
return
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const shareOptions: ShareSingleOptions = {
|
|
334
|
-
social: Social.Sms,
|
|
335
|
-
recipient: recipient.e164PhoneNumber,
|
|
336
|
-
message: t('inviteWithSmsMessage.shareMessage', { shareUrl }),
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
await Share.shareSingle(shareOptions)
|
|
340
|
-
|
|
341
|
-
AppAnalytics.track(SendEvents.send_select_recipient_invite_press, {
|
|
342
|
-
recipientType: recipient.recipientType,
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
return
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Sends
|
|
349
|
-
AppAnalytics.track(SendEvents.send_select_recipient_send_press, {
|
|
350
|
-
recipientType: recipient.recipientType,
|
|
351
|
-
})
|
|
352
|
-
nextScreen(recipient)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
288
|
const renderSearchResults = () => {
|
|
356
289
|
if (mergedRecipients.length) {
|
|
357
290
|
return (
|
|
@@ -360,7 +293,7 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
360
293
|
<RecipientPicker
|
|
361
294
|
testID={'SelectRecipient/AllRecipientsPicker'}
|
|
362
295
|
recipients={mergedRecipients}
|
|
363
|
-
onSelectRecipient={
|
|
296
|
+
onSelectRecipient={setSelectedRecipient}
|
|
364
297
|
selectedRecipient={recipient}
|
|
365
298
|
isSelectedRecipientLoading={
|
|
366
299
|
!!recipient && recipientVerificationStatus === RecipientVerificationStatus.UNKNOWN
|
|
@@ -407,7 +340,7 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
407
340
|
<RecipientPicker
|
|
408
341
|
testID={'SelectRecipient/ContactRecipientPicker'}
|
|
409
342
|
recipients={contactRecipients}
|
|
410
|
-
onSelectRecipient={
|
|
343
|
+
onSelectRecipient={setSelectedRecipient}
|
|
411
344
|
selectedRecipient={recipient}
|
|
412
345
|
isSelectedRecipientLoading={
|
|
413
346
|
!!recipient && recipientVerificationStatus === RecipientVerificationStatus.UNKNOWN
|
|
@@ -437,22 +370,6 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
437
370
|
</>
|
|
438
371
|
)}
|
|
439
372
|
</KeyboardAwareScrollView>
|
|
440
|
-
{showUnknownAddressInfo && (
|
|
441
|
-
<InLineNotification
|
|
442
|
-
variant={NotificationVariant.Info}
|
|
443
|
-
description={t('sendSelectRecipient.unknownAddressInfo')}
|
|
444
|
-
testID="UnknownAddressInfo"
|
|
445
|
-
style={styles.unknownAddressInfo}
|
|
446
|
-
/>
|
|
447
|
-
)}
|
|
448
|
-
{showSendOrInviteButton && (
|
|
449
|
-
<SendOrInviteButton
|
|
450
|
-
recipient={recipient}
|
|
451
|
-
recipientVerificationStatus={recipientVerificationStatus}
|
|
452
|
-
onPress={onPressSendOrInvite}
|
|
453
|
-
shareUrl={shareUrl}
|
|
454
|
-
/>
|
|
455
|
-
)}
|
|
456
373
|
</SafeAreaView>
|
|
457
374
|
)
|
|
458
375
|
}
|
|
@@ -494,14 +411,6 @@ const styles = StyleSheet.create({
|
|
|
494
411
|
padding: Spacing.Regular16,
|
|
495
412
|
textAlign: 'center',
|
|
496
413
|
},
|
|
497
|
-
unknownAddressInfo: {
|
|
498
|
-
margin: Spacing.Regular16,
|
|
499
|
-
marginBottom: variables.contentPadding,
|
|
500
|
-
},
|
|
501
|
-
sendOrInviteButton: {
|
|
502
|
-
margin: Spacing.Regular16,
|
|
503
|
-
marginTop: variables.contentPadding,
|
|
504
|
-
},
|
|
505
414
|
})
|
|
506
415
|
|
|
507
416
|
export default SendSelectRecipient
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next'
|
|
1
2
|
import { BooleanFilterChip } from 'src/components/FilterChipsCarousel'
|
|
2
3
|
import { getDynamicConfigParams } from 'src/statsig'
|
|
3
4
|
import { DynamicConfigs } from 'src/statsig/constants'
|
|
@@ -18,6 +19,8 @@ export default function useSendFilterChips({
|
|
|
18
19
|
filterChips: BooleanFilterChip<TokenBalance>[]
|
|
19
20
|
defaultToken: TokenBalance | undefined
|
|
20
21
|
} {
|
|
22
|
+
const { t } = useTranslation()
|
|
23
|
+
|
|
21
24
|
const { miniPayTokenIds: configTokenIds } = getDynamicConfigParams(
|
|
22
25
|
DynamicConfigs[StatsigDynamicConfigs.SEND_CONFIG]
|
|
23
26
|
)
|
|
@@ -27,7 +30,7 @@ export default function useSendFilterChips({
|
|
|
27
30
|
? [
|
|
28
31
|
{
|
|
29
32
|
id: 'minipay',
|
|
30
|
-
name: '
|
|
33
|
+
name: t('sendEnterAmountScreen.miniPayFilterChip'),
|
|
31
34
|
filterFn: (token: TokenBalance) => miniPayTokenIds.includes(token.tokenId),
|
|
32
35
|
isSelected: true,
|
|
33
36
|
},
|