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.
- package/locales/base/translation.json +1 -0
- package/package.json +1 -1
- package/src/components/ReviewTransaction.test.tsx +26 -0
- package/src/components/ReviewTransaction.tsx +28 -7
- package/src/navigator/types.tsx +3 -1
- package/src/recipients/verifier.ts +5 -3
- 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/SendConfirmation.test.tsx +23 -2
- package/src/send/SendConfirmation.tsx +1 -4
- 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
|
@@ -2174,6 +2174,7 @@
|
|
|
2174
2174
|
"recipient": "Recipient",
|
|
2175
2175
|
"explanation": "<0>{{name}}</0> has more than one wallet linked to the phone number. Choose which one to send to."
|
|
2176
2176
|
},
|
|
2177
|
+
"unverifiedAddress": "Unverified",
|
|
2177
2178
|
"sendSelectRecipient": {
|
|
2178
2179
|
"searchText": "Search by name, phone, wallet...",
|
|
2179
2180
|
"searchInputLabel": "To",
|
package/package.json
CHANGED
|
@@ -172,6 +172,32 @@ describe('ReviewSummaryItemContact', () => {
|
|
|
172
172
|
expect(subtitle).toHaveTextContent('MiniPay', { exact: false })
|
|
173
173
|
})
|
|
174
174
|
|
|
175
|
+
it('shows the unverified warning in the subtitle for phone recipients with a known-unverified address', () => {
|
|
176
|
+
const address = '0x0123456789012345678901234567890123456789'
|
|
177
|
+
const recipient = {
|
|
178
|
+
name: 'John Doe',
|
|
179
|
+
e164PhoneNumber: '+222222222',
|
|
180
|
+
address,
|
|
181
|
+
} as Recipient
|
|
182
|
+
const tree = renderContact(recipient, {
|
|
183
|
+
identity: { addressToVerifiedBy: { [address]: null } },
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const subtitle = tree.getByTestId('ContactItem/SecondaryValue')
|
|
187
|
+
expect(subtitle).toHaveTextContent('0x0123...6789', { exact: false })
|
|
188
|
+
expect(subtitle).toHaveTextContent('unverifiedAddress', { exact: false })
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('omits the unverified warning for address-only recipients (the address is already the primary value)', () => {
|
|
192
|
+
const address = '0x0123456789012345678901234567890123456789'
|
|
193
|
+
const recipient = { address } as Recipient
|
|
194
|
+
const tree = renderContact(recipient, {
|
|
195
|
+
identity: { addressToVerifiedBy: { [address]: null } },
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(tree.queryByTestId('ContactItem/SecondaryValue')).toBeNull()
|
|
199
|
+
})
|
|
200
|
+
|
|
175
201
|
it('logs an error if no name/phone/address exist', () => {
|
|
176
202
|
const recipient = {} as Recipient
|
|
177
203
|
const tree = renderContact(recipient)
|
|
@@ -11,6 +11,7 @@ import CustomHeader from 'src/components/header/CustomHeader'
|
|
|
11
11
|
import SkeletonPlaceholder from 'src/components/SkeletonPlaceholder'
|
|
12
12
|
import { formatValueToDisplay } from 'src/components/TokenDisplay'
|
|
13
13
|
import Touchable from 'src/components/Touchable'
|
|
14
|
+
import AttentionIcon from 'src/icons/Attention'
|
|
14
15
|
import InfoIcon from 'src/icons/InfoIcon'
|
|
15
16
|
import WalletIcon from 'src/icons/navigator/Wallet'
|
|
16
17
|
import PhoneIcon from 'src/icons/Phone'
|
|
@@ -126,17 +127,31 @@ export function ReviewSummaryItem(props: {
|
|
|
126
127
|
|
|
127
128
|
function renderAddressAndVerifier(
|
|
128
129
|
shortAddress: string | undefined,
|
|
129
|
-
verifierName: string | undefined
|
|
130
|
+
verifierName: string | null | undefined
|
|
130
131
|
): ReactNode {
|
|
131
|
-
if (!shortAddress &&
|
|
132
|
+
if (!shortAddress && verifierName === undefined) return undefined
|
|
133
|
+
const isUnverified = verifierName === null
|
|
132
134
|
return (
|
|
133
135
|
<>
|
|
134
|
-
{!!shortAddress &&
|
|
135
|
-
|
|
136
|
+
{!!shortAddress && (
|
|
137
|
+
<Text style={[styles.reviewSummaryItemSecondaryValue, isUnverified && styles.warningText]}>
|
|
138
|
+
{shortAddress}
|
|
139
|
+
</Text>
|
|
140
|
+
)}
|
|
141
|
+
{isUnverified ? (
|
|
136
142
|
<>
|
|
137
|
-
<
|
|
138
|
-
<Text style={styles.reviewSummaryItemSecondaryValue}>
|
|
143
|
+
<AttentionIcon size={14} color={colors.warningPrimary} />
|
|
144
|
+
<Text style={[styles.reviewSummaryItemSecondaryValue, styles.warningText]}>
|
|
145
|
+
<Trans i18nKey="unverifiedAddress" />
|
|
146
|
+
</Text>
|
|
139
147
|
</>
|
|
148
|
+
) : (
|
|
149
|
+
!!verifierName && (
|
|
150
|
+
<>
|
|
151
|
+
<VerifiedBadge color={colors.contentSecondary} />
|
|
152
|
+
<Text style={styles.reviewSummaryItemSecondaryValue}>{verifierName}</Text>
|
|
153
|
+
</>
|
|
154
|
+
)
|
|
140
155
|
)}
|
|
141
156
|
</>
|
|
142
157
|
)
|
|
@@ -155,6 +170,7 @@ export function ReviewSummaryItemContact({
|
|
|
155
170
|
const phone = recipient.displayNumber || recipient.e164PhoneNumber
|
|
156
171
|
// For recipients with a phone mapping, surface the resolved on-chain address (and verifier,
|
|
157
172
|
// if known) as a subtitle so the user can verify the actual destination they are signing.
|
|
173
|
+
// When the address is known to be unverified, swap the verified badge for a warning.
|
|
158
174
|
const shortAddress = recipient.address ? formatShortenedAddress(recipient.address) : undefined
|
|
159
175
|
const phoneSubtitle = renderAddressAndVerifier(shortAddress, verifierName)
|
|
160
176
|
|
|
@@ -169,7 +185,9 @@ export function ReviewSummaryItemContact({
|
|
|
169
185
|
if (recipient.address) {
|
|
170
186
|
return {
|
|
171
187
|
title: recipient.address,
|
|
172
|
-
|
|
188
|
+
// For plain wallet recipients, suppress the unverified warning
|
|
189
|
+
// by collapsing `null` to `undefined`.
|
|
190
|
+
subtitle: renderAddressAndVerifier(undefined, verifierName ?? undefined),
|
|
173
191
|
icon: WalletIcon,
|
|
174
192
|
}
|
|
175
193
|
}
|
|
@@ -513,6 +531,9 @@ const styles = StyleSheet.create({
|
|
|
513
531
|
...typeScale.bodySmall,
|
|
514
532
|
color: colors.contentSecondary,
|
|
515
533
|
},
|
|
534
|
+
warningText: {
|
|
535
|
+
color: colors.warningPrimary,
|
|
536
|
+
},
|
|
516
537
|
reviewSummaryItemSecondaryValueWrapper: {
|
|
517
538
|
flexDirection: 'row',
|
|
518
539
|
gap: Spacing.Smallest8,
|
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 {
|
|
@@ -13,8 +13,10 @@ export function isKnownVerifier(verifier: string | null | undefined): verifier i
|
|
|
13
13
|
return !!verifier && Object.hasOwn(VERIFIERS, verifier)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function useVerifierName(address: string | undefined): string | undefined {
|
|
16
|
+
export function useVerifierName(address: string | undefined): string | null | undefined {
|
|
17
17
|
const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
if (!address) return undefined
|
|
19
|
+
const verifier = addressToVerifiedBy[address.toLowerCase()]
|
|
20
|
+
if (isKnownVerifier(verifier)) return VERIFIERS[verifier].name
|
|
21
|
+
return verifier === null ? null : undefined
|
|
20
22
|
}
|
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
|
+
})
|