wallet-stack 1.0.0-alpha.133 → 1.0.0-alpha.134
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/analytics/Properties.tsx +1 -1
- package/src/components/ReviewTransaction.test.tsx +49 -8
- package/src/components/ReviewTransaction.tsx +58 -12
- package/src/icons/VerifiedBadge.tsx +27 -0
- package/src/identity/actions.ts +0 -17
- package/src/identity/contactMapping.test.ts +78 -19
- package/src/identity/contactMapping.ts +50 -20
- package/src/identity/reducer.ts +7 -21
- package/src/identity/selectors.ts +0 -2
- package/src/recipients/RecipientItemV2.test.tsx +11 -72
- package/src/recipients/RecipientItemV2.tsx +1 -34
- package/src/recipients/recipient.test.ts +6 -5
- package/src/recipients/recipient.ts +7 -12
- package/src/recipients/verifier.ts +20 -0
- package/src/redux/migrations.test.ts +25 -0
- package/src/redux/migrations.ts +21 -0
- package/src/redux/store.test.ts +1 -2
- package/src/redux/store.ts +1 -1
- package/src/send/SelectRecipientAddress.tsx +11 -13
- package/src/send/SendConfirmation.test.tsx +3 -3
- package/src/send/SendConfirmation.tsx +3 -3
- package/src/send/SendSelectRecipient.test.tsx +37 -51
- package/src/send/useFetchRecipientVerificationStatus.ts +6 -11
- package/src/transactions/UserSection.tsx +37 -11
- package/src/transactions/feed/TransactionDetailsScreen.test.tsx +6 -6
- package/src/utils/phoneNumbers.ts +0 -11
package/package.json
CHANGED
|
@@ -71,7 +71,7 @@ import { NftOrigin } from 'src/nfts/types'
|
|
|
71
71
|
import { NotificationReceiveState } from 'src/notifications/types'
|
|
72
72
|
import { PointsActivityId } from 'src/points/types'
|
|
73
73
|
import { RecipientType } from 'src/recipients/recipient'
|
|
74
|
-
import { Verifier } from 'src/
|
|
74
|
+
import { Verifier } from 'src/recipients/verifier'
|
|
75
75
|
import { AmountEnteredIn, QrCode } from 'src/send/types'
|
|
76
76
|
import { Field, SwapType } from 'src/swap/types'
|
|
77
77
|
import { TokenActionName } from 'src/tokens/types'
|
|
@@ -76,18 +76,27 @@ describe('ReviewSummaryItem', () => {
|
|
|
76
76
|
})
|
|
77
77
|
|
|
78
78
|
describe('ReviewSummaryItemContact', () => {
|
|
79
|
-
|
|
79
|
+
const renderContact = (recipient: Recipient, storeOverrides: Record<string, unknown> = {}) =>
|
|
80
|
+
render(
|
|
81
|
+
<Provider store={createMockStore(storeOverrides)}>
|
|
82
|
+
<ReviewSummaryItemContact recipient={recipient} testID="ContactItem" />
|
|
83
|
+
</Provider>
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
it('shows name as primary value and resolved short address as subtitle for phone recipients', () => {
|
|
80
87
|
const recipient = {
|
|
81
88
|
name: 'John Doe',
|
|
82
89
|
displayNumber: '+111111111',
|
|
83
90
|
e164PhoneNumber: '+222222222',
|
|
91
|
+
address: '0x0123456789012345678901234567890123456789',
|
|
84
92
|
} as Recipient
|
|
85
|
-
const tree =
|
|
93
|
+
const tree = renderContact(recipient)
|
|
86
94
|
|
|
87
95
|
expect(tree.getByTestId('ContactItem/PrimaryValue')).toHaveTextContent('John Doe', {
|
|
88
96
|
exact: false,
|
|
89
97
|
})
|
|
90
|
-
|
|
98
|
+
// Phone recipients always surface the on-chain destination on the review screen.
|
|
99
|
+
expect(tree.getByTestId('ContactItem/SecondaryValue')).toHaveTextContent('0x0123...6789', {
|
|
91
100
|
exact: false,
|
|
92
101
|
})
|
|
93
102
|
})
|
|
@@ -109,14 +118,17 @@ describe('ReviewSummaryItemContact', () => {
|
|
|
109
118
|
])(
|
|
110
119
|
'displays only $phoneNumberType phone if name is not available',
|
|
111
120
|
({ displayNumber, e164PhoneNumber, expectedDisplayedValue }) => {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
121
|
+
const address = '0x0123456789012345678901234567890123456789'
|
|
122
|
+
const recipient = { displayNumber, e164PhoneNumber, address } as Recipient
|
|
123
|
+
const tree = renderContact(recipient)
|
|
114
124
|
|
|
115
125
|
expect(tree.getByTestId('ContactItem/PrimaryValue')).toHaveTextContent(
|
|
116
126
|
expectedDisplayedValue,
|
|
117
127
|
{ exact: false }
|
|
118
128
|
)
|
|
119
|
-
expect(tree.
|
|
129
|
+
expect(tree.getByTestId('ContactItem/SecondaryValue')).toHaveTextContent('0x0123...6789', {
|
|
130
|
+
exact: false,
|
|
131
|
+
})
|
|
120
132
|
}
|
|
121
133
|
)
|
|
122
134
|
|
|
@@ -124,16 +136,45 @@ describe('ReviewSummaryItemContact', () => {
|
|
|
124
136
|
const recipient = {
|
|
125
137
|
address: '0x123456789',
|
|
126
138
|
} as Recipient
|
|
127
|
-
const tree =
|
|
139
|
+
const tree = renderContact(recipient)
|
|
128
140
|
|
|
129
141
|
expect(tree.getByTestId('ContactItem/PrimaryValue')).toHaveTextContent('0x123456789', {
|
|
130
142
|
exact: false,
|
|
131
143
|
})
|
|
132
144
|
})
|
|
133
145
|
|
|
146
|
+
it('inlines the verifier with the short address for phone recipients', () => {
|
|
147
|
+
const address = '0x0123456789012345678901234567890123456789'
|
|
148
|
+
const recipient = {
|
|
149
|
+
name: 'John Doe',
|
|
150
|
+
displayNumber: '+111111111',
|
|
151
|
+
e164PhoneNumber: '+222222222',
|
|
152
|
+
address,
|
|
153
|
+
} as Recipient
|
|
154
|
+
const tree = renderContact(recipient, {
|
|
155
|
+
identity: { addressToVerifiedBy: { [address]: 'valora' } },
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const subtitle = tree.getByTestId('ContactItem/SecondaryValue')
|
|
159
|
+
expect(subtitle).toHaveTextContent('0x0123...6789', { exact: false })
|
|
160
|
+
expect(subtitle).toHaveTextContent('Valora', { exact: false })
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('inlines just the verifier for address-only recipients with a known verifier', () => {
|
|
164
|
+
const address = '0x0123456789012345678901234567890123456789'
|
|
165
|
+
const recipient = { address } as Recipient
|
|
166
|
+
const tree = renderContact(recipient, {
|
|
167
|
+
identity: { addressToVerifiedBy: { [address]: 'minipay' } },
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const subtitle = tree.getByTestId('ContactItem/SecondaryValue')
|
|
171
|
+
// No address in the subtitle — it's already in the primary slot for address-only recipients
|
|
172
|
+
expect(subtitle).toHaveTextContent('MiniPay', { exact: false })
|
|
173
|
+
})
|
|
174
|
+
|
|
134
175
|
it('logs an error if no name/phone/address exist', () => {
|
|
135
176
|
const recipient = {} as Recipient
|
|
136
|
-
const tree =
|
|
177
|
+
const tree = renderContact(recipient)
|
|
137
178
|
expect(Logger.error).toHaveBeenCalledTimes(1)
|
|
138
179
|
expect(tree.toJSON()).toBeNull()
|
|
139
180
|
})
|
|
@@ -4,6 +4,7 @@ import React, { useMemo, type ReactNode } from 'react'
|
|
|
4
4
|
import { Trans, useTranslation } from 'react-i18next'
|
|
5
5
|
import { ScrollView, StyleSheet, Text, View, type StyleProp, type TextStyle } from 'react-native'
|
|
6
6
|
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
7
|
+
import { formatShortenedAddress } from 'src/account/utils'
|
|
7
8
|
import BackButton from 'src/components/BackButton'
|
|
8
9
|
import ContactCircle from 'src/components/ContactCircle'
|
|
9
10
|
import CustomHeader from 'src/components/header/CustomHeader'
|
|
@@ -14,8 +15,10 @@ import InfoIcon from 'src/icons/InfoIcon'
|
|
|
14
15
|
import WalletIcon from 'src/icons/navigator/Wallet'
|
|
15
16
|
import PhoneIcon from 'src/icons/Phone'
|
|
16
17
|
import UserIcon from 'src/icons/User'
|
|
18
|
+
import VerifiedBadge from 'src/icons/VerifiedBadge'
|
|
17
19
|
import { LocalCurrencySymbol } from 'src/localCurrency/consts'
|
|
18
|
-
import {
|
|
20
|
+
import { type Recipient } from 'src/recipients/recipient'
|
|
21
|
+
import { useVerifierName } from 'src/recipients/verifier'
|
|
19
22
|
import colors, { type ColorValue } from 'src/styles/colors'
|
|
20
23
|
import { typeScale } from 'src/styles/fonts'
|
|
21
24
|
import { Spacing } from 'src/styles/styles'
|
|
@@ -70,7 +73,7 @@ export function ReviewSummaryItem(props: {
|
|
|
70
73
|
label: string
|
|
71
74
|
icon: ReactNode
|
|
72
75
|
primaryValue: string
|
|
73
|
-
secondaryValue?:
|
|
76
|
+
secondaryValue?: ReactNode
|
|
74
77
|
testID?: string
|
|
75
78
|
onPress?: () => void
|
|
76
79
|
}) {
|
|
@@ -96,12 +99,21 @@ export function ReviewSummaryItem(props: {
|
|
|
96
99
|
|
|
97
100
|
{!!props.secondaryValue && (
|
|
98
101
|
<View style={styles.reviewSummaryItemSecondaryValueWrapper}>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
{typeof props.secondaryValue === 'string' ? (
|
|
103
|
+
<Text
|
|
104
|
+
style={styles.reviewSummaryItemSecondaryValue}
|
|
105
|
+
testID={`${props.testID}/SecondaryValue`}
|
|
106
|
+
>
|
|
107
|
+
{props.secondaryValue}
|
|
108
|
+
</Text>
|
|
109
|
+
) : (
|
|
110
|
+
<View
|
|
111
|
+
style={styles.reviewSummaryItemSecondaryValueContent}
|
|
112
|
+
testID={`${props.testID}/SecondaryValue`}
|
|
113
|
+
>
|
|
114
|
+
{props.secondaryValue}
|
|
115
|
+
</View>
|
|
116
|
+
)}
|
|
105
117
|
{!!props.onPress && <InfoIcon size={14} color={colors.contentSecondary} />}
|
|
106
118
|
</View>
|
|
107
119
|
)}
|
|
@@ -112,6 +124,24 @@ export function ReviewSummaryItem(props: {
|
|
|
112
124
|
)
|
|
113
125
|
}
|
|
114
126
|
|
|
127
|
+
function renderAddressAndVerifier(
|
|
128
|
+
shortAddress: string | undefined,
|
|
129
|
+
verifierName: string | undefined
|
|
130
|
+
): ReactNode {
|
|
131
|
+
if (!shortAddress && !verifierName) return undefined
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
{!!shortAddress && <Text style={styles.reviewSummaryItemSecondaryValue}>{shortAddress}</Text>}
|
|
135
|
+
{!!verifierName && (
|
|
136
|
+
<>
|
|
137
|
+
<VerifiedBadge color={colors.contentSecondary} />
|
|
138
|
+
<Text style={styles.reviewSummaryItemSecondaryValue}>{verifierName}</Text>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
115
145
|
export function ReviewSummaryItemContact({
|
|
116
146
|
testID,
|
|
117
147
|
recipient,
|
|
@@ -120,20 +150,30 @@ export function ReviewSummaryItemContact({
|
|
|
120
150
|
recipient: Recipient
|
|
121
151
|
}) {
|
|
122
152
|
const { t } = useTranslation()
|
|
153
|
+
const verifierName = useVerifierName(recipient.address)
|
|
123
154
|
const contact = useMemo(() => {
|
|
124
155
|
const phone = recipient.displayNumber || recipient.e164PhoneNumber
|
|
156
|
+
// For recipients with a phone mapping, surface the resolved on-chain address (and verifier,
|
|
157
|
+
// if known) as a subtitle so the user can verify the actual destination they are signing.
|
|
158
|
+
const shortAddress = recipient.address ? formatShortenedAddress(recipient.address) : undefined
|
|
159
|
+
const phoneSubtitle = renderAddressAndVerifier(shortAddress, verifierName)
|
|
160
|
+
|
|
125
161
|
if (recipient.name) {
|
|
126
|
-
return { title: recipient.name, subtitle:
|
|
162
|
+
return { title: recipient.name, subtitle: phoneSubtitle, icon: UserIcon }
|
|
127
163
|
}
|
|
128
164
|
|
|
129
165
|
if (phone) {
|
|
130
|
-
return { title: phone, icon: PhoneIcon }
|
|
166
|
+
return { title: phone, subtitle: phoneSubtitle, icon: PhoneIcon }
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
if (recipient.address) {
|
|
134
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
title: recipient.address,
|
|
172
|
+
subtitle: renderAddressAndVerifier(undefined, verifierName),
|
|
173
|
+
icon: WalletIcon,
|
|
174
|
+
}
|
|
135
175
|
}
|
|
136
|
-
}, [recipient])
|
|
176
|
+
}, [recipient, verifierName])
|
|
137
177
|
|
|
138
178
|
// This should never happen
|
|
139
179
|
if (!contact) {
|
|
@@ -478,6 +518,12 @@ const styles = StyleSheet.create({
|
|
|
478
518
|
gap: Spacing.Smallest8,
|
|
479
519
|
alignItems: 'center',
|
|
480
520
|
},
|
|
521
|
+
reviewSummaryItemSecondaryValueContent: {
|
|
522
|
+
flexDirection: 'row',
|
|
523
|
+
gap: Spacing.Tiny4,
|
|
524
|
+
alignItems: 'center',
|
|
525
|
+
flexShrink: 1,
|
|
526
|
+
},
|
|
481
527
|
reviewDetails: {
|
|
482
528
|
gap: Spacing.Regular16,
|
|
483
529
|
width: '100%',
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import Svg, { Path } from 'react-native-svg'
|
|
3
|
+
import colors, { ColorValue } from 'src/styles/colors'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
size?: number
|
|
7
|
+
color?: ColorValue
|
|
8
|
+
testID?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function VerifiedBadge({ size = 14, color = colors.contentSecondary, testID }: Props) {
|
|
12
|
+
return (
|
|
13
|
+
<Svg width={size} height={size} viewBox="0 0 22 22" fill="none" testID={testID}>
|
|
14
|
+
<Path
|
|
15
|
+
d="M20.396 11c-.018-.646-.215-1.275-.57-1.816-.354-.54-.852-.972-1.438-1.246.223-.607.27-1.264.14-1.897-.131-.634-.437-1.218-.882-1.687-.47-.445-1.053-.75-1.687-.882-.633-.13-1.29-.083-1.897.14-.273-.587-.704-1.086-1.245-1.44S11.647 1.62 11 1.604c-.646.017-1.273.213-1.813.568s-.969.854-1.24 1.44c-.608-.223-1.267-.272-1.902-.14-.635.13-1.22.436-1.69.882-.445.47-.749 1.055-.878 1.688-.13.633-.08 1.29.144 1.896-.587.274-1.087.705-1.443 1.245-.356.54-.555 1.17-.574 1.817.02.647.218 1.276.574 1.817.356.54.856.972 1.443 1.245-.224.606-.274 1.263-.144 1.896.13.634.433 1.218.877 1.688.47.443 1.054.747 1.687.878.633.132 1.29.084 1.897-.138.274.586.705 1.084 1.246 1.439.54.354 1.17.551 1.816.569.647-.016 1.276-.213 1.817-.567s.972-.854 1.245-1.44c.604.239 1.266.296 1.903.164.636-.132 1.22-.447 1.68-.907.46-.46.776-1.044.908-1.681s.075-1.299-.165-1.903c.586-.274 1.084-.705 1.439-1.246.354-.54.551-1.17.569-1.816Z"
|
|
16
|
+
stroke={color}
|
|
17
|
+
strokeWidth="1.2"
|
|
18
|
+
/>
|
|
19
|
+
<Path
|
|
20
|
+
d="M9.662 14.85 6.233 11.42l1.293-1.302 2.072 2.072 4.4-4.794 1.347 1.246-5.683 6.208Z"
|
|
21
|
+
fill={color}
|
|
22
|
+
/>
|
|
23
|
+
</Svg>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default React.memo(VerifiedBadge)
|
package/src/identity/actions.ts
CHANGED
|
@@ -15,7 +15,6 @@ export enum Actions {
|
|
|
15
15
|
CANCEL_IMPORT_CONTACTS = 'IDENTITY/CANCEL_IMPORT_CONTACTS',
|
|
16
16
|
END_IMPORT_CONTACTS = 'IDENTITY/END_IMPORT_CONTACTS',
|
|
17
17
|
FETCH_ADDRESS_VERIFICATION_STATUS = 'IDENTITY/FETCH_ADDRESS_VERIFICATION_STATUS',
|
|
18
|
-
ADDRESS_VERIFICATION_STATUS_RECEIVED = 'IDENTITY/ADDRESS_VERIFICATION_STATUS_RECEIVED',
|
|
19
18
|
CONTACTS_SAVED = 'IDENTITY/CONTACTS_SAVED',
|
|
20
19
|
STORED_PASSWORD_REFRESHED = 'IDENTITY/STORED_PASSWORD_REFRESHED',
|
|
21
20
|
}
|
|
@@ -58,12 +57,6 @@ export interface FetchAddressVerificationAction {
|
|
|
58
57
|
address: string
|
|
59
58
|
}
|
|
60
59
|
|
|
61
|
-
export interface AddressVerificationStatusReceivedAction {
|
|
62
|
-
type: Actions.ADDRESS_VERIFICATION_STATUS_RECEIVED
|
|
63
|
-
address: string
|
|
64
|
-
addressVerified: boolean
|
|
65
|
-
}
|
|
66
|
-
|
|
67
60
|
interface ContactsSavedAction {
|
|
68
61
|
type: Actions.CONTACTS_SAVED
|
|
69
62
|
hash: string
|
|
@@ -81,7 +74,6 @@ export type ActionTypes =
|
|
|
81
74
|
| EndImportContactsAction
|
|
82
75
|
| FetchAddressesAndValidateAction
|
|
83
76
|
| FetchAddressVerificationAction
|
|
84
|
-
| AddressVerificationStatusReceivedAction
|
|
85
77
|
| ContactsSavedAction
|
|
86
78
|
| StoredPasswordRefreshedAction
|
|
87
79
|
|
|
@@ -90,15 +82,6 @@ export const fetchAddressesAndValidate = (e164Number: string): FetchAddressesAnd
|
|
|
90
82
|
e164Number,
|
|
91
83
|
})
|
|
92
84
|
|
|
93
|
-
export const addressVerificationStatusReceived = (
|
|
94
|
-
address: string,
|
|
95
|
-
addressVerified: boolean
|
|
96
|
-
): AddressVerificationStatusReceivedAction => ({
|
|
97
|
-
type: Actions.ADDRESS_VERIFICATION_STATUS_RECEIVED,
|
|
98
|
-
address,
|
|
99
|
-
addressVerified,
|
|
100
|
-
})
|
|
101
|
-
|
|
102
85
|
export const fetchAddressVerification = (address: string): FetchAddressVerificationAction => ({
|
|
103
86
|
type: Actions.FETCH_ADDRESS_VERIFICATION_STATUS,
|
|
104
87
|
address,
|
|
@@ -6,13 +6,12 @@ import { call, select } from 'redux-saga/effects'
|
|
|
6
6
|
import { setUserContactDetails } from 'src/account/actions'
|
|
7
7
|
import { defaultCountryCodeSelector, e164NumberSelector } from 'src/account/selectors'
|
|
8
8
|
import { showError, showErrorOrFallback } from 'src/alert/actions'
|
|
9
|
-
import { IdentityEvents } from 'src/analytics/Events'
|
|
10
9
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
10
|
+
import { IdentityEvents } from 'src/analytics/Events'
|
|
11
11
|
import { ErrorMessages } from 'src/app/ErrorMessages'
|
|
12
12
|
import { phoneNumberVerifiedSelector } from 'src/app/selectors'
|
|
13
13
|
import {
|
|
14
14
|
Actions,
|
|
15
|
-
addressVerificationStatusReceived,
|
|
16
15
|
contactsSaved,
|
|
17
16
|
fetchAddressVerification,
|
|
18
17
|
fetchAddressesAndValidate,
|
|
@@ -25,7 +24,9 @@ import {
|
|
|
25
24
|
saveContacts,
|
|
26
25
|
} from 'src/identity/contactMapping'
|
|
27
26
|
import {
|
|
28
|
-
|
|
27
|
+
addressToE164NumberSelector,
|
|
28
|
+
addressToVerifiedBySelector,
|
|
29
|
+
e164NumberToAddressSelector,
|
|
29
30
|
lastSavedContactsHashSelector,
|
|
30
31
|
} from 'src/identity/selectors'
|
|
31
32
|
import { retrieveSignedMessage } from 'src/pincode/authentication'
|
|
@@ -93,12 +94,19 @@ describe('Fetch Addresses Saga', () => {
|
|
|
93
94
|
mockFetch.resetMocks()
|
|
94
95
|
})
|
|
95
96
|
|
|
97
|
+
const emptyMappingProviders: [any, any][] = [
|
|
98
|
+
[select(e164NumberToAddressSelector), {}],
|
|
99
|
+
[select(addressToE164NumberSelector), {}],
|
|
100
|
+
[select(addressToVerifiedBySelector), {}],
|
|
101
|
+
]
|
|
102
|
+
|
|
96
103
|
it('fetches and caches addresses correctly', async () => {
|
|
97
104
|
const updatedAccount = '0xAbC'
|
|
98
105
|
mockFetch.mockResponseOnce(JSON.stringify({ data: { addresses: [updatedAccount] } }))
|
|
99
106
|
|
|
100
107
|
await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
|
|
101
108
|
.provide([
|
|
109
|
+
...emptyMappingProviders,
|
|
102
110
|
[select(walletAddressSelector), '0xxyz'],
|
|
103
111
|
[call(retrieveSignedMessage), 'some signed message'],
|
|
104
112
|
])
|
|
@@ -131,6 +139,7 @@ describe('Fetch Addresses Saga', () => {
|
|
|
131
139
|
|
|
132
140
|
await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
|
|
133
141
|
.provide([
|
|
142
|
+
...emptyMappingProviders,
|
|
134
143
|
[select(walletAddressSelector), '0xxyz'],
|
|
135
144
|
[call(retrieveSignedMessage), 'some signed message'],
|
|
136
145
|
])
|
|
@@ -162,6 +171,7 @@ describe('Fetch Addresses Saga', () => {
|
|
|
162
171
|
|
|
163
172
|
await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
|
|
164
173
|
.provide([
|
|
174
|
+
...emptyMappingProviders,
|
|
165
175
|
[select(walletAddressSelector), '0xxyz'],
|
|
166
176
|
[call(retrieveSignedMessage), 'some signed message'],
|
|
167
177
|
])
|
|
@@ -176,11 +186,44 @@ describe('Fetch Addresses Saga', () => {
|
|
|
176
186
|
.run()
|
|
177
187
|
})
|
|
178
188
|
|
|
189
|
+
it('prunes stale address mappings no longer returned by the fresh lookup', async () => {
|
|
190
|
+
mockFetch.mockResponseOnce(
|
|
191
|
+
JSON.stringify({
|
|
192
|
+
data: {
|
|
193
|
+
addresses: ['0xkept'],
|
|
194
|
+
verifiedAddresses: [{ address: '0xkept', verifiedBy: 'valora' }],
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
|
|
200
|
+
.provide([
|
|
201
|
+
[select(e164NumberToAddressSelector), { [mockE164Number]: ['0xstale', '0xkept'] }],
|
|
202
|
+
[
|
|
203
|
+
select(addressToE164NumberSelector),
|
|
204
|
+
{ '0xstale': mockE164Number, '0xkept': mockE164Number },
|
|
205
|
+
],
|
|
206
|
+
[select(addressToVerifiedBySelector), { '0xstale': 'valora', '0xkept': 'valora' }],
|
|
207
|
+
[select(walletAddressSelector), '0xxyz'],
|
|
208
|
+
[call(retrieveSignedMessage), 'some signed message'],
|
|
209
|
+
])
|
|
210
|
+
.put(updateE164PhoneNumberAddresses({ [mockE164Number]: undefined }, {}))
|
|
211
|
+
.put(
|
|
212
|
+
updateE164PhoneNumberAddresses(
|
|
213
|
+
{ [mockE164Number]: ['0xkept'] },
|
|
214
|
+
{ '0xstale': null, '0xkept': mockE164Number },
|
|
215
|
+
{ '0xstale': null, '0xkept': 'valora' }
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
.run()
|
|
219
|
+
})
|
|
220
|
+
|
|
179
221
|
it('handles lookup errors correctly', async () => {
|
|
180
222
|
mockFetch.mockReject()
|
|
181
223
|
|
|
182
224
|
await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
|
|
183
225
|
.provide([
|
|
226
|
+
...emptyMappingProviders,
|
|
184
227
|
[select(walletAddressSelector), '0xxyz'],
|
|
185
228
|
[call(retrieveSignedMessage), 'some signed message'],
|
|
186
229
|
])
|
|
@@ -195,48 +238,64 @@ describe('Fetch Address Verification Saga', () => {
|
|
|
195
238
|
mockFetch.resetMocks()
|
|
196
239
|
})
|
|
197
240
|
|
|
198
|
-
it('
|
|
199
|
-
mockFetch.mockResponseOnce(
|
|
241
|
+
it('records the `verifiedBy` value returned by the backend', async () => {
|
|
242
|
+
mockFetch.mockResponseOnce(
|
|
243
|
+
JSON.stringify({ data: { addressVerified: true, verifiedBy: 'minipay' } })
|
|
244
|
+
)
|
|
200
245
|
|
|
201
246
|
await expectSaga(fetchAddressVerificationSaga, fetchAddressVerification(mockAccount))
|
|
202
247
|
.provide([
|
|
203
|
-
[select(addressToVerificationStatusSelector), {}],
|
|
204
248
|
[select(walletAddressSelector), '0xxyz'],
|
|
205
249
|
[call(retrieveSignedMessage), 'some signed message'],
|
|
206
250
|
])
|
|
207
|
-
.put(
|
|
251
|
+
.put(updateE164PhoneNumberAddresses({}, {}, { [mockAccount.toLowerCase()]: 'minipay' }))
|
|
208
252
|
.run()
|
|
209
253
|
|
|
210
254
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
211
255
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
212
|
-
`${networkConfig.checkAddressVerifiedUrl}?address=${mockAccount}&clientPlatform=android&clientVersion=0.0.1`,
|
|
213
|
-
{
|
|
256
|
+
`${networkConfig.checkAddressVerifiedUrl}?address=${mockAccount.toLowerCase()}&clientPlatform=android&clientVersion=0.0.1`,
|
|
257
|
+
expect.objectContaining({
|
|
214
258
|
method: 'GET',
|
|
215
|
-
headers: {
|
|
216
|
-
'Content-Type': 'application/json',
|
|
259
|
+
headers: expect.objectContaining({
|
|
217
260
|
authorization: `${networkConfig.authHeaderIssuer} 0xxyz:some signed message`,
|
|
218
|
-
},
|
|
219
|
-
|
|
220
|
-
}
|
|
261
|
+
}),
|
|
262
|
+
})
|
|
221
263
|
)
|
|
222
264
|
})
|
|
223
265
|
|
|
224
|
-
it('
|
|
266
|
+
it('falls back to `valora` when the backend confirms the address without a `verifiedBy` field', async () => {
|
|
267
|
+
mockFetch.mockResponseOnce(JSON.stringify({ data: { addressVerified: true } }))
|
|
268
|
+
|
|
225
269
|
await expectSaga(fetchAddressVerificationSaga, fetchAddressVerification(mockAccount))
|
|
226
|
-
.provide([
|
|
270
|
+
.provide([
|
|
271
|
+
[select(walletAddressSelector), '0xxyz'],
|
|
272
|
+
[call(retrieveSignedMessage), 'some signed message'],
|
|
273
|
+
])
|
|
274
|
+
.put(updateE164PhoneNumberAddresses({}, {}, { [mockAccount.toLowerCase()]: 'valora' }))
|
|
227
275
|
.run()
|
|
276
|
+
})
|
|
228
277
|
|
|
229
|
-
|
|
278
|
+
it('records `null` (checked, not verified) when the backend returns false', async () => {
|
|
279
|
+
mockFetch.mockResponseOnce(JSON.stringify({ data: { addressVerified: false } }))
|
|
280
|
+
|
|
281
|
+
await expectSaga(fetchAddressVerificationSaga, fetchAddressVerification(mockAccount))
|
|
282
|
+
.provide([
|
|
283
|
+
[select(walletAddressSelector), '0xxyz'],
|
|
284
|
+
[call(retrieveSignedMessage), 'some signed message'],
|
|
285
|
+
])
|
|
286
|
+
.put(updateE164PhoneNumberAddresses({}, {}, { [mockAccount.toLowerCase()]: null }))
|
|
287
|
+
.run()
|
|
230
288
|
})
|
|
231
289
|
|
|
232
|
-
it('
|
|
290
|
+
it('does not touch `addressToVerifiedBy` on network errors — the check is inconclusive, not negative', async () => {
|
|
233
291
|
mockFetch.mockReject()
|
|
234
292
|
await expectSaga(fetchAddressVerificationSaga, fetchAddressVerification(mockAccount))
|
|
235
293
|
.provide([
|
|
236
|
-
[select(addressToVerificationStatusSelector), {}],
|
|
237
294
|
[select(walletAddressSelector), '0xxyz'],
|
|
238
295
|
[call(retrieveSignedMessage), 'some signed message'],
|
|
239
296
|
])
|
|
297
|
+
.not.put.actionType(Actions.UPDATE_E164_PHONE_NUMBER_ADDRESSES)
|
|
298
|
+
.put(showErrorOrFallback(expect.anything(), ErrorMessages.ADDRESS_LOOKUP_FAILURE))
|
|
240
299
|
.run()
|
|
241
300
|
expect(AppAnalytics.track).toHaveBeenCalledWith(IdentityEvents.address_lookup_error, {
|
|
242
301
|
error: 'Unable to fetch verification status for this address',
|
|
@@ -11,15 +11,20 @@ import {
|
|
|
11
11
|
Actions,
|
|
12
12
|
FetchAddressVerificationAction,
|
|
13
13
|
FetchAddressesAndValidateAction,
|
|
14
|
-
addressVerificationStatusReceived,
|
|
15
14
|
contactsSaved,
|
|
16
15
|
endImportContacts,
|
|
17
16
|
updateE164PhoneNumberAddresses,
|
|
18
17
|
updateImportContactsProgress,
|
|
19
18
|
} from 'src/identity/actions'
|
|
20
|
-
import { AddressToE164NumberType, E164NumberToAddressType } from 'src/identity/reducer'
|
|
21
19
|
import {
|
|
22
|
-
|
|
20
|
+
AddressToE164NumberType,
|
|
21
|
+
AddressToVerifiedByType,
|
|
22
|
+
E164NumberToAddressType,
|
|
23
|
+
} from 'src/identity/reducer'
|
|
24
|
+
import {
|
|
25
|
+
addressToE164NumberSelector,
|
|
26
|
+
addressToVerifiedBySelector,
|
|
27
|
+
e164NumberToAddressSelector,
|
|
23
28
|
lastSavedContactsHashSelector,
|
|
24
29
|
} from 'src/identity/selectors'
|
|
25
30
|
import { ImportContactsStatus } from 'src/identity/types'
|
|
@@ -139,6 +144,13 @@ export function* fetchAddressesAndValidateSaga({ e164Number }: FetchAddressesAnd
|
|
|
139
144
|
try {
|
|
140
145
|
Logger.debug(TAG + '@fetchAddressesAndValidate', `Fetching addresses for number`)
|
|
141
146
|
|
|
147
|
+
// Snapshot the previous mappings before we clear them so we can prune stale entries
|
|
148
|
+
// after the fresh response arrives (see the pruning block below).
|
|
149
|
+
const prevE164NumberToAddress = yield* select(e164NumberToAddressSelector)
|
|
150
|
+
const prevAddressToE164Number = yield* select(addressToE164NumberSelector)
|
|
151
|
+
const prevAddressToVerifiedBy = yield* select(addressToVerifiedBySelector)
|
|
152
|
+
const prevAddresses = prevE164NumberToAddress[e164Number] ?? []
|
|
153
|
+
|
|
142
154
|
// Clear existing entries for those numbers so our mapping consumers know new status is pending.
|
|
143
155
|
yield* put(updateE164PhoneNumberAddresses({ [e164Number]: undefined }, {}))
|
|
144
156
|
|
|
@@ -148,10 +160,26 @@ export function* fetchAddressesAndValidateSaga({ e164Number }: FetchAddressesAnd
|
|
|
148
160
|
// it includes addresses verified by both CPV and SocialConnect.
|
|
149
161
|
// The `addresses` field is used for backward compatibility.
|
|
150
162
|
const walletAddresses = verifiedAddresses ? verifiedAddresses.map((v) => v.address) : addresses
|
|
163
|
+
const walletAddressSet = new Set(walletAddresses)
|
|
151
164
|
|
|
152
165
|
const e164NumberToAddressUpdates: E164NumberToAddressType = {}
|
|
153
166
|
const addressToE164NumberUpdates: AddressToE164NumberType = {}
|
|
154
|
-
const addressToVerifiedByUpdates:
|
|
167
|
+
const addressToVerifiedByUpdates: AddressToVerifiedByType = {}
|
|
168
|
+
|
|
169
|
+
// Prune addresses previously associated with this phone number but no longer present
|
|
170
|
+
// in the fresh response.
|
|
171
|
+
for (const prevAddress of prevAddresses) {
|
|
172
|
+
if (!walletAddressSet.has(prevAddress)) {
|
|
173
|
+
// Clear the reverse mapping for this number.
|
|
174
|
+
if (prevAddressToE164Number[prevAddress] === e164Number) {
|
|
175
|
+
addressToE164NumberUpdates[prevAddress] = null
|
|
176
|
+
}
|
|
177
|
+
// Clear verifier info.
|
|
178
|
+
if (prevAddress in prevAddressToVerifiedBy) {
|
|
179
|
+
addressToVerifiedByUpdates[prevAddress] = null
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
155
183
|
|
|
156
184
|
if (verifiedAddresses) {
|
|
157
185
|
for (const { address, verifiedBy } of verifiedAddresses) {
|
|
@@ -188,14 +216,20 @@ export function* fetchAddressesAndValidateSaga({ e164Number }: FetchAddressesAnd
|
|
|
188
216
|
}
|
|
189
217
|
|
|
190
218
|
export function* fetchAddressVerificationSaga({ address }: FetchAddressVerificationAction) {
|
|
219
|
+
const normalizedAddress = address.toLowerCase()
|
|
191
220
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
221
|
+
AppAnalytics.track(IdentityEvents.address_lookup_start)
|
|
222
|
+
const { addressVerified, verifiedBy } = yield* call(fetchAddressVerification, normalizedAddress)
|
|
223
|
+
// Older backend responses omit `verifiedBy` and only signal Valora-verified addresses,
|
|
224
|
+
// so fall back to 'valora' when verification is confirmed without a verifier.
|
|
225
|
+
yield* put(
|
|
226
|
+
updateE164PhoneNumberAddresses(
|
|
227
|
+
{},
|
|
228
|
+
{},
|
|
229
|
+
{ [normalizedAddress]: addressVerified ? (verifiedBy ?? 'valora') : null }
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
AppAnalytics.track(IdentityEvents.address_lookup_complete)
|
|
199
233
|
} catch (err) {
|
|
200
234
|
const error = ensureError(err)
|
|
201
235
|
Logger.debug(
|
|
@@ -203,13 +237,8 @@ export function* fetchAddressVerificationSaga({ address }: FetchAddressVerificat
|
|
|
203
237
|
`Error fetching address verification`,
|
|
204
238
|
error
|
|
205
239
|
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
})
|
|
209
|
-
// Setting this address to "false" does not mean that the address
|
|
210
|
-
// if definitely unverified; we set it to false to indicate that
|
|
211
|
-
// the request is finished, and possibly unverified.
|
|
212
|
-
yield* put(addressVerificationStatusReceived(address, false))
|
|
240
|
+
yield* put(showErrorOrFallback(error, ErrorMessages.ADDRESS_LOOKUP_FAILURE))
|
|
241
|
+
AppAnalytics.track(IdentityEvents.address_lookup_error, { error: error.message })
|
|
213
242
|
}
|
|
214
243
|
}
|
|
215
244
|
|
|
@@ -291,8 +320,9 @@ function* fetchAddressVerification(address: string) {
|
|
|
291
320
|
)
|
|
292
321
|
}
|
|
293
322
|
|
|
294
|
-
const { data }: { data: { addressVerified: boolean } } =
|
|
295
|
-
|
|
323
|
+
const { data }: { data: { addressVerified: boolean; verifiedBy?: string | null } } =
|
|
324
|
+
yield* call([response, 'json'])
|
|
325
|
+
return { addressVerified: data.addressVerified, verifiedBy: data.verifiedBy }
|
|
296
326
|
} catch (error) {
|
|
297
327
|
Logger.warn(`${TAG}/fetchAddressVerification`, 'Unable to look up address', error)
|
|
298
328
|
throw new Error('Unable to fetch verification status for this address')
|