wallet-stack 1.0.0-alpha.136 → 1.0.0-alpha.138

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.
@@ -0,0 +1,197 @@
1
+ import React, { memo, useRef } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
4
+ import { formatShortenedAddress } from 'src/account/utils'
5
+ import BottomSheet, { BottomSheetModalRefType } from 'src/components/BottomSheet'
6
+ import ContactCircle from 'src/components/ContactCircle'
7
+ import Touchable from 'src/components/Touchable'
8
+ import AttentionIcon from 'src/icons/Attention'
9
+ import DownArrowIcon from 'src/icons/DownArrowIcon'
10
+ import PhoneIcon from 'src/icons/Phone'
11
+ import UserIcon from 'src/icons/User'
12
+ import VerifiedBadge from 'src/icons/VerifiedBadge'
13
+ import WalletIcon from 'src/icons/navigator/Wallet'
14
+ import { Recipient } from 'src/recipients/recipient'
15
+ import { useVerifierName } from 'src/recipients/verifier'
16
+ import SelectRecipientAddressList, { type Entry } from 'src/send/SelectRecipientAddressList'
17
+ import { type RecipientLookupStatus, type VerifiedAddressEntry } from 'src/send/useRecipientLookup'
18
+ import Colors from 'src/styles/colors'
19
+ import { typeScale } from 'src/styles/fonts'
20
+ import { Spacing } from 'src/styles/styles'
21
+
22
+ const SECONDARY_ROW_MIN_HEIGHT = 20
23
+
24
+ interface Props {
25
+ recipient: Recipient & { address: string }
26
+ status: RecipientLookupStatus
27
+ verifiedAddresses: VerifiedAddressEntry[]
28
+ // The address the recipient was navigated in with. If it's not in the verified set
29
+ // (e.g. came from recents and the mapping no longer holds), it stays selectable in the
30
+ // sheet as an unverified option so the user can switch back after picking a verified one.
31
+ originalAddress: string
32
+ onSelectAddress(address: string): void
33
+ }
34
+
35
+ function SelectedRecipientCard({
36
+ recipient,
37
+ status,
38
+ verifiedAddresses,
39
+ originalAddress,
40
+ onSelectAddress,
41
+ }: Props) {
42
+ const { t } = useTranslation()
43
+ const sheetRef = useRef<BottomSheetModalRefType>(null)
44
+
45
+ const verifierName = useVerifierName(recipient.address)
46
+ const isLoading = status === 'loading'
47
+
48
+ const originalIsVerified = verifiedAddresses.some(
49
+ (entry) => entry.address.toLowerCase() === originalAddress.toLowerCase()
50
+ )
51
+ const sheetEntries: Entry[] = originalIsVerified
52
+ ? verifiedAddresses
53
+ : [...verifiedAddresses, { address: originalAddress, verifier: null }]
54
+
55
+ const hasAlternative = sheetEntries.some(
56
+ (entry) => entry.address.toLowerCase() !== recipient.address.toLowerCase()
57
+ )
58
+ const isTappable = !isLoading && hasAlternative
59
+
60
+ const onPress = isTappable ? () => sheetRef.current?.snapToIndex(0) : undefined
61
+
62
+ const onSelectFromSheet = (address: string) => {
63
+ sheetRef.current?.close()
64
+ onSelectAddress(address)
65
+ }
66
+
67
+ const shortAddress = formatShortenedAddress(recipient.address)
68
+ const phone = recipient.displayNumber || recipient.e164PhoneNumber
69
+ const contact = recipient.name
70
+ ? { title: recipient.name, icon: UserIcon, shortAddress }
71
+ : phone
72
+ ? { title: phone, icon: PhoneIcon, shortAddress }
73
+ : { title: shortAddress, icon: WalletIcon, shortAddress: undefined as string | undefined }
74
+
75
+ const renderSecondary = () => {
76
+ const { shortAddress } = contact
77
+
78
+ // Address-only recipients (no name/phone) use the address as the title and have no secondary
79
+ // line — by design we don't show an extra "unverified" warning row in that case.
80
+ if (status === 'unverified' && shortAddress) {
81
+ return (
82
+ <View style={styles.secondaryRow} testID="SelectedRecipientCard/Unverified">
83
+ <Text style={[styles.secondaryText, styles.warningText]}>{shortAddress}</Text>
84
+ <AttentionIcon size={14} color={Colors.warningPrimary} />
85
+ <Text style={[styles.secondaryText, styles.warningText]}>{t('unverifiedAddress')}</Text>
86
+ </View>
87
+ )
88
+ }
89
+
90
+ if (!shortAddress && !verifierName) return null
91
+
92
+ return (
93
+ <View style={styles.secondaryRow}>
94
+ {!!shortAddress && <Text style={styles.secondaryText}>{shortAddress}</Text>}
95
+ {!!verifierName && (
96
+ <>
97
+ <VerifiedBadge color={Colors.contentSecondary} />
98
+ <Text style={styles.secondaryText}>{verifierName}</Text>
99
+ </>
100
+ )}
101
+ </View>
102
+ )
103
+ }
104
+
105
+ const secondary = renderSecondary()
106
+
107
+ return (
108
+ <View style={styles.summary} testID="SelectedRecipientCard">
109
+ <Touchable
110
+ style={styles.content}
111
+ onPress={onPress}
112
+ disabled={!onPress}
113
+ testID="SelectedRecipientCard/Touchable"
114
+ >
115
+ <>
116
+ <ContactCircle
117
+ size={32}
118
+ backgroundColor={Colors.backgroundTertiary}
119
+ foregroundColor={Colors.contentPrimary}
120
+ recipient={recipient}
121
+ DefaultIcon={contact.icon}
122
+ />
123
+ <View style={styles.values}>
124
+ <Text style={styles.primary} numberOfLines={1} ellipsizeMode="middle">
125
+ {contact.title}
126
+ </Text>
127
+ {secondary ? <View style={styles.secondaryWrapper}>{secondary}</View> : null}
128
+ </View>
129
+ {isLoading ? (
130
+ <ActivityIndicator
131
+ size="small"
132
+ color={Colors.loadingIndicator}
133
+ testID="SelectedRecipientCard/Spinner"
134
+ />
135
+ ) : isTappable ? (
136
+ <DownArrowIcon height={24} color={Colors.contentSecondary} />
137
+ ) : null}
138
+ </>
139
+ </Touchable>
140
+ {hasAlternative && (
141
+ <BottomSheet
142
+ forwardedRef={sheetRef}
143
+ title={t('selectRecipientAddress.header')}
144
+ snapPoints={['90%']}
145
+ testId="SelectRecipientAddressSheet"
146
+ >
147
+ <SelectRecipientAddressList
148
+ entries={sheetEntries}
149
+ onSelectAddress={onSelectFromSheet}
150
+ compact
151
+ />
152
+ </BottomSheet>
153
+ )}
154
+ </View>
155
+ )
156
+ }
157
+
158
+ const styles = StyleSheet.create({
159
+ summary: {
160
+ borderWidth: 1,
161
+ borderColor: Colors.borderPrimary,
162
+ borderRadius: Spacing.Small12,
163
+ backgroundColor: Colors.backgroundSecondary,
164
+ padding: Spacing.Regular16,
165
+ },
166
+ content: {
167
+ flexDirection: 'row',
168
+ gap: Spacing.Smallest8,
169
+ alignItems: 'center',
170
+ },
171
+ values: {
172
+ flex: 1,
173
+ flexShrink: 1,
174
+ },
175
+ primary: {
176
+ ...typeScale.labelMedium,
177
+ },
178
+ secondaryWrapper: {
179
+ minHeight: SECONDARY_ROW_MIN_HEIGHT,
180
+ justifyContent: 'center',
181
+ },
182
+ secondaryRow: {
183
+ flexDirection: 'row',
184
+ alignItems: 'center',
185
+ gap: Spacing.Tiny4,
186
+ flexShrink: 1,
187
+ },
188
+ secondaryText: {
189
+ ...typeScale.bodySmall,
190
+ color: Colors.contentSecondary,
191
+ },
192
+ warningText: {
193
+ color: Colors.warningPrimary,
194
+ },
195
+ })
196
+
197
+ export default memo(SelectedRecipientCard)
@@ -23,6 +23,7 @@ import {
23
23
  mockCusdTokenBalance,
24
24
  mockCusdTokenId,
25
25
  mockPoofTokenId,
26
+ mockRecipient,
26
27
  mockTokenBalances,
27
28
  mockTokenTransactionData,
28
29
  } from 'test/values'
@@ -174,7 +175,27 @@ describe('SendConfirmation', () => {
174
175
  it('shows the unknown address warning when the recipient address is not a known app user', () => {
175
176
  const { getByTestId } = renderScreen(mockSendConfirmationProps, {
176
177
  identity: {
177
- addressToVerifiedBy: { [mockAccount]: null },
178
+ addressToVerifiedBy: { [mockAccount.toLowerCase()]: null },
179
+ },
180
+ })
181
+
182
+ expect(getByTestId('UnknownAddressInfo')).toBeTruthy()
183
+ })
184
+
185
+ it('shows the unknown address warning for phone recipients whose resolved address is known-unverified', () => {
186
+ const phoneRecipientProps = getMockStackScreenProps(Screens.SendConfirmation, {
187
+ ...mockBaseScreenProps,
188
+ transactionData: {
189
+ ...mockTokenTransactionData,
190
+ recipient: { ...mockRecipient, address: mockAccount },
191
+ },
192
+ prepareTransactionsResult: getSerializablePreparedTransactionsPossible(
193
+ mockPrepareTransactionsResultPossible
194
+ ),
195
+ })
196
+ const { getByTestId } = renderScreen(phoneRecipientProps, {
197
+ identity: {
198
+ addressToVerifiedBy: { [mockAccount.toLowerCase()]: null },
178
199
  },
179
200
  })
180
201
 
@@ -184,7 +205,7 @@ describe('SendConfirmation', () => {
184
205
  it('does not show the unknown address warning when the recipient address is verified', () => {
185
206
  const { queryByTestId } = renderScreen(mockSendConfirmationProps, {
186
207
  identity: {
187
- addressToVerifiedBy: { [mockAccount]: 'valora' },
208
+ addressToVerifiedBy: { [mockAccount.toLowerCase()]: 'valora' },
188
209
  },
189
210
  })
190
211
 
@@ -30,7 +30,6 @@ import { getLocalCurrencyCode, getLocalCurrencySymbol } from 'src/localCurrency/
30
30
  import { noHeader } from 'src/navigator/Headers'
31
31
  import { Screens } from 'src/navigator/Screens'
32
32
  import { StackParamList } from 'src/navigator/types'
33
- import { RecipientType } from 'src/recipients/recipient'
34
33
  import { useDispatch, useSelector } from 'src/redux/hooks'
35
34
  import { sendPayment } from 'src/send/actions'
36
35
  import { isSendingSelector } from 'src/send/selectors'
@@ -93,9 +92,7 @@ export default function SendConfirmation({ route: { params } }: Props) {
93
92
  const walletAddress = useSelector(walletAddressSelector)
94
93
  const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
95
94
  const showUnknownAddressInfo =
96
- recipient.recipientType === RecipientType.Address &&
97
- !!recipient.address &&
98
- addressToVerifiedBy[recipient.address] === null
95
+ !!recipient.address && addressToVerifiedBy[recipient.address.toLowerCase()] === null
99
96
 
100
97
  const feeCurrencies = useSelector((state) => feeCurrenciesSelector(state, tokenInfo!.networkId))
101
98
  const {
@@ -241,6 +241,14 @@ describe('SendEnterAmount', () => {
241
241
 
242
242
  describe('MiniPay filter', () => {
243
243
  const miniPayTokenIds = [mockCusdTokenId, mockUSDCTokenId]
244
+ // The screen now derives `isMiniPayRecipient` from the store rather than a route param,
245
+ // so set up the verifier mapping for the recipient address used in `params`.
246
+ const miniPayStore = createMockStore({
247
+ tokens: { tokenBalances },
248
+ identity: {
249
+ addressToVerifiedBy: { [params.recipient.address.toLowerCase()]: 'minipay' },
250
+ },
251
+ })
244
252
 
245
253
  beforeEach(() => {
246
254
  jest.mocked(getDynamicConfigParams).mockReturnValue({
@@ -248,13 +256,10 @@ describe('SendEnterAmount', () => {
248
256
  })
249
257
  })
250
258
 
251
- it('should show MiniPay chip pre-selected and only MiniPay tokens when isMiniPayRecipient is true', () => {
259
+ it('should show MiniPay chip pre-selected and only MiniPay tokens when the recipient address is verified by minipay', () => {
252
260
  const { getAllByTestId, getByText } = render(
253
- <Provider store={store}>
254
- <MockedNavigator
255
- component={SendEnterAmount}
256
- params={{ ...params, isMiniPayRecipient: true }}
257
- />
261
+ <Provider store={miniPayStore}>
262
+ <MockedNavigator component={SendEnterAmount} params={params} />
258
263
  </Provider>
259
264
  )
260
265
 
@@ -269,11 +274,8 @@ describe('SendEnterAmount', () => {
269
274
 
270
275
  it('should select default token from MiniPay list', () => {
271
276
  const { getByTestId } = render(
272
- <Provider store={store}>
273
- <MockedNavigator
274
- component={SendEnterAmount}
275
- params={{ ...params, isMiniPayRecipient: true }}
276
- />
277
+ <Provider store={miniPayStore}>
278
+ <MockedNavigator component={SendEnterAmount} params={params} />
277
279
  </Provider>
278
280
  )
279
281
 
@@ -283,11 +285,8 @@ describe('SendEnterAmount', () => {
283
285
 
284
286
  it('should show all tokens when MiniPay chip is toggled off', () => {
285
287
  const { getAllByTestId, getByText } = render(
286
- <Provider store={store}>
287
- <MockedNavigator
288
- component={SendEnterAmount}
289
- params={{ ...params, isMiniPayRecipient: true }}
290
- />
288
+ <Provider store={miniPayStore}>
289
+ <MockedNavigator component={SendEnterAmount} params={params} />
291
290
  </Provider>
292
291
  )
293
292
 
@@ -298,7 +297,7 @@ describe('SendEnterAmount', () => {
298
297
  expect(tokens).toHaveLength(3)
299
298
  })
300
299
 
301
- it('should not show MiniPay chip when isMiniPayRecipient is not set', () => {
300
+ it('should not show MiniPay chip when the recipient address is not verified by minipay', () => {
302
301
  const { queryByText } = render(
303
302
  <Provider store={store}>
304
303
  <MockedNavigator component={SendEnterAmount} params={params} />
@@ -308,17 +307,14 @@ describe('SendEnterAmount', () => {
308
307
  expect(queryByText('sendEnterAmountScreen.miniPayFilterChip')).toBeFalsy()
309
308
  })
310
309
 
311
- it('should not select a default token when isMiniPayRecipient is true and user has no MiniPay tokens', () => {
310
+ it('should not select a default token when the recipient is minipay-verified and user has no MiniPay tokens', () => {
312
311
  jest.mocked(getDynamicConfigParams).mockReturnValue({
313
312
  miniPayTokenIds: ['celo-alfajores:0xNOT_HELD_BY_USER'],
314
313
  })
315
314
 
316
315
  const { getByText, queryByTestId, getByTestId } = render(
317
- <Provider store={store}>
318
- <MockedNavigator
319
- component={SendEnterAmount}
320
- params={{ ...params, isMiniPayRecipient: true }}
321
- />
316
+ <Provider store={miniPayStore}>
317
+ <MockedNavigator component={SendEnterAmount} params={params} />
322
318
  </Provider>
323
319
  )
324
320
 
@@ -335,7 +331,7 @@ describe('SendEnterAmount', () => {
335
331
  expect(getByTestId('TokenBottomSheet')).toBeTruthy()
336
332
  })
337
333
 
338
- it('should include isMiniPayRecipient in send_amount_continue analytics', async () => {
334
+ it('should include isMiniPayRecipient=true in send_amount_continue analytics when the address is minipay-verified', async () => {
339
335
  jest.mocked(usePrepareSendTransactions).mockReturnValue({
340
336
  prepareTransactionsResult: mockPrepareTransactionsResultPossible,
341
337
  prepareTransactionLoading: false,
@@ -344,11 +340,8 @@ describe('SendEnterAmount', () => {
344
340
  prepareTransactionError: undefined,
345
341
  })
346
342
  const { getByTestId, getByText } = render(
347
- <Provider store={store}>
348
- <MockedNavigator
349
- component={SendEnterAmount}
350
- params={{ ...params, isMiniPayRecipient: true }}
351
- />
343
+ <Provider store={miniPayStore}>
344
+ <MockedNavigator component={SendEnterAmount} params={params} />
352
345
  </Provider>
353
346
  )
354
347
 
@@ -1,16 +1,19 @@
1
1
  import { NativeStackScreenProps } from '@react-navigation/native-stack'
2
2
  import BigNumber from 'bignumber.js'
3
- import React from 'react'
3
+ import React, { useCallback, useMemo, useState } from 'react'
4
4
  import AppAnalytics from 'src/analytics/AppAnalytics'
5
5
  import { SendEvents } from 'src/analytics/Events'
6
+ import { addressToVerifiedBySelector } from 'src/identity/selectors'
6
7
  import { getLocalCurrencyCode, usdToLocalCurrencyRateSelector } from 'src/localCurrency/selectors'
7
8
  import { navigate } from 'src/navigator/NavigationService'
8
9
  import { Screens } from 'src/navigator/Screens'
9
10
  import { StackParamList } from 'src/navigator/types'
10
11
  import { useSelector } from 'src/redux/hooks'
11
12
  import EnterAmount, { ProceedArgs, SendProceed } from 'src/send/EnterAmount'
13
+ import SelectedRecipientCard from 'src/send/SelectedRecipientCard'
12
14
  import { lastUsedTokenIdSelector } from 'src/send/selectors'
13
15
  import { usePrepareSendTransactions } from 'src/send/usePrepareSendTransactions'
16
+ import { useRecipientLookup } from 'src/send/useRecipientLookup'
14
17
  import useSendFilterChips from 'src/send/useSendFilterChips'
15
18
  import { sortedTokensWithBalanceOrShowZeroBalanceSelector } from 'src/tokens/selectors'
16
19
  import { TokenBalance } from 'src/tokens/slice'
@@ -26,11 +29,29 @@ function SendEnterAmount({ route }: Props) {
26
29
  const {
27
30
  defaultTokenIdOverride,
28
31
  origin,
29
- recipient,
32
+ recipient: initialRecipient,
30
33
  isFromScan,
31
34
  forceTokenId,
32
- isMiniPayRecipient,
35
+ skipRecipientLookup,
33
36
  } = route.params
37
+
38
+ // Local state lets the user swap addresses (via SelectedRecipientCard) without re-navigating,
39
+ // so the typed amount is preserved.
40
+ const [selectedAddress, setSelectedAddress] = useState(initialRecipient.address)
41
+ const recipient = useMemo(
42
+ () => ({ ...initialRecipient, address: selectedAddress }),
43
+ [initialRecipient, selectedAddress]
44
+ )
45
+
46
+ const { status: lookupStatus, verifiedAddresses } = useRecipientLookup(recipient, {
47
+ skipFetch: skipRecipientLookup,
48
+ })
49
+
50
+ // Derive from the store so a fresh lookup that updates the verifier is reflected
51
+ // immediately (token filtering and analytics stay in sync with the selected address).
52
+ const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
53
+ const isMiniPayRecipient = addressToVerifiedBy[selectedAddress.toLowerCase()] === 'minipay'
54
+
34
55
  // explicitly allow zero state tokens to be shown for exploration purposes for
35
56
  // new users with no balance
36
57
  const tokens = useSelector(sortedTokensWithBalanceOrShowZeroBalanceSelector)
@@ -79,7 +100,7 @@ function SendEnterAmount({ route }: Props) {
79
100
  amountEnteredIn,
80
101
  tokenId: token.tokenId,
81
102
  networkId: token.networkId,
82
- isMiniPayRecipient: isMiniPayRecipient ?? false,
103
+ isMiniPayRecipient,
83
104
  })
84
105
  }
85
106
 
@@ -112,6 +133,10 @@ function SendEnterAmount({ route }: Props) {
112
133
  })
113
134
  }
114
135
 
136
+ const handleSelectAddress = useCallback((address: string) => {
137
+ setSelectedAddress(address)
138
+ }, [])
139
+
115
140
  return (
116
141
  <EnterAmount
117
142
  tokens={tokens}
@@ -123,8 +148,18 @@ function SendEnterAmount({ route }: Props) {
123
148
  prepareTransactionError={prepareTransactionError}
124
149
  tokenSelectionDisabled={!!forceTokenId}
125
150
  onPressProceed={handleReviewSend}
151
+ disableProceed={lookupStatus === 'loading'}
126
152
  ProceedComponent={SendProceed}
127
153
  filterChips={filterChips}
154
+ recipientSlot={
155
+ <SelectedRecipientCard
156
+ recipient={recipient}
157
+ status={lookupStatus}
158
+ verifiedAddresses={verifiedAddresses}
159
+ originalAddress={initialRecipient.address}
160
+ onSelectAddress={handleSelectAddress}
161
+ />
162
+ }
128
163
  />
129
164
  )
130
165
  }
@@ -1,5 +1,5 @@
1
1
  import Clipboard from '@react-native-clipboard/clipboard'
2
- import { act, fireEvent, render, waitFor } from '@testing-library/react-native'
2
+ import { act, fireEvent, render, waitFor, within } from '@testing-library/react-native'
3
3
  import * as React from 'react'
4
4
  import { Provider } from 'react-redux'
5
5
  import AppAnalytics from 'src/analytics/AppAnalytics'
@@ -14,10 +14,10 @@ import {
14
14
  import { navigate } from 'src/navigator/NavigationService'
15
15
  import { Screens } from 'src/navigator/Screens'
16
16
  import { RecipientType } from 'src/recipients/recipient'
17
+ import { setupStore } from 'src/redux/store'
17
18
  import SendSelectRecipient from 'src/send/SendSelectRecipient'
18
19
  import { getDynamicConfigParams } from 'src/statsig'
19
20
  import { StatsigDynamicConfigs } from 'src/statsig/types'
20
- import { setupStore } from 'src/redux/store'
21
21
  import { createMockStore, getMockStackScreenProps, getMockStoreData } from 'test/utils'
22
22
  import {
23
23
  mockAccount,
@@ -237,6 +237,38 @@ describe('SendSelectRecipient', () => {
237
237
  })
238
238
  })
239
239
 
240
+ it('passes skipRecipientLookup=false when a recent recipient is tapped (cached mappings may be stale)', async () => {
241
+ const store = createMockStore({
242
+ ...defaultStore,
243
+ send: {
244
+ recentRecipients: [{ ...mockRecipient, address: mockAccount2.toLowerCase() }],
245
+ },
246
+ identity: {
247
+ addressToVerifiedBy: { [mockAccount2.toLowerCase()]: 'valora' },
248
+ },
249
+ })
250
+
251
+ const { getByTestId } = render(
252
+ <Provider store={store}>
253
+ <SendSelectRecipient {...mockScreenProps({})} />
254
+ </Provider>
255
+ )
256
+
257
+ await act(() => {
258
+ fireEvent.press(
259
+ within(getByTestId('SelectRecipient/RecentRecipientPicker')).getByTestId('RecipientItem')
260
+ )
261
+ })
262
+
263
+ expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_recent_press, {
264
+ recipientType: mockRecipient.recipientType,
265
+ })
266
+ expect(navigate).toHaveBeenCalledWith(
267
+ Screens.SendEnterAmount,
268
+ expect.objectContaining({ skipRecipientLookup: false })
269
+ )
270
+ })
271
+
240
272
  it('navigates to send amount when a verified phone recipient is tapped in search results', async () => {
241
273
  const store = createMockStore({
242
274
  ...storeWithPhoneVerified,
@@ -270,7 +302,7 @@ describe('SendSelectRecipient', () => {
270
302
  forceTokenId: undefined,
271
303
  recipient: expect.any(Object),
272
304
  origin: SendOrigin.AppSendFlow,
273
- isMiniPayRecipient: false,
305
+ skipRecipientLookup: true,
274
306
  })
275
307
  })
276
308
  it('navigates to send amount when an address is tapped and the user phone number is not verified', async () => {
@@ -304,7 +336,7 @@ describe('SendSelectRecipient', () => {
304
336
  forceTokenId: undefined,
305
337
  recipient: expect.any(Object),
306
338
  origin: SendOrigin.AppSendFlow,
307
- isMiniPayRecipient: false,
339
+ skipRecipientLookup: true,
308
340
  })
309
341
  })
310
342
 
@@ -549,44 +581,7 @@ describe('SendSelectRecipient', () => {
549
581
  recipientType: 'PhoneNumber',
550
582
  },
551
583
  origin: SendOrigin.AppSendFlow,
552
- isMiniPayRecipient: false,
553
- })
554
- })
555
- it('navigates with isMiniPayRecipient when address is verified by minipay', async () => {
556
- const store = createMockStore({
557
- ...storeWithPhoneVerified,
558
- identity: {
559
- e164NumberToAddress: { [mockE164Number3]: [mockAccount3] },
560
- addressToVerifiedBy: { [mockAccount3]: 'minipay' },
561
- },
562
- })
563
-
564
- const { getByTestId } = render(
565
- <Provider store={store}>
566
- <SendSelectRecipient {...mockScreenProps({})} />
567
- </Provider>
568
- )
569
- const searchInput = getByTestId('SendSelectRecipientSearchInput')
570
-
571
- await act(() => {
572
- fireEvent.changeText(searchInput, mockE164Number3)
573
- })
574
- await act(() => {
575
- fireEvent.press(getByTestId('RecipientItem'))
576
- })
577
-
578
- expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
579
- isFromScan: false,
580
- defaultTokenIdOverride: undefined,
581
- forceTokenId: undefined,
582
- recipient: {
583
- address: mockAccount3,
584
- displayNumber: '(415) 555-0123',
585
- e164PhoneNumber: mockE164Number3,
586
- recipientType: 'PhoneNumber',
587
- },
588
- origin: SendOrigin.AppSendFlow,
589
- isMiniPayRecipient: true,
584
+ skipRecipientLookup: true,
590
585
  })
591
586
  })
592
587
  it('navigates to address picker when phone number recipient has multiple verified addresses', async () => {
@@ -676,7 +671,7 @@ describe('SendSelectRecipient', () => {
676
671
  recipientType: 'PhoneNumber',
677
672
  },
678
673
  origin: SendOrigin.AppSendFlow,
679
- isMiniPayRecipient: false,
674
+ skipRecipientLookup: true,
680
675
  })
681
676
  })
682
677
  it.each([{ searchAddress: mockAccount2 }, { searchAddress: mockAccount3 }])(
@@ -730,7 +725,7 @@ describe('SendSelectRecipient', () => {
730
725
  thumbnailPath: undefined,
731
726
  },
732
727
  origin: SendOrigin.AppSendFlow,
733
- isMiniPayRecipient: searchAddress.toLowerCase() === mockAccount3.toLowerCase(),
728
+ skipRecipientLookup: true,
734
729
  })
735
730
  }
736
731
  )
@@ -215,7 +215,8 @@ function SendSelectRecipient({ route }: Props) {
215
215
  AppAnalytics.track(SendEvents.send_select_recipient_send_press, {
216
216
  recipientType: recipient.recipientType,
217
217
  })
218
- nextScreen(recipient)
218
+ // useFetchRecipientVerificationStatus already performed the lookup for search results.
219
+ nextScreen(recipient, true)
219
220
  }, [recipient, recipientVerificationStatus, shareUrl])
220
221
 
221
222
  const onContactsPermissionGranted = () => {
@@ -231,10 +232,11 @@ function SendSelectRecipient({ route }: Props) {
231
232
  AppAnalytics.track(SendEvents.send_select_recipient_recent_press, {
232
233
  recipientType: recentRecipient.recipientType,
233
234
  })
234
- nextScreen(recentRecipient)
235
+ // Recents may carry stale mappings — let the enter-amount screen refresh.
236
+ nextScreen(recentRecipient, false)
235
237
  }
236
238
 
237
- const nextScreen = (selectedRecipient: Recipient) => {
239
+ const nextScreen = (selectedRecipient: Recipient, skipRecipientLookup: boolean) => {
238
240
  // use the address from the recipient object
239
241
  let address: string | null | undefined = selectedRecipient.address
240
242
 
@@ -271,7 +273,7 @@ function SendSelectRecipient({ route }: Props) {
271
273
  address,
272
274
  },
273
275
  origin: SendOrigin.AppSendFlow,
274
- isMiniPayRecipient: addressToVerifiedBy?.[address] === 'minipay',
276
+ skipRecipientLookup,
275
277
  })
276
278
  }
277
279