wallet-stack 1.0.0-alpha.137 → 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.
- package/package.json +1 -1
- package/src/navigator/types.tsx +3 -1
- package/src/send/EnterAmount.tsx +17 -0
- package/src/send/SelectRecipientAddress.test.tsx +4 -4
- package/src/send/SelectRecipientAddress.tsx +5 -56
- package/src/send/SelectRecipientAddressList.test.tsx +68 -0
- package/src/send/SelectRecipientAddressList.tsx +117 -0
- package/src/send/SelectedRecipientCard.test.tsx +96 -0
- package/src/send/SelectedRecipientCard.tsx +197 -0
- package/src/send/SendEnterAmount.test.tsx +22 -29
- package/src/send/SendEnterAmount.tsx +39 -4
- package/src/send/SendSelectRecipient.test.tsx +39 -44
- package/src/send/SendSelectRecipient.tsx +6 -4
- package/src/send/useRecipientLookup.test.tsx +139 -0
- package/src/send/useRecipientLookup.ts +74 -0
package/package.json
CHANGED
package/src/navigator/types.tsx
CHANGED
|
@@ -42,7 +42,9 @@ type SendEnterAmountParams = {
|
|
|
42
42
|
origin: SendOrigin
|
|
43
43
|
forceTokenId?: boolean
|
|
44
44
|
defaultTokenIdOverride?: string
|
|
45
|
-
|
|
45
|
+
// Set to true when the caller has just performed a fresh phone-number lookup
|
|
46
|
+
// (e.g. recipient picker) so the enter-amount screen can skip re-fetching mappings.
|
|
47
|
+
skipRecipientLookup?: boolean
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
interface SelectRecipientAddressParams {
|
package/src/send/EnterAmount.tsx
CHANGED
|
@@ -30,6 +30,7 @@ import { useSelector } from 'src/redux/hooks'
|
|
|
30
30
|
import EnterAmountOptions from 'src/send/EnterAmountOptions'
|
|
31
31
|
import { AmountEnteredIn } from 'src/send/types'
|
|
32
32
|
import { typeScale } from 'src/styles/fonts'
|
|
33
|
+
import Colors from 'src/styles/colors'
|
|
33
34
|
import { Spacing } from 'src/styles/styles'
|
|
34
35
|
import { feeCurrenciesSelector } from 'src/tokens/selectors'
|
|
35
36
|
import { TokenBalance } from 'src/tokens/slice'
|
|
@@ -69,6 +70,7 @@ interface Props {
|
|
|
69
70
|
ProceedComponent: ComponentType<ProceedComponentProps>
|
|
70
71
|
disableBalanceCheck?: boolean
|
|
71
72
|
filterChips?: FilterChip<TokenBalance>[]
|
|
73
|
+
recipientSlot?: React.ReactNode
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
export const SendProceed = ({
|
|
@@ -111,6 +113,7 @@ export default function EnterAmount({
|
|
|
111
113
|
ProceedComponent,
|
|
112
114
|
disableBalanceCheck = false,
|
|
113
115
|
filterChips,
|
|
116
|
+
recipientSlot,
|
|
114
117
|
}: Props) {
|
|
115
118
|
const { t } = useTranslation()
|
|
116
119
|
const insets = useSafeAreaInsets()
|
|
@@ -253,6 +256,13 @@ export default function EnterAmount({
|
|
|
253
256
|
onOpenTokenPicker={tokenSelectionDisabled ? undefined : onOpenTokenPicker}
|
|
254
257
|
/>
|
|
255
258
|
|
|
259
|
+
{!!recipientSlot && (
|
|
260
|
+
<>
|
|
261
|
+
<View style={styles.connectorLine} />
|
|
262
|
+
{recipientSlot}
|
|
263
|
+
</>
|
|
264
|
+
)}
|
|
265
|
+
|
|
256
266
|
{token &&
|
|
257
267
|
prepareTransactionsResult?.type !== 'not-enough-balance-for-gas' &&
|
|
258
268
|
!!networkFee && (
|
|
@@ -384,4 +394,11 @@ const styles = StyleSheet.create({
|
|
|
384
394
|
paddingHorizontal: Spacing.Regular16,
|
|
385
395
|
borderRadius: 16,
|
|
386
396
|
},
|
|
397
|
+
connectorLine: {
|
|
398
|
+
width: 2,
|
|
399
|
+
height: 12,
|
|
400
|
+
backgroundColor: Colors.borderPrimary,
|
|
401
|
+
alignSelf: 'center',
|
|
402
|
+
marginVertical: Spacing.Tiny4,
|
|
403
|
+
},
|
|
387
404
|
})
|
|
@@ -102,7 +102,7 @@ describe('SelectRecipientAddress', () => {
|
|
|
102
102
|
expect(queryByTestId(`SelectRecipientAddress/Row/${mockAccount3.toLowerCase()}`)).toBeNull()
|
|
103
103
|
})
|
|
104
104
|
|
|
105
|
-
it('navigates to SendEnterAmount on Valora row tap
|
|
105
|
+
it('navigates to SendEnterAmount on Valora row tap', () => {
|
|
106
106
|
const { getByTestId } = renderScreen({})
|
|
107
107
|
fireEvent.press(getByTestId(`SelectRecipientAddress/Row/${mockAccount2.toLowerCase()}`))
|
|
108
108
|
|
|
@@ -115,7 +115,7 @@ describe('SelectRecipientAddress', () => {
|
|
|
115
115
|
address: mockAccount2.toLowerCase(),
|
|
116
116
|
},
|
|
117
117
|
origin: SendOrigin.AppSendFlow,
|
|
118
|
-
|
|
118
|
+
skipRecipientLookup: true,
|
|
119
119
|
})
|
|
120
120
|
expect(AppAnalytics.track).toHaveBeenCalledWith(
|
|
121
121
|
SendEvents.send_select_recipient_address_select,
|
|
@@ -123,7 +123,7 @@ describe('SelectRecipientAddress', () => {
|
|
|
123
123
|
)
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
-
it('navigates to SendEnterAmount on MiniPay row tap
|
|
126
|
+
it('navigates to SendEnterAmount on MiniPay row tap', () => {
|
|
127
127
|
const { getByTestId } = renderScreen({})
|
|
128
128
|
fireEvent.press(getByTestId(`SelectRecipientAddress/Row/${mockAccount3.toLowerCase()}`))
|
|
129
129
|
|
|
@@ -136,7 +136,7 @@ describe('SelectRecipientAddress', () => {
|
|
|
136
136
|
address: mockAccount3.toLowerCase(),
|
|
137
137
|
},
|
|
138
138
|
origin: SendOrigin.AppSendFlow,
|
|
139
|
-
|
|
139
|
+
skipRecipientLookup: true,
|
|
140
140
|
})
|
|
141
141
|
expect(AppAnalytics.track).toHaveBeenCalledWith(
|
|
142
142
|
SendEvents.send_select_recipient_address_select,
|
|
@@ -1,35 +1,27 @@
|
|
|
1
1
|
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
2
2
|
import React, { useEffect } from 'react'
|
|
3
3
|
import { Trans, useTranslation } from 'react-i18next'
|
|
4
|
-
import {
|
|
4
|
+
import { ScrollView, StyleSheet, Text } from 'react-native'
|
|
5
5
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
6
|
-
import { formatShortenedAddress } from 'src/account/utils'
|
|
7
6
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
8
7
|
import { SendEvents } from 'src/analytics/Events'
|
|
9
8
|
import BackButton from 'src/components/BackButton'
|
|
10
|
-
import Touchable from 'src/components/Touchable'
|
|
11
9
|
import CustomHeader from 'src/components/header/CustomHeader'
|
|
12
|
-
import VerifiedBadge from 'src/icons/VerifiedBadge'
|
|
13
10
|
import { addressToVerifiedBySelector, e164NumberToAddressSelector } from 'src/identity/selectors'
|
|
14
11
|
import { noHeader } from 'src/navigator/Headers'
|
|
15
12
|
import { navigate } from 'src/navigator/NavigationService'
|
|
16
13
|
import { Screens } from 'src/navigator/Screens'
|
|
17
14
|
import { StackParamList } from 'src/navigator/types'
|
|
18
15
|
import { getDisplayName } from 'src/recipients/recipient'
|
|
19
|
-
import {
|
|
16
|
+
import { Verifier, isKnownVerifier } from 'src/recipients/verifier'
|
|
20
17
|
import { useSelector } from 'src/redux/hooks'
|
|
18
|
+
import SelectRecipientAddressList from 'src/send/SelectRecipientAddressList'
|
|
21
19
|
import Colors from 'src/styles/colors'
|
|
22
20
|
import { typeScale } from 'src/styles/fonts'
|
|
23
21
|
import { Spacing } from 'src/styles/styles'
|
|
24
22
|
|
|
25
23
|
type Props = NativeStackScreenProps<StackParamList, Screens.SelectRecipientAddress>
|
|
26
24
|
|
|
27
|
-
const ICON_SIZE = 40
|
|
28
|
-
|
|
29
|
-
function VerifierIcon({ verifier }: { verifier: Verifier }) {
|
|
30
|
-
return <Image source={VERIFIERS[verifier].icon} style={styles.icon} resizeMode="contain" />
|
|
31
|
-
}
|
|
32
|
-
|
|
33
25
|
function SelectRecipientAddress({ route }: Props) {
|
|
34
26
|
const { t } = useTranslation()
|
|
35
27
|
const { recipient, origin, forceTokenId, defaultTokenIdOverride } = route.params
|
|
@@ -63,7 +55,7 @@ function SelectRecipientAddress({ route }: Props) {
|
|
|
63
55
|
address,
|
|
64
56
|
},
|
|
65
57
|
origin,
|
|
66
|
-
|
|
58
|
+
skipRecipientLookup: true,
|
|
67
59
|
})
|
|
68
60
|
}
|
|
69
61
|
|
|
@@ -83,24 +75,7 @@ function SelectRecipientAddress({ route }: Props) {
|
|
|
83
75
|
<Text style={styles.explanationName} />
|
|
84
76
|
</Trans>
|
|
85
77
|
</Text>
|
|
86
|
-
{verifiedEntries
|
|
87
|
-
<Touchable
|
|
88
|
-
key={address}
|
|
89
|
-
onPress={() => onSelectAddress(address, verifier)}
|
|
90
|
-
testID={`SelectRecipientAddress/Row/${address}`}
|
|
91
|
-
>
|
|
92
|
-
<View style={styles.row}>
|
|
93
|
-
<VerifierIcon verifier={verifier} />
|
|
94
|
-
<View style={styles.rowContent}>
|
|
95
|
-
<Text style={styles.address}>{formatShortenedAddress(address)}</Text>
|
|
96
|
-
<View style={styles.verifier}>
|
|
97
|
-
<VerifiedBadge color={Colors.contentSecondary} />
|
|
98
|
-
<Text style={styles.verifierName}>{VERIFIERS[verifier].name}</Text>
|
|
99
|
-
</View>
|
|
100
|
-
</View>
|
|
101
|
-
</View>
|
|
102
|
-
</Touchable>
|
|
103
|
-
))}
|
|
78
|
+
<SelectRecipientAddressList entries={verifiedEntries} onSelectAddress={onSelectAddress} />
|
|
104
79
|
</ScrollView>
|
|
105
80
|
</SafeAreaView>
|
|
106
81
|
)
|
|
@@ -131,32 +106,6 @@ const styles = StyleSheet.create({
|
|
|
131
106
|
explanationName: {
|
|
132
107
|
fontWeight: 'bold',
|
|
133
108
|
},
|
|
134
|
-
icon: {
|
|
135
|
-
width: ICON_SIZE,
|
|
136
|
-
height: ICON_SIZE,
|
|
137
|
-
},
|
|
138
|
-
row: {
|
|
139
|
-
flexDirection: 'row',
|
|
140
|
-
alignItems: 'center',
|
|
141
|
-
paddingHorizontal: Spacing.Regular16,
|
|
142
|
-
paddingVertical: Spacing.Regular16,
|
|
143
|
-
},
|
|
144
|
-
rowContent: {
|
|
145
|
-
flex: 1,
|
|
146
|
-
marginLeft: Spacing.Small12,
|
|
147
|
-
},
|
|
148
|
-
address: {
|
|
149
|
-
...typeScale.labelMedium,
|
|
150
|
-
},
|
|
151
|
-
verifier: {
|
|
152
|
-
flexDirection: 'row',
|
|
153
|
-
alignItems: 'center',
|
|
154
|
-
gap: Spacing.Tiny4,
|
|
155
|
-
},
|
|
156
|
-
verifierName: {
|
|
157
|
-
...typeScale.bodySmall,
|
|
158
|
-
color: Colors.contentSecondary,
|
|
159
|
-
},
|
|
160
109
|
})
|
|
161
110
|
|
|
162
111
|
SelectRecipientAddress.navigationOptions = noHeader
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/react-native'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import SelectRecipientAddressList from 'src/send/SelectRecipientAddressList'
|
|
4
|
+
|
|
5
|
+
const mockAddress = '0x0000000000000000000000000000000000000001'
|
|
6
|
+
const mockAddress2 = '0x0000000000000000000000000000000000000002'
|
|
7
|
+
|
|
8
|
+
describe('SelectRecipientAddressList', () => {
|
|
9
|
+
it('renders one row per entry with the verifier name and a shortened address', () => {
|
|
10
|
+
const { getByTestId } = render(
|
|
11
|
+
<SelectRecipientAddressList
|
|
12
|
+
entries={[
|
|
13
|
+
{ address: mockAddress, verifier: 'valora' },
|
|
14
|
+
{ address: mockAddress2, verifier: 'minipay' },
|
|
15
|
+
]}
|
|
16
|
+
onSelectAddress={jest.fn()}
|
|
17
|
+
/>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const valoraRow = getByTestId(`SelectRecipientAddress/Row/${mockAddress}`)
|
|
21
|
+
expect(valoraRow).toHaveTextContent('Valora', { exact: false })
|
|
22
|
+
expect(valoraRow).toHaveTextContent('0x0000...0001', { exact: false })
|
|
23
|
+
|
|
24
|
+
const minipayRow = getByTestId(`SelectRecipientAddress/Row/${mockAddress2}`)
|
|
25
|
+
expect(minipayRow).toHaveTextContent('MiniPay', { exact: false })
|
|
26
|
+
expect(minipayRow).toHaveTextContent('0x0000...0002', { exact: false })
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('renders an unverified row with a warning label when verifier is null', () => {
|
|
30
|
+
const { getByTestId } = render(
|
|
31
|
+
<SelectRecipientAddressList
|
|
32
|
+
entries={[{ address: mockAddress, verifier: null }]}
|
|
33
|
+
onSelectAddress={jest.fn()}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const row = getByTestId(`SelectRecipientAddress/Row/${mockAddress}`)
|
|
38
|
+
expect(row).toHaveTextContent('unverifiedAddress', { exact: false })
|
|
39
|
+
expect(row).toHaveTextContent('0x0000...0001', { exact: false })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('invokes onSelectAddress with null verifier when an unverified row is tapped', () => {
|
|
43
|
+
const onSelectAddress = jest.fn()
|
|
44
|
+
const { getByTestId } = render(
|
|
45
|
+
<SelectRecipientAddressList
|
|
46
|
+
entries={[{ address: mockAddress, verifier: null }]}
|
|
47
|
+
onSelectAddress={onSelectAddress}
|
|
48
|
+
/>
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
fireEvent.press(getByTestId(`SelectRecipientAddress/Row/${mockAddress}`))
|
|
52
|
+
expect(onSelectAddress).toHaveBeenCalledWith(mockAddress, null)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('invokes onSelectAddress with the row address and verifier when tapped', () => {
|
|
56
|
+
const onSelectAddress = jest.fn()
|
|
57
|
+
const { getByTestId } = render(
|
|
58
|
+
<SelectRecipientAddressList
|
|
59
|
+
entries={[{ address: mockAddress, verifier: 'minipay' }]}
|
|
60
|
+
onSelectAddress={onSelectAddress}
|
|
61
|
+
/>
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
fireEvent.press(getByTestId(`SelectRecipientAddress/Row/${mockAddress}`))
|
|
65
|
+
|
|
66
|
+
expect(onSelectAddress).toHaveBeenCalledWith(mockAddress, 'minipay')
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Image, StyleSheet, Text, View } from 'react-native'
|
|
4
|
+
import { formatShortenedAddress } from 'src/account/utils'
|
|
5
|
+
import Touchable from 'src/components/Touchable'
|
|
6
|
+
import AttentionIcon from 'src/icons/Attention'
|
|
7
|
+
import WalletIcon from 'src/icons/navigator/Wallet'
|
|
8
|
+
import VerifiedBadge from 'src/icons/VerifiedBadge'
|
|
9
|
+
import { VERIFIERS, Verifier } from 'src/recipients/verifier'
|
|
10
|
+
import Colors from 'src/styles/colors'
|
|
11
|
+
import { typeScale } from 'src/styles/fonts'
|
|
12
|
+
import { Spacing } from 'src/styles/styles'
|
|
13
|
+
|
|
14
|
+
const ICON_SIZE = 40
|
|
15
|
+
|
|
16
|
+
function VerifierIcon({ verifier }: { verifier: Verifier | null }) {
|
|
17
|
+
if (!verifier) {
|
|
18
|
+
return (
|
|
19
|
+
<View style={styles.unverifiedIcon}>
|
|
20
|
+
<WalletIcon color={Colors.contentPrimary} size={24} />
|
|
21
|
+
</View>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
return <Image source={VERIFIERS[verifier].icon} style={styles.icon} resizeMode="contain" />
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Entry {
|
|
28
|
+
address: string
|
|
29
|
+
verifier: Verifier | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
entries: Entry[]
|
|
34
|
+
onSelectAddress(address: string, verifier: Verifier | null): void
|
|
35
|
+
// Compact rows for the in-sheet variant — the standalone screen keeps the original padding.
|
|
36
|
+
compact?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function SelectRecipientAddressList({ entries, onSelectAddress, compact }: Props) {
|
|
40
|
+
const { t } = useTranslation()
|
|
41
|
+
return (
|
|
42
|
+
<>
|
|
43
|
+
{entries.map(({ address, verifier }) => (
|
|
44
|
+
<Touchable
|
|
45
|
+
key={address}
|
|
46
|
+
onPress={() => onSelectAddress(address, verifier)}
|
|
47
|
+
testID={`SelectRecipientAddress/Row/${address}`}
|
|
48
|
+
>
|
|
49
|
+
<View style={[styles.row, compact && styles.rowCompact]}>
|
|
50
|
+
<VerifierIcon verifier={verifier} />
|
|
51
|
+
<View style={styles.rowContent}>
|
|
52
|
+
<Text style={styles.address}>{formatShortenedAddress(address)}</Text>
|
|
53
|
+
{verifier ? (
|
|
54
|
+
<View style={styles.verifier}>
|
|
55
|
+
<VerifiedBadge color={Colors.contentSecondary} />
|
|
56
|
+
<Text style={styles.verifierName}>{VERIFIERS[verifier].name}</Text>
|
|
57
|
+
</View>
|
|
58
|
+
) : (
|
|
59
|
+
<View style={styles.verifier}>
|
|
60
|
+
<AttentionIcon size={14} color={Colors.warningPrimary} />
|
|
61
|
+
<Text style={styles.unverifiedText}>{t('unverifiedAddress')}</Text>
|
|
62
|
+
</View>
|
|
63
|
+
)}
|
|
64
|
+
</View>
|
|
65
|
+
</View>
|
|
66
|
+
</Touchable>
|
|
67
|
+
))}
|
|
68
|
+
</>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const styles = StyleSheet.create({
|
|
73
|
+
icon: {
|
|
74
|
+
width: ICON_SIZE,
|
|
75
|
+
height: ICON_SIZE,
|
|
76
|
+
},
|
|
77
|
+
unverifiedIcon: {
|
|
78
|
+
width: ICON_SIZE,
|
|
79
|
+
height: ICON_SIZE,
|
|
80
|
+
borderRadius: ICON_SIZE / 2,
|
|
81
|
+
backgroundColor: Colors.backgroundSecondary,
|
|
82
|
+
borderWidth: 1,
|
|
83
|
+
borderColor: Colors.borderPrimary,
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
justifyContent: 'center',
|
|
86
|
+
},
|
|
87
|
+
row: {
|
|
88
|
+
flexDirection: 'row',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
paddingHorizontal: Spacing.Regular16,
|
|
91
|
+
paddingVertical: Spacing.Regular16,
|
|
92
|
+
},
|
|
93
|
+
rowCompact: {
|
|
94
|
+
paddingHorizontal: 0,
|
|
95
|
+
paddingVertical: Spacing.Smallest8,
|
|
96
|
+
},
|
|
97
|
+
rowContent: {
|
|
98
|
+
flex: 1,
|
|
99
|
+
marginLeft: Spacing.Small12,
|
|
100
|
+
},
|
|
101
|
+
address: {
|
|
102
|
+
...typeScale.labelMedium,
|
|
103
|
+
},
|
|
104
|
+
verifier: {
|
|
105
|
+
flexDirection: 'row',
|
|
106
|
+
alignItems: 'center',
|
|
107
|
+
gap: Spacing.Tiny4,
|
|
108
|
+
},
|
|
109
|
+
verifierName: {
|
|
110
|
+
...typeScale.bodySmall,
|
|
111
|
+
color: Colors.contentSecondary,
|
|
112
|
+
},
|
|
113
|
+
unverifiedText: {
|
|
114
|
+
...typeScale.bodySmall,
|
|
115
|
+
color: Colors.warningPrimary,
|
|
116
|
+
},
|
|
117
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { fireEvent, render } from '@testing-library/react-native'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Provider } from 'react-redux'
|
|
4
|
+
import { Recipient, RecipientType } from 'src/recipients/recipient'
|
|
5
|
+
import SelectedRecipientCard from 'src/send/SelectedRecipientCard'
|
|
6
|
+
import { createMockStore } from 'test/utils'
|
|
7
|
+
import { mockAccount, mockAccount2, mockE164Number, mockName } from 'test/values'
|
|
8
|
+
|
|
9
|
+
jest.mock('src/components/BottomSheet', () => {
|
|
10
|
+
const React = require('react')
|
|
11
|
+
const { View } = require('react-native')
|
|
12
|
+
// Render the sheet's children inline so the verified rows are queryable in tests.
|
|
13
|
+
const BottomSheet = React.forwardRef(({ children }: any, _ref: any) =>
|
|
14
|
+
React.createElement(View, { testID: 'BottomSheet' }, children)
|
|
15
|
+
)
|
|
16
|
+
return { __esModule: true, default: BottomSheet }
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const phoneRecipient = {
|
|
20
|
+
name: mockName,
|
|
21
|
+
e164PhoneNumber: mockE164Number,
|
|
22
|
+
recipientType: RecipientType.PhoneNumber,
|
|
23
|
+
address: mockAccount,
|
|
24
|
+
} as Recipient & { address: string }
|
|
25
|
+
|
|
26
|
+
function renderCard(
|
|
27
|
+
props: Partial<React.ComponentProps<typeof SelectedRecipientCard>> = {},
|
|
28
|
+
storeOverrides: Record<string, unknown> = {}
|
|
29
|
+
) {
|
|
30
|
+
const onSelectAddress = jest.fn()
|
|
31
|
+
const utils = render(
|
|
32
|
+
<Provider store={createMockStore(storeOverrides)}>
|
|
33
|
+
<SelectedRecipientCard
|
|
34
|
+
recipient={phoneRecipient}
|
|
35
|
+
status="verified"
|
|
36
|
+
verifiedAddresses={[]}
|
|
37
|
+
originalAddress={phoneRecipient.address}
|
|
38
|
+
onSelectAddress={onSelectAddress}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
</Provider>
|
|
42
|
+
)
|
|
43
|
+
return { ...utils, onSelectAddress }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('SelectedRecipientCard', () => {
|
|
47
|
+
it('shows the recipient name as the title and a spinner while the lookup is loading', () => {
|
|
48
|
+
const { getByTestId } = renderCard({ status: 'loading' })
|
|
49
|
+
expect(getByTestId('SelectedRecipientCard')).toHaveTextContent(mockName, { exact: false })
|
|
50
|
+
expect(getByTestId('SelectedRecipientCard/Spinner')).toBeTruthy()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('shows an unverified warning subtitle when the resolved address is known-unverified', () => {
|
|
54
|
+
const { getByTestId } = renderCard({ status: 'unverified' })
|
|
55
|
+
expect(getByTestId('SelectedRecipientCard/Unverified')).toHaveTextContent('unverifiedAddress', {
|
|
56
|
+
exact: false,
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('is not tappable when there is only one address option (the current one)', () => {
|
|
61
|
+
const { getByTestId } = renderCard({
|
|
62
|
+
status: 'verified',
|
|
63
|
+
verifiedAddresses: [{ address: mockAccount.toLowerCase(), verifier: 'valora' }],
|
|
64
|
+
})
|
|
65
|
+
const touchable = getByTestId('SelectedRecipientCard/Touchable')
|
|
66
|
+
expect(touchable).toBeDisabled()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('opens the sheet and invokes onSelectAddress when tapping a row', () => {
|
|
70
|
+
const { getByTestId, onSelectAddress } = renderCard({
|
|
71
|
+
status: 'verified',
|
|
72
|
+
verifiedAddresses: [
|
|
73
|
+
{ address: mockAccount.toLowerCase(), verifier: 'valora' },
|
|
74
|
+
{ address: mockAccount2.toLowerCase(), verifier: 'minipay' },
|
|
75
|
+
],
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
fireEvent.press(getByTestId(`SelectRecipientAddress/Row/${mockAccount2.toLowerCase()}`))
|
|
79
|
+
expect(onSelectAddress).toHaveBeenCalledWith(mockAccount2.toLowerCase())
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('keeps the original (unverified) address selectable in the sheet alongside verified options', () => {
|
|
83
|
+
const { getByTestId, onSelectAddress } = renderCard({
|
|
84
|
+
status: 'verified',
|
|
85
|
+
// Verified set does not include the original address — common after the user picked a
|
|
86
|
+
// verified option from a recipient that came in via recents with a stale mapping.
|
|
87
|
+
verifiedAddresses: [{ address: mockAccount2.toLowerCase(), verifier: 'valora' }],
|
|
88
|
+
originalAddress: mockAccount,
|
|
89
|
+
recipient: { ...phoneRecipient, address: mockAccount2.toLowerCase() },
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// The original (unverified) address still renders as a selectable row.
|
|
93
|
+
fireEvent.press(getByTestId(`SelectRecipientAddress/Row/${mockAccount}`))
|
|
94
|
+
expect(onSelectAddress).toHaveBeenCalledWith(mockAccount)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -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)
|
|
@@ -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
|
|
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={
|
|
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={
|
|
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={
|
|
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
|
|
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
|
|
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={
|
|
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={
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
+
skipRecipientLookup,
|
|
275
277
|
})
|
|
276
278
|
}
|
|
277
279
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-native'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Provider } from 'react-redux'
|
|
4
|
+
import {
|
|
5
|
+
fetchAddressVerification,
|
|
6
|
+
fetchAddressesAndValidate,
|
|
7
|
+
recipientLookupResolved,
|
|
8
|
+
updateE164PhoneNumberAddresses,
|
|
9
|
+
} from 'src/identity/actions'
|
|
10
|
+
import { Recipient, RecipientType } from 'src/recipients/recipient'
|
|
11
|
+
import { setupStore } from 'src/redux/store'
|
|
12
|
+
import { useRecipientLookup } from 'src/send/useRecipientLookup'
|
|
13
|
+
import { getMockStoreData } from 'test/utils'
|
|
14
|
+
import { mockAccount, mockAccount2, mockE164Number, mockName } from 'test/values'
|
|
15
|
+
|
|
16
|
+
jest.mock('src/redux/sagas', () => ({
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
18
|
+
rootSaga: jest.fn(function* () {}),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
const phoneRecipient: Recipient & { address: string } = {
|
|
22
|
+
name: mockName,
|
|
23
|
+
contactId: 'contactId',
|
|
24
|
+
e164PhoneNumber: mockE164Number,
|
|
25
|
+
recipientType: RecipientType.PhoneNumber,
|
|
26
|
+
address: mockAccount,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const addressRecipient: Recipient & { address: string } = {
|
|
30
|
+
address: mockAccount,
|
|
31
|
+
recipientType: RecipientType.Address,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setupHook(
|
|
35
|
+
recipient: Recipient & { address: string },
|
|
36
|
+
options?: { skipFetch?: boolean },
|
|
37
|
+
storeOverrides: Parameters<typeof getMockStoreData>[0] = {}
|
|
38
|
+
) {
|
|
39
|
+
const { store } = setupStore(getMockStoreData(storeOverrides))
|
|
40
|
+
const dispatchSpy = jest.spyOn(store, 'dispatch')
|
|
41
|
+
const { result, rerender } = renderHook(() => useRecipientLookup(recipient, options), {
|
|
42
|
+
wrapper: ({ children }: { children: React.ReactNode }) => (
|
|
43
|
+
<Provider store={store}>{children}</Provider>
|
|
44
|
+
),
|
|
45
|
+
})
|
|
46
|
+
return { result, rerender, store, dispatchSpy }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('useRecipientLookup', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.clearAllMocks()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('dispatches fetchAddressesAndValidate for phone recipients', () => {
|
|
55
|
+
const { dispatchSpy } = setupHook(phoneRecipient)
|
|
56
|
+
expect(dispatchSpy).toHaveBeenCalledWith(fetchAddressesAndValidate(mockE164Number))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('dispatches fetchAddressVerification for address-only recipients', () => {
|
|
60
|
+
const { dispatchSpy } = setupHook(addressRecipient)
|
|
61
|
+
expect(dispatchSpy).toHaveBeenCalledWith(fetchAddressVerification(mockAccount.toLowerCase()))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('does not dispatch when skipFetch is set (caller already triggered the lookup)', () => {
|
|
65
|
+
const { dispatchSpy } = setupHook(phoneRecipient, { skipFetch: true })
|
|
66
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(fetchAddressesAndValidate(mockE164Number))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('reports loading while the lookup is in flight and resolves to unknown when no verifier is recorded', () => {
|
|
70
|
+
const { result, store } = setupHook(addressRecipient)
|
|
71
|
+
expect(result.current.status).toBe('loading')
|
|
72
|
+
|
|
73
|
+
act(() => {
|
|
74
|
+
store.dispatch(recipientLookupResolved())
|
|
75
|
+
})
|
|
76
|
+
expect(result.current.status).toBe('unknown')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns "unverified" for a phone recipient when the lookup resolved with no verified addresses', () => {
|
|
80
|
+
const { result, store } = setupHook(phoneRecipient, { skipFetch: true })
|
|
81
|
+
act(() => {
|
|
82
|
+
store.dispatch(updateE164PhoneNumberAddresses({ [mockE164Number]: null }, {}))
|
|
83
|
+
})
|
|
84
|
+
expect(result.current.status).toBe('unverified')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('returns "verified" when the address has a known verifier', () => {
|
|
88
|
+
const { result } = setupHook(
|
|
89
|
+
addressRecipient,
|
|
90
|
+
{ skipFetch: true },
|
|
91
|
+
{
|
|
92
|
+
identity: {
|
|
93
|
+
addressToVerifiedBy: { [mockAccount.toLowerCase()]: 'valora' },
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
expect(result.current.status).toBe('verified')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('returns "unverified" when the address is known to be unverified (verifier=null)', () => {
|
|
101
|
+
const { result } = setupHook(
|
|
102
|
+
addressRecipient,
|
|
103
|
+
{ skipFetch: true },
|
|
104
|
+
{
|
|
105
|
+
identity: {
|
|
106
|
+
addressToVerifiedBy: { [mockAccount.toLowerCase()]: null },
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
expect(result.current.status).toBe('unverified')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('returns the verified addresses linked to a phone recipient, filtering out unverified ones', () => {
|
|
114
|
+
const { result, store } = setupHook(
|
|
115
|
+
phoneRecipient,
|
|
116
|
+
{ skipFetch: true },
|
|
117
|
+
{
|
|
118
|
+
identity: {
|
|
119
|
+
addressToVerifiedBy: {
|
|
120
|
+
[mockAccount.toLowerCase()]: 'valora',
|
|
121
|
+
[mockAccount2.toLowerCase()]: null,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
act(() => {
|
|
127
|
+
store.dispatch(
|
|
128
|
+
updateE164PhoneNumberAddresses(
|
|
129
|
+
{ [mockE164Number]: [mockAccount.toLowerCase(), mockAccount2.toLowerCase()] },
|
|
130
|
+
{}
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(result.current.verifiedAddresses).toEqual([
|
|
136
|
+
{ address: mockAccount.toLowerCase(), verifier: 'valora' },
|
|
137
|
+
])
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { fetchAddressVerification, fetchAddressesAndValidate } from 'src/identity/actions'
|
|
3
|
+
import type { AddressToVerifiedByType } from 'src/identity/reducer'
|
|
4
|
+
import {
|
|
5
|
+
addressToVerifiedBySelector,
|
|
6
|
+
e164NumberToAddressSelector,
|
|
7
|
+
recipientLookupLoadingSelector,
|
|
8
|
+
} from 'src/identity/selectors'
|
|
9
|
+
import { Recipient } from 'src/recipients/recipient'
|
|
10
|
+
import { type Verifier, isKnownVerifier } from 'src/recipients/verifier'
|
|
11
|
+
import { useDispatch, useSelector } from 'src/redux/hooks'
|
|
12
|
+
|
|
13
|
+
export type RecipientLookupStatus = 'unknown' | 'loading' | 'verified' | 'unverified'
|
|
14
|
+
|
|
15
|
+
export interface VerifiedAddressEntry {
|
|
16
|
+
address: string
|
|
17
|
+
verifier: Verifier
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getVerifiedAddresses(
|
|
21
|
+
addresses: readonly string[],
|
|
22
|
+
addressToVerifiedBy: AddressToVerifiedByType
|
|
23
|
+
): VerifiedAddressEntry[] {
|
|
24
|
+
return addresses
|
|
25
|
+
.map((address) => ({ address, verifier: addressToVerifiedBy[address] }))
|
|
26
|
+
.filter((entry): entry is VerifiedAddressEntry => isKnownVerifier(entry.verifier))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Dispatches a verification lookup for `recipient` and returns the status plus any verified
|
|
31
|
+
* addresses linked to the phone number. Pass `skipFetch` when the caller has already triggered
|
|
32
|
+
* a fresh lookup and re-fetching would be wasteful.
|
|
33
|
+
*/
|
|
34
|
+
export function useRecipientLookup(
|
|
35
|
+
recipient: Recipient & { address: string },
|
|
36
|
+
options: { skipFetch?: boolean } = {}
|
|
37
|
+
): { status: RecipientLookupStatus; verifiedAddresses: VerifiedAddressEntry[] } {
|
|
38
|
+
const dispatch = useDispatch()
|
|
39
|
+
const e164PhoneNumber = recipient.e164PhoneNumber
|
|
40
|
+
const recipientAddress = recipient.address.toLowerCase()
|
|
41
|
+
const skipFetch = !!options.skipFetch
|
|
42
|
+
|
|
43
|
+
const e164NumberToAddress = useSelector(e164NumberToAddressSelector)
|
|
44
|
+
const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
|
|
45
|
+
const recipientLookupLoading = useSelector(recipientLookupLoadingSelector)
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (skipFetch || !e164PhoneNumber) return
|
|
49
|
+
dispatch(fetchAddressesAndValidate(e164PhoneNumber))
|
|
50
|
+
}, [dispatch, e164PhoneNumber, skipFetch])
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (skipFetch || e164PhoneNumber) return
|
|
54
|
+
dispatch(fetchAddressVerification(recipientAddress))
|
|
55
|
+
}, [dispatch, e164PhoneNumber, recipientAddress, skipFetch])
|
|
56
|
+
|
|
57
|
+
const cachedAddresses = e164PhoneNumber ? e164NumberToAddress[e164PhoneNumber] : undefined
|
|
58
|
+
const verifiedAddresses = getVerifiedAddresses(cachedAddresses ?? [], addressToVerifiedBy)
|
|
59
|
+
|
|
60
|
+
const verifier = addressToVerifiedBy[recipientAddress]
|
|
61
|
+
|
|
62
|
+
let status: RecipientLookupStatus
|
|
63
|
+
if (recipientLookupLoading) {
|
|
64
|
+
status = 'loading'
|
|
65
|
+
} else if (isKnownVerifier(verifier)) {
|
|
66
|
+
status = 'verified'
|
|
67
|
+
} else if (verifier === null || cachedAddresses === null) {
|
|
68
|
+
status = 'unverified'
|
|
69
|
+
} else {
|
|
70
|
+
status = 'unknown'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { status, verifiedAddresses }
|
|
74
|
+
}
|