wallet-stack 1.0.0-alpha.128 → 1.0.0-alpha.130
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 +2 -2
- package/src/analytics/Properties.tsx +1 -0
- package/src/fiatExchanges/ExternalExchanges.tsx +3 -2
- package/src/fiatExchanges/Spend.tsx +3 -2
- package/src/fiatconnect/LinkAccountScreen.tsx +3 -2
- package/src/fiatconnect/ReviewScreen.tsx +3 -2
- package/src/fiatconnect/TransferStatusScreen.tsx +4 -3
- package/src/identity/actions.ts +5 -1
- package/src/identity/contactMapping.test.ts +41 -2
- package/src/identity/contactMapping.ts +34 -5
- package/src/identity/reducer.ts +11 -0
- package/src/identity/selectors.ts +1 -0
- package/src/keylessBackup/KeylessBackupIntro.tsx +3 -2
- package/src/navigator/types.tsx +1 -0
- package/src/nfts/NftsLoadError.tsx +3 -2
- package/src/recipients/RecipientItemV2.tsx +5 -13
- package/src/redux/migrations.test.ts +9 -0
- package/src/redux/migrations.ts +7 -0
- package/src/redux/store.test.ts +2 -1
- package/src/redux/store.ts +1 -1
- package/src/send/EnterAmount.tsx +4 -0
- package/src/send/SendEnterAmount.test.tsx +108 -1
- package/src/send/SendEnterAmount.tsx +18 -9
- package/src/send/SendSelectRecipient.test.tsx +53 -0
- package/src/send/SendSelectRecipient.tsx +3 -0
- package/src/send/ValidateRecipientAccount.test.tsx +1 -0
- package/src/send/ValidateRecipientAccount.tsx +5 -0
- package/src/send/useSendFilterChips.ts +46 -0
- package/src/statsig/constants.ts +6 -0
- package/src/statsig/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wallet-stack",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.130",
|
|
4
4
|
"author": "Valora Inc",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"provenance": true
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=20"
|
|
21
|
+
"node": ">=20.20.2"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"tsconfig.json",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { useTranslation } from 'react-i18next'
|
|
4
|
-
import {
|
|
4
|
+
import { ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
5
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
5
6
|
import { FiatExchangeEvents } from 'src/analytics/Events'
|
|
6
7
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
7
8
|
import BackButton from 'src/components/BackButton'
|
|
@@ -55,7 +56,7 @@ function ExternalExchanges({ route }: Props) {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
return (
|
|
58
|
-
<SafeAreaView style={styles.container}>
|
|
59
|
+
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
59
60
|
<Text style={styles.pleaseSelectExchange}>
|
|
60
61
|
{t('youCanTransferOut', {
|
|
61
62
|
digitalAsset: tokenInfo?.symbol,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { useTranslation } from 'react-i18next'
|
|
4
|
-
import {
|
|
4
|
+
import { ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
5
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
5
6
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
6
7
|
import { FiatExchangeEvents } from 'src/analytics/Events'
|
|
7
8
|
import BackButton from 'src/components/BackButton'
|
|
@@ -54,7 +55,7 @@ function Spend(props: Props) {
|
|
|
54
55
|
|
|
55
56
|
return (
|
|
56
57
|
<ScrollView style={styles.container}>
|
|
57
|
-
<SafeAreaView>
|
|
58
|
+
<SafeAreaView edges={['bottom']}>
|
|
58
59
|
<Text style={styles.pleaseSelectProvider}>{t('useBalanceWithMerchants')}</Text>
|
|
59
60
|
<View>
|
|
60
61
|
{merchants
|
|
@@ -3,7 +3,8 @@ import { RouteProp } from '@react-navigation/native'
|
|
|
3
3
|
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
4
4
|
import React from 'react'
|
|
5
5
|
import { Trans, useTranslation } from 'react-i18next'
|
|
6
|
-
import {
|
|
6
|
+
import { StyleSheet, Text } from 'react-native'
|
|
7
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
7
8
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
8
9
|
import { FiatExchangeEvents } from 'src/analytics/Events'
|
|
9
10
|
import BackButton from 'src/components/BackButton'
|
|
@@ -105,7 +106,7 @@ function LinkAccountSection(props: {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
return (
|
|
108
|
-
<SafeAreaView style={styles.content}>
|
|
109
|
+
<SafeAreaView style={styles.content} edges={['bottom']}>
|
|
109
110
|
<Text style={styles.title}>{t(bodyTitle)}</Text>
|
|
110
111
|
<Text testID="descriptionText" style={styles.description}>
|
|
111
112
|
<Trans i18nKey={description} values={{ providerName: quote.getProviderName() }}>
|
|
@@ -5,7 +5,8 @@ import BigNumber from 'bignumber.js'
|
|
|
5
5
|
import React, { useEffect, useLayoutEffect, useMemo, useState } from 'react'
|
|
6
6
|
import { useAsync } from 'react-async-hook'
|
|
7
7
|
import { useTranslation } from 'react-i18next'
|
|
8
|
-
import { ActivityIndicator, BackHandler,
|
|
8
|
+
import { ActivityIndicator, BackHandler, StyleSheet, Text, View } from 'react-native'
|
|
9
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
9
10
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
10
11
|
import { FiatExchangeEvents } from 'src/analytics/Events'
|
|
11
12
|
import BackButton from 'src/components/BackButton'
|
|
@@ -251,7 +252,7 @@ export default function FiatConnectReviewScreen({ route, navigation }: Props) {
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
return (
|
|
254
|
-
<SafeAreaView style={styles.content}>
|
|
255
|
+
<SafeAreaView style={styles.content} edges={['bottom']}>
|
|
255
256
|
<Dialog
|
|
256
257
|
testID="expiredQuoteDialog"
|
|
257
258
|
isVisible={showingExpiredQuoteDialog}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
2
2
|
import React, { useEffect, useState, type JSX } from 'react'
|
|
3
3
|
import { useTranslation } from 'react-i18next'
|
|
4
|
-
import { ActivityIndicator,
|
|
4
|
+
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native'
|
|
5
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
5
6
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
6
7
|
import { FiatExchangeEvents } from 'src/analytics/Events'
|
|
7
8
|
import BackButton from 'src/components/BackButton'
|
|
@@ -269,7 +270,7 @@ export default function FiatConnectTransferStatusScreen({ route, navigation }: P
|
|
|
269
270
|
),
|
|
270
271
|
})
|
|
271
272
|
return (
|
|
272
|
-
<SafeAreaView style={styles.content}>
|
|
273
|
+
<SafeAreaView style={styles.content} edges={['bottom']}>
|
|
273
274
|
<FailureSection flow={flow} normalizedQuote={normalizedQuote} fiatAccount={fiatAccount} />
|
|
274
275
|
</SafeAreaView>
|
|
275
276
|
)
|
|
@@ -282,7 +283,7 @@ export default function FiatConnectTransferStatusScreen({ route, navigation }: P
|
|
|
282
283
|
// intentionally falls thru since TxProcessing and Completed use the same component
|
|
283
284
|
case SendingTransferStatus.TxProcessing:
|
|
284
285
|
return (
|
|
285
|
-
<SafeAreaView style={styles.content}>
|
|
286
|
+
<SafeAreaView style={styles.content} edges={['bottom']}>
|
|
286
287
|
<SuccessOrProcessingSection
|
|
287
288
|
status={fiatConnectTransfer.status}
|
|
288
289
|
flow={flow}
|
package/src/identity/actions.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AddressToDisplayNameType,
|
|
3
3
|
AddressToE164NumberType,
|
|
4
|
+
AddressToVerifiedByType,
|
|
4
5
|
AddressValidationType,
|
|
5
6
|
E164NumberToAddressType,
|
|
6
7
|
} from 'src/identity/reducer'
|
|
@@ -31,6 +32,7 @@ export interface UpdateE164PhoneNumberAddressesAction {
|
|
|
31
32
|
type: Actions.UPDATE_E164_PHONE_NUMBER_ADDRESSES
|
|
32
33
|
e164NumberToAddress: E164NumberToAddressType
|
|
33
34
|
addressToE164Number: AddressToE164NumberType
|
|
35
|
+
addressToVerifiedBy: AddressToVerifiedByType
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface UpdateKnownAddressesAction {
|
|
@@ -162,11 +164,13 @@ export const endFetchingAddresses = (
|
|
|
162
164
|
|
|
163
165
|
export const updateE164PhoneNumberAddresses = (
|
|
164
166
|
e164NumberToAddress: E164NumberToAddressType,
|
|
165
|
-
addressToE164Number: AddressToE164NumberType
|
|
167
|
+
addressToE164Number: AddressToE164NumberType,
|
|
168
|
+
addressToVerifiedBy: AddressToVerifiedByType = {}
|
|
166
169
|
): UpdateE164PhoneNumberAddressesAction => ({
|
|
167
170
|
type: Actions.UPDATE_E164_PHONE_NUMBER_ADDRESSES,
|
|
168
171
|
e164NumberToAddress,
|
|
169
172
|
addressToE164Number,
|
|
173
|
+
addressToVerifiedBy,
|
|
170
174
|
})
|
|
171
175
|
|
|
172
176
|
export const updateKnownAddresses = (
|
|
@@ -115,7 +115,8 @@ describe('Fetch Addresses Saga', () => {
|
|
|
115
115
|
.put(
|
|
116
116
|
updateE164PhoneNumberAddresses(
|
|
117
117
|
{ [mockE164Number]: ['0xabc'] },
|
|
118
|
-
{ '0xabc': mockE164Number }
|
|
118
|
+
{ '0xabc': mockE164Number },
|
|
119
|
+
{}
|
|
119
120
|
)
|
|
120
121
|
)
|
|
121
122
|
.run()
|
|
@@ -151,7 +152,45 @@ describe('Fetch Addresses Saga', () => {
|
|
|
151
152
|
.put(
|
|
152
153
|
updateE164PhoneNumberAddresses(
|
|
153
154
|
{ [mockE164Number]: ['0xabc', '0xdef'] },
|
|
154
|
-
{ '0xabc': mockE164Number, '0xdef': mockE164Number }
|
|
155
|
+
{ '0xabc': mockE164Number, '0xdef': mockE164Number },
|
|
156
|
+
{}
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
.put(requireSecureSend(mockE164Number, AddressValidationType.PARTIAL))
|
|
160
|
+
.run()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('uses verifiedAddresses as source of truth when present', async () => {
|
|
164
|
+
const mockE164NumberToAddress = {
|
|
165
|
+
[mockE164Number]: [mockAccount.toLowerCase()],
|
|
166
|
+
}
|
|
167
|
+
// addresses only contains DB-verified addresses (backward compat),
|
|
168
|
+
// verifiedAddresses contains all (DB + SC) and is the source of truth
|
|
169
|
+
mockFetch.mockResponseOnce(
|
|
170
|
+
JSON.stringify({
|
|
171
|
+
data: {
|
|
172
|
+
addresses: ['0xAbC'],
|
|
173
|
+
verifiedAddresses: [
|
|
174
|
+
{ address: '0xAbC', verifiedBy: 'valora' },
|
|
175
|
+
{ address: '0xDef', verifiedBy: 'minipay' },
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
await expectSaga(fetchAddressesAndValidateSaga, fetchAddressesAndValidate(mockE164Number))
|
|
182
|
+
.provide([
|
|
183
|
+
[select(e164NumberToAddressSelector), mockE164NumberToAddress],
|
|
184
|
+
[select(walletAddressSelector), mockAccount],
|
|
185
|
+
[call(retrieveSignedMessage), 'some signed message'],
|
|
186
|
+
[select(secureSendPhoneNumberMappingSelector), {}],
|
|
187
|
+
])
|
|
188
|
+
.put(updateE164PhoneNumberAddresses({ [mockE164Number]: undefined }, {}))
|
|
189
|
+
.put(
|
|
190
|
+
updateE164PhoneNumberAddresses(
|
|
191
|
+
{ [mockE164Number]: ['0xabc', '0xdef'] },
|
|
192
|
+
{ '0xabc': mockE164Number, '0xdef': mockE164Number },
|
|
193
|
+
{ '0xabc': 'valora', '0xdef': 'minipay' }
|
|
155
194
|
)
|
|
156
195
|
)
|
|
157
196
|
.put(requireSecureSend(mockE164Number, AddressValidationType.PARTIAL))
|
|
@@ -159,10 +159,22 @@ export function* fetchAddressesAndValidateSaga({
|
|
|
159
159
|
// Clear existing entries for those numbers so our mapping consumers know new status is pending.
|
|
160
160
|
yield* put(updateE164PhoneNumberAddresses({ [e164Number]: undefined }, {}))
|
|
161
161
|
|
|
162
|
-
const
|
|
162
|
+
const { addresses, verifiedAddresses } = yield* call(fetchWalletAddresses, e164Number)
|
|
163
|
+
|
|
164
|
+
// When `verifiedAddresses` is present, use it as source of truth:
|
|
165
|
+
// it includes addresses verified by both CPV and SocialConnect.
|
|
166
|
+
// The `addresses` field is used for backward compatibility.
|
|
167
|
+
const walletAddresses = verifiedAddresses ? verifiedAddresses.map((v) => v.address) : addresses
|
|
163
168
|
|
|
164
169
|
const e164NumberToAddressUpdates: E164NumberToAddressType = {}
|
|
165
170
|
const addressToE164NumberUpdates: AddressToE164NumberType = {}
|
|
171
|
+
const addressToVerifiedByUpdates: Record<string, string> = {}
|
|
172
|
+
|
|
173
|
+
if (verifiedAddresses) {
|
|
174
|
+
for (const { address, verifiedBy } of verifiedAddresses) {
|
|
175
|
+
addressToVerifiedByUpdates[address] = verifiedBy
|
|
176
|
+
}
|
|
177
|
+
}
|
|
166
178
|
|
|
167
179
|
if (!walletAddresses.length) {
|
|
168
180
|
Logger.debug(TAG + '@fetchAddressesAndValidate', `No addresses for number`)
|
|
@@ -197,7 +209,11 @@ export function* fetchAddressesAndValidateSaga({
|
|
|
197
209
|
yield* put(requireSecureSend(e164Number, addressValidationType))
|
|
198
210
|
}
|
|
199
211
|
yield* put(
|
|
200
|
-
updateE164PhoneNumberAddresses(
|
|
212
|
+
updateE164PhoneNumberAddresses(
|
|
213
|
+
e164NumberToAddressUpdates,
|
|
214
|
+
addressToE164NumberUpdates,
|
|
215
|
+
addressToVerifiedByUpdates
|
|
216
|
+
)
|
|
201
217
|
)
|
|
202
218
|
yield* put(endFetchingAddresses(e164Number, true))
|
|
203
219
|
AppAnalytics.track(IdentityEvents.phone_number_lookup_complete)
|
|
@@ -265,9 +281,22 @@ function* fetchWalletAddresses(e164Number: string) {
|
|
|
265
281
|
throw new Error(`Failed to look up phone number: ${response.status} ${response.statusText}`)
|
|
266
282
|
}
|
|
267
283
|
|
|
268
|
-
const {
|
|
269
|
-
|
|
270
|
-
|
|
284
|
+
const {
|
|
285
|
+
data,
|
|
286
|
+
}: {
|
|
287
|
+
data: {
|
|
288
|
+
addresses: string[]
|
|
289
|
+
verifiedAddresses?: Array<{ address: string; verifiedBy: string }>
|
|
290
|
+
}
|
|
291
|
+
} = yield* call([response, 'json'])
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
addresses: data.addresses.map((address) => address.toLowerCase()),
|
|
295
|
+
verifiedAddresses: data.verifiedAddresses?.map((v) => ({
|
|
296
|
+
...v,
|
|
297
|
+
address: v.address.toLowerCase(),
|
|
298
|
+
})),
|
|
299
|
+
}
|
|
271
300
|
} catch (error) {
|
|
272
301
|
Logger.debug(`${TAG}/fetchWalletAddresses`, 'Unable to look up phone number', error)
|
|
273
302
|
throw new Error('Unable to fetch wallet address for this phone number')
|
package/src/identity/reducer.ts
CHANGED
|
@@ -54,6 +54,10 @@ export interface AddressToVerificationStatus {
|
|
|
54
54
|
[address: string]: boolean | undefined
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export interface AddressToVerifiedByType {
|
|
58
|
+
[address: string]: string | undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
57
61
|
interface State {
|
|
58
62
|
addressToE164Number: AddressToE164NumberType
|
|
59
63
|
// Note: Do not access values in this directly, use the `getAddressFromPhoneNumber` helper in contactMapping
|
|
@@ -67,6 +71,8 @@ interface State {
|
|
|
67
71
|
secureSendPhoneNumberMapping: SecureSendPhoneNumberMapping
|
|
68
72
|
// Mapping of address to verification status; undefined entries represent a loading state
|
|
69
73
|
addressToVerificationStatus: AddressToVerificationStatus
|
|
74
|
+
// Mapping of address to the entity that verified it (e.g. "valora", "minipay")
|
|
75
|
+
addressToVerifiedBy: AddressToVerifiedByType
|
|
70
76
|
lastSavedContactsHash: string | null
|
|
71
77
|
shouldRefreshStoredPasswordHash: boolean
|
|
72
78
|
}
|
|
@@ -83,6 +89,7 @@ const initialState: State = {
|
|
|
83
89
|
},
|
|
84
90
|
secureSendPhoneNumberMapping: {},
|
|
85
91
|
addressToVerificationStatus: {},
|
|
92
|
+
addressToVerifiedBy: {},
|
|
86
93
|
lastSavedContactsHash: null,
|
|
87
94
|
shouldRefreshStoredPasswordHash: false,
|
|
88
95
|
}
|
|
@@ -114,6 +121,10 @@ export const reducer = (
|
|
|
114
121
|
...state.e164NumberToAddress,
|
|
115
122
|
...action.e164NumberToAddress,
|
|
116
123
|
},
|
|
124
|
+
addressToVerifiedBy: {
|
|
125
|
+
...state.addressToVerifiedBy,
|
|
126
|
+
...action.addressToVerifiedBy,
|
|
127
|
+
},
|
|
117
128
|
}
|
|
118
129
|
case Actions.UPDATE_KNOWN_ADDRESSES:
|
|
119
130
|
return {
|
|
@@ -4,6 +4,7 @@ export const e164NumberToAddressSelector = (state: RootState) => state.identity.
|
|
|
4
4
|
export const addressToVerificationStatusSelector = (state: RootState) =>
|
|
5
5
|
state.identity.addressToVerificationStatus
|
|
6
6
|
export const addressToE164NumberSelector = (state: RootState) => state.identity.addressToE164Number
|
|
7
|
+
export const addressToVerifiedBySelector = (state: RootState) => state.identity.addressToVerifiedBy
|
|
7
8
|
export const secureSendPhoneNumberMappingSelector = (state: RootState) =>
|
|
8
9
|
state.identity.secureSendPhoneNumberMapping
|
|
9
10
|
export const importContactsProgressSelector = (state: RootState) =>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
2
2
|
import React from 'react'
|
|
3
3
|
import { Trans, useTranslation } from 'react-i18next'
|
|
4
|
-
import {
|
|
4
|
+
import { ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
5
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
5
6
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
6
7
|
import { KeylessBackupEvents } from 'src/analytics/Events'
|
|
7
8
|
import BackButton from 'src/components/BackButton'
|
|
@@ -33,7 +34,7 @@ function KeylessBackupIntro({ route }: Props) {
|
|
|
33
34
|
const { t } = useTranslation()
|
|
34
35
|
|
|
35
36
|
return (
|
|
36
|
-
<SafeAreaView style={styles.container}>
|
|
37
|
+
<SafeAreaView style={styles.container} edges={['bottom']}>
|
|
37
38
|
<ScrollView style={styles.scrollContainer}>
|
|
38
39
|
{isSetup && <Text style={styles.title}>{t('keylessBackupIntro.setup.title')}</Text>}
|
|
39
40
|
<Text
|
package/src/navigator/types.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useEffect } from 'react'
|
|
2
2
|
import { Trans, useTranslation } from 'react-i18next'
|
|
3
|
-
import {
|
|
3
|
+
import { ScrollView, StyleSheet, Text, View } from 'react-native'
|
|
4
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
4
5
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
5
6
|
import { NftEvents } from 'src/analytics/Events'
|
|
6
7
|
import Touchable from 'src/components/Touchable'
|
|
@@ -31,7 +32,7 @@ export default function NftsLoadError({ testID }: Props) {
|
|
|
31
32
|
}, [])
|
|
32
33
|
|
|
33
34
|
return (
|
|
34
|
-
<SafeAreaView style={styles.safeArea} testID={testID}>
|
|
35
|
+
<SafeAreaView style={styles.safeArea} testID={testID} edges={['bottom']}>
|
|
35
36
|
<ScrollView
|
|
36
37
|
contentContainerStyle={styles.contentContainerStyle}
|
|
37
38
|
style={styles.scrollContainer}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
addressToVerificationStatusSelector,
|
|
10
10
|
e164NumberToAddressSelector,
|
|
11
11
|
} from 'src/identity/selectors'
|
|
12
|
-
import
|
|
12
|
+
import Checkmark from 'src/icons/Checkmark'
|
|
13
13
|
import {
|
|
14
14
|
Recipient,
|
|
15
15
|
RecipientType,
|
|
@@ -63,12 +63,9 @@ function RecipientItem({ recipient, onSelectRecipient, loading, selected }: Prop
|
|
|
63
63
|
DefaultIcon={() => renderDefaultIcon(recipient)} // no need to honor color props here since the color we need match the defaults
|
|
64
64
|
/>
|
|
65
65
|
{!!showAppIcon && (
|
|
66
|
-
<
|
|
67
|
-
color={Colors.
|
|
68
|
-
|
|
69
|
-
size={ICON_SIZE}
|
|
70
|
-
testID="RecipientItem/AppIcon"
|
|
71
|
-
/>
|
|
66
|
+
<View style={styles.appIcon} testID="RecipientItem/AppIcon">
|
|
67
|
+
<Checkmark color={Colors.contentTertiary} height={ICON_SIZE} width={ICON_SIZE} />
|
|
68
|
+
</View>
|
|
72
69
|
)}
|
|
73
70
|
</View>
|
|
74
71
|
<View style={styles.contentContainer}>
|
|
@@ -129,13 +126,8 @@ const styles = StyleSheet.create({
|
|
|
129
126
|
top: 22,
|
|
130
127
|
left: 22,
|
|
131
128
|
backgroundColor: Colors.accent,
|
|
132
|
-
padding: 4,
|
|
133
129
|
borderRadius: 100,
|
|
134
|
-
|
|
135
|
-
shadowColor: undefined,
|
|
136
|
-
shadowOpacity: undefined,
|
|
137
|
-
shadowRadius: undefined,
|
|
138
|
-
shadowOffset: undefined,
|
|
130
|
+
padding: 2,
|
|
139
131
|
},
|
|
140
132
|
})
|
|
141
133
|
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
v235Schema,
|
|
68
68
|
v251Schema,
|
|
69
69
|
v253Schema,
|
|
70
|
+
v254Schema,
|
|
70
71
|
v28Schema,
|
|
71
72
|
v2Schema,
|
|
72
73
|
v35Schema,
|
|
@@ -1937,4 +1938,12 @@ describe('Redux persist migrations', () => {
|
|
|
1937
1938
|
expectedSchema.home = _.omit(oldSchema.home, 'hasSeenDivviBottomSheet')
|
|
1938
1939
|
expect(migratedSchema).toStrictEqual(expectedSchema)
|
|
1939
1940
|
})
|
|
1941
|
+
|
|
1942
|
+
it('works from 254 to 255', () => {
|
|
1943
|
+
const oldSchema = {
|
|
1944
|
+
...v254Schema,
|
|
1945
|
+
}
|
|
1946
|
+
const migratedSchema = migrations[255](oldSchema)
|
|
1947
|
+
expect(migratedSchema.identity.addressToVerifiedBy).toStrictEqual({})
|
|
1948
|
+
})
|
|
1940
1949
|
})
|
package/src/redux/migrations.ts
CHANGED
|
@@ -2070,4 +2070,11 @@ export const migrations = {
|
|
|
2070
2070
|
...state,
|
|
2071
2071
|
home: _.omit(state.home, 'hasSeenDivviBottomSheet'),
|
|
2072
2072
|
}),
|
|
2073
|
+
255: (state: any) => ({
|
|
2074
|
+
...state,
|
|
2075
|
+
identity: {
|
|
2076
|
+
...state.identity,
|
|
2077
|
+
addressToVerifiedBy: {},
|
|
2078
|
+
},
|
|
2079
|
+
}),
|
|
2073
2080
|
}
|
package/src/redux/store.test.ts
CHANGED
|
@@ -143,7 +143,7 @@ describe('store state', () => {
|
|
|
143
143
|
{
|
|
144
144
|
"_persist": {
|
|
145
145
|
"rehydrated": true,
|
|
146
|
-
"version":
|
|
146
|
+
"version": 255,
|
|
147
147
|
},
|
|
148
148
|
"account": {
|
|
149
149
|
"acceptedTerms": false,
|
|
@@ -245,6 +245,7 @@ describe('store state', () => {
|
|
|
245
245
|
"addressToDisplayName": {},
|
|
246
246
|
"addressToE164Number": {},
|
|
247
247
|
"addressToVerificationStatus": {},
|
|
248
|
+
"addressToVerifiedBy": {},
|
|
248
249
|
"askedContactsPermission": false,
|
|
249
250
|
"e164NumberToAddress": {},
|
|
250
251
|
"importContactsProgress": {
|
package/src/redux/store.ts
CHANGED
|
@@ -30,7 +30,7 @@ const persistConfig: PersistConfig<ReducersRootState> = {
|
|
|
30
30
|
key: 'root',
|
|
31
31
|
// default is -1, increment as we make migrations
|
|
32
32
|
// See https://github.com/valora-xyz/wallet/tree/main/WALLET.md#redux-state-migration
|
|
33
|
-
version:
|
|
33
|
+
version: 255,
|
|
34
34
|
keyPrefix: `reduxStore-`, // the redux-persist default is `persist:` which doesn't work with some file systems.
|
|
35
35
|
storage: FSStorage(),
|
|
36
36
|
blacklist: ['networkInfo', 'alert', 'imports', 'keylessBackup', transactionFeedV2Api.reducerPath],
|
package/src/send/EnterAmount.tsx
CHANGED
|
@@ -10,6 +10,7 @@ import BackButton from 'src/components/BackButton'
|
|
|
10
10
|
import { BottomSheetModalRefType } from 'src/components/BottomSheet'
|
|
11
11
|
import Button, { BtnSizes } from 'src/components/Button'
|
|
12
12
|
import FeeInfoBottomSheet from 'src/components/FeeInfoBottomSheet'
|
|
13
|
+
import { FilterChip } from 'src/components/FilterChipsCarousel'
|
|
13
14
|
import InLineNotification, { NotificationVariant } from 'src/components/InLineNotification'
|
|
14
15
|
import KeyboardAwareScrollView from 'src/components/KeyboardAwareScrollView'
|
|
15
16
|
import { ReviewDetailsItem } from 'src/components/ReviewTransaction'
|
|
@@ -66,6 +67,7 @@ interface Props {
|
|
|
66
67
|
children?: React.ReactNode
|
|
67
68
|
ProceedComponent: ComponentType<ProceedComponentProps>
|
|
68
69
|
disableBalanceCheck?: boolean
|
|
70
|
+
filterChips?: FilterChip<TokenBalance>[]
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
export const SendProceed = ({
|
|
@@ -107,6 +109,7 @@ export default function EnterAmount({
|
|
|
107
109
|
children,
|
|
108
110
|
ProceedComponent,
|
|
109
111
|
disableBalanceCheck = false,
|
|
112
|
+
filterChips,
|
|
110
113
|
}: Props) {
|
|
111
114
|
const { t } = useTranslation()
|
|
112
115
|
const insets = useSafeAreaInsets()
|
|
@@ -325,6 +328,7 @@ export default function EnterAmount({
|
|
|
325
328
|
tokens={tokens}
|
|
326
329
|
title={t('sendEnterAmountScreen.selectToken')}
|
|
327
330
|
titleStyle={styles.title}
|
|
331
|
+
filterChips={filterChips}
|
|
328
332
|
/>
|
|
329
333
|
</SafeAreaView>
|
|
330
334
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fireEvent, render, waitFor } from '@testing-library/react-native'
|
|
1
|
+
import { fireEvent, render, waitFor, within } from '@testing-library/react-native'
|
|
2
2
|
import BigNumber from 'bignumber.js'
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import { Provider } from 'react-redux'
|
|
@@ -10,6 +10,7 @@ import { Screens } from 'src/navigator/Screens'
|
|
|
10
10
|
import { RecipientType } from 'src/recipients/recipient'
|
|
11
11
|
import SendEnterAmount from 'src/send/SendEnterAmount'
|
|
12
12
|
import { usePrepareSendTransactions } from 'src/send/usePrepareSendTransactions'
|
|
13
|
+
import { getDynamicConfigParams } from 'src/statsig'
|
|
13
14
|
import { getSerializablePreparedTransactionsPossible } from 'src/viem/preparedTransactionSerialization'
|
|
14
15
|
import { PreparedTransactionsPossible } from 'src/viem/prepareTransactions'
|
|
15
16
|
import MockedNavigator from 'test/MockedNavigator'
|
|
@@ -29,6 +30,10 @@ import {
|
|
|
29
30
|
jest.mock('src/statsig')
|
|
30
31
|
jest.mock('src/send/usePrepareSendTransactions')
|
|
31
32
|
|
|
33
|
+
jest.mocked(getDynamicConfigParams).mockReturnValue({
|
|
34
|
+
miniPayTokenIds: [],
|
|
35
|
+
})
|
|
36
|
+
|
|
32
37
|
const mockPrepareTransactionsResultPossible: PreparedTransactionsPossible = {
|
|
33
38
|
type: 'possible',
|
|
34
39
|
transactions: [
|
|
@@ -164,6 +169,7 @@ describe('SendEnterAmount', () => {
|
|
|
164
169
|
underlyingTokenAddress: mockCeloAddress,
|
|
165
170
|
underlyingTokenSymbol: 'CELO',
|
|
166
171
|
amountEnteredIn: 'token',
|
|
172
|
+
isMiniPayRecipient: false,
|
|
167
173
|
})
|
|
168
174
|
expect(navigate).toHaveBeenCalledWith(Screens.SendConfirmation, {
|
|
169
175
|
origin: params.origin,
|
|
@@ -232,4 +238,105 @@ describe('SendEnterAmount', () => {
|
|
|
232
238
|
expect(getByTestId('SendEnterAmount/TokenSelect')).toHaveTextContent('cUSD', { exact: false })
|
|
233
239
|
expect(getByTestId('SendEnterAmount/TokenSelect')).toBeDisabled()
|
|
234
240
|
})
|
|
241
|
+
|
|
242
|
+
describe('MiniPay filter', () => {
|
|
243
|
+
const miniPayTokenIds = [mockCusdTokenId, mockUSDCTokenId]
|
|
244
|
+
|
|
245
|
+
beforeEach(() => {
|
|
246
|
+
jest.mocked(getDynamicConfigParams).mockReturnValue({
|
|
247
|
+
miniPayTokenIds,
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('should show MiniPay chip pre-selected and only MiniPay tokens when isMiniPayRecipient is true', () => {
|
|
252
|
+
const { getAllByTestId, getByText } = render(
|
|
253
|
+
<Provider store={store}>
|
|
254
|
+
<MockedNavigator
|
|
255
|
+
component={SendEnterAmount}
|
|
256
|
+
params={{ ...params, isMiniPayRecipient: true }}
|
|
257
|
+
/>
|
|
258
|
+
</Provider>
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
expect(getByText('MiniPay')).toBeTruthy()
|
|
262
|
+
|
|
263
|
+
const tokenBottomSheet = getAllByTestId('TokenBottomSheet')[0]
|
|
264
|
+
const tokens = within(tokenBottomSheet).getAllByTestId('TokenBalanceItem')
|
|
265
|
+
// only cUSD visible (USDC is on a different network and filtered by selector)
|
|
266
|
+
expect(tokens).toHaveLength(1)
|
|
267
|
+
expect(tokens[0]).toHaveTextContent('cUSD', { exact: false })
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('should select default token from MiniPay list', () => {
|
|
271
|
+
const { getByTestId } = render(
|
|
272
|
+
<Provider store={store}>
|
|
273
|
+
<MockedNavigator
|
|
274
|
+
component={SendEnterAmount}
|
|
275
|
+
params={{ ...params, isMiniPayRecipient: true }}
|
|
276
|
+
/>
|
|
277
|
+
</Provider>
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
// cUSD is the only MiniPay token with balance, not CELO (which is excluded)
|
|
281
|
+
expect(getByTestId('SendEnterAmount/TokenSelect')).toHaveTextContent('cUSD', { exact: false })
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('should show all tokens when MiniPay chip is toggled off', () => {
|
|
285
|
+
const { getAllByTestId, getByText } = render(
|
|
286
|
+
<Provider store={store}>
|
|
287
|
+
<MockedNavigator
|
|
288
|
+
component={SendEnterAmount}
|
|
289
|
+
params={{ ...params, isMiniPayRecipient: true }}
|
|
290
|
+
/>
|
|
291
|
+
</Provider>
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
fireEvent.press(getByText('MiniPay'))
|
|
295
|
+
|
|
296
|
+
const tokenBottomSheet = getAllByTestId('TokenBottomSheet')[0]
|
|
297
|
+
const tokens = within(tokenBottomSheet).getAllByTestId('TokenBalanceItem')
|
|
298
|
+
expect(tokens).toHaveLength(3)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('should not show MiniPay chip when isMiniPayRecipient is not set', () => {
|
|
302
|
+
const { queryByText } = render(
|
|
303
|
+
<Provider store={store}>
|
|
304
|
+
<MockedNavigator component={SendEnterAmount} params={params} />
|
|
305
|
+
</Provider>
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
expect(queryByText('MiniPay')).toBeFalsy()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should include isMiniPayRecipient in send_amount_continue analytics', async () => {
|
|
312
|
+
jest.mocked(usePrepareSendTransactions).mockReturnValue({
|
|
313
|
+
prepareTransactionsResult: mockPrepareTransactionsResultPossible,
|
|
314
|
+
prepareTransactionLoading: false,
|
|
315
|
+
refreshPreparedTransactions: jest.fn(),
|
|
316
|
+
clearPreparedTransactions: jest.fn(),
|
|
317
|
+
prepareTransactionError: undefined,
|
|
318
|
+
})
|
|
319
|
+
const { getByTestId, getByText } = render(
|
|
320
|
+
<Provider store={store}>
|
|
321
|
+
<MockedNavigator
|
|
322
|
+
component={SendEnterAmount}
|
|
323
|
+
params={{ ...params, isMiniPayRecipient: true }}
|
|
324
|
+
/>
|
|
325
|
+
</Provider>
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
fireEvent.changeText(getByTestId('SendEnterAmount/TokenAmountInput'), '8')
|
|
329
|
+
|
|
330
|
+
await waitFor(() => expect(getByText('review')).not.toBeDisabled())
|
|
331
|
+
fireEvent.press(getByText('review'))
|
|
332
|
+
|
|
333
|
+
await waitFor(() => expect(AppAnalytics.track).toHaveBeenCalledTimes(1))
|
|
334
|
+
expect(AppAnalytics.track).toHaveBeenCalledWith(
|
|
335
|
+
SendEvents.send_amount_continue,
|
|
336
|
+
expect.objectContaining({
|
|
337
|
+
isMiniPayRecipient: true,
|
|
338
|
+
})
|
|
339
|
+
)
|
|
340
|
+
})
|
|
341
|
+
})
|
|
235
342
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NativeStackScreenProps } from '@react-navigation/native-stack'
|
|
2
2
|
import BigNumber from 'bignumber.js'
|
|
3
|
-
import React
|
|
3
|
+
import React from 'react'
|
|
4
4
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
5
5
|
import { SendEvents } from 'src/analytics/Events'
|
|
6
6
|
import { getLocalCurrencyCode, usdToLocalCurrencyRateSelector } from 'src/localCurrency/selectors'
|
|
@@ -11,6 +11,7 @@ import { useSelector } from 'src/redux/hooks'
|
|
|
11
11
|
import EnterAmount, { ProceedArgs, SendProceed } from 'src/send/EnterAmount'
|
|
12
12
|
import { lastUsedTokenIdSelector } from 'src/send/selectors'
|
|
13
13
|
import { usePrepareSendTransactions } from 'src/send/usePrepareSendTransactions'
|
|
14
|
+
import useSendFilterChips from 'src/send/useSendFilterChips'
|
|
14
15
|
import { sortedTokensWithBalanceOrShowZeroBalanceSelector } from 'src/tokens/selectors'
|
|
15
16
|
import { TokenBalance } from 'src/tokens/slice'
|
|
16
17
|
import Logger from 'src/utils/Logger'
|
|
@@ -22,18 +23,24 @@ type Props = NativeStackScreenProps<StackParamList, Screens.SendEnterAmount>
|
|
|
22
23
|
const TAG = 'SendEnterAmount'
|
|
23
24
|
|
|
24
25
|
function SendEnterAmount({ route }: Props) {
|
|
25
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
defaultTokenIdOverride,
|
|
28
|
+
origin,
|
|
29
|
+
recipient,
|
|
30
|
+
isFromScan,
|
|
31
|
+
forceTokenId,
|
|
32
|
+
isMiniPayRecipient,
|
|
33
|
+
} = route.params
|
|
26
34
|
// explicitly allow zero state tokens to be shown for exploration purposes for
|
|
27
35
|
// new users with no balance
|
|
28
36
|
const tokens = useSelector(sortedTokensWithBalanceOrShowZeroBalanceSelector)
|
|
29
37
|
const lastUsedTokenId = useSelector(lastUsedTokenIdSelector)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}, [tokens, defaultTokenIdOverride, lastUsedTokenId])
|
|
38
|
+
const { filterChips, defaultToken } = useSendFilterChips({
|
|
39
|
+
isMiniPayRecipient,
|
|
40
|
+
tokens,
|
|
41
|
+
defaultTokenIdOverride,
|
|
42
|
+
lastUsedTokenId,
|
|
43
|
+
})
|
|
37
44
|
|
|
38
45
|
const localCurrencyCode = useSelector(getLocalCurrencyCode)
|
|
39
46
|
const localCurrencyExchangeRate = useSelector(usdToLocalCurrencyRateSelector)
|
|
@@ -72,6 +79,7 @@ function SendEnterAmount({ route }: Props) {
|
|
|
72
79
|
amountEnteredIn,
|
|
73
80
|
tokenId: token.tokenId,
|
|
74
81
|
networkId: token.networkId,
|
|
82
|
+
isMiniPayRecipient: isMiniPayRecipient ?? false,
|
|
75
83
|
})
|
|
76
84
|
}
|
|
77
85
|
|
|
@@ -116,6 +124,7 @@ function SendEnterAmount({ route }: Props) {
|
|
|
116
124
|
tokenSelectionDisabled={!!forceTokenId}
|
|
117
125
|
onPressProceed={handleReviewSend}
|
|
118
126
|
ProceedComponent={SendProceed}
|
|
127
|
+
filterChips={filterChips}
|
|
119
128
|
/>
|
|
120
129
|
)
|
|
121
130
|
}
|
|
@@ -243,6 +243,7 @@ describe('SendSelectRecipient', () => {
|
|
|
243
243
|
forceTokenId: undefined,
|
|
244
244
|
recipient: expect.any(Object),
|
|
245
245
|
origin: SendOrigin.AppSendFlow,
|
|
246
|
+
isMiniPayRecipient: false,
|
|
246
247
|
})
|
|
247
248
|
})
|
|
248
249
|
it('navigates to send amount when address recipient is pressed', async () => {
|
|
@@ -280,6 +281,7 @@ describe('SendSelectRecipient', () => {
|
|
|
280
281
|
forceTokenId: undefined,
|
|
281
282
|
recipient: expect.any(Object),
|
|
282
283
|
origin: SendOrigin.AppSendFlow,
|
|
284
|
+
isMiniPayRecipient: false,
|
|
283
285
|
})
|
|
284
286
|
})
|
|
285
287
|
|
|
@@ -522,6 +524,54 @@ describe('SendSelectRecipient', () => {
|
|
|
522
524
|
recipientType: 'PhoneNumber',
|
|
523
525
|
},
|
|
524
526
|
origin: SendOrigin.AppSendFlow,
|
|
527
|
+
isMiniPayRecipient: false,
|
|
528
|
+
})
|
|
529
|
+
})
|
|
530
|
+
it('navigates to send amount with isMiniPayRecipient when address is verified by minipay', async () => {
|
|
531
|
+
jest
|
|
532
|
+
.mocked(getRecipientVerificationStatus)
|
|
533
|
+
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
|
|
534
|
+
|
|
535
|
+
const store = createMockStore({
|
|
536
|
+
...storeWithPhoneVerified,
|
|
537
|
+
identity: {
|
|
538
|
+
secureSendPhoneNumberMapping: {
|
|
539
|
+
[mockE164Number3]: { addressValidationType: AddressValidationType.NONE },
|
|
540
|
+
},
|
|
541
|
+
e164NumberToAddress: { [mockE164Number3]: [mockAccount3] },
|
|
542
|
+
addressToVerifiedBy: { [mockAccount3]: 'minipay' },
|
|
543
|
+
},
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
const { getByTestId } = render(
|
|
547
|
+
<Provider store={store}>
|
|
548
|
+
<SendSelectRecipient {...mockScreenProps({})} />
|
|
549
|
+
</Provider>
|
|
550
|
+
)
|
|
551
|
+
const searchInput = getByTestId('SendSelectRecipientSearchInput')
|
|
552
|
+
|
|
553
|
+
await act(() => {
|
|
554
|
+
fireEvent.changeText(searchInput, mockE164Number3)
|
|
555
|
+
})
|
|
556
|
+
await act(() => {
|
|
557
|
+
fireEvent.press(getByTestId('RecipientItem'))
|
|
558
|
+
})
|
|
559
|
+
await act(() => {
|
|
560
|
+
fireEvent.press(getByTestId('SendOrInviteButton'))
|
|
561
|
+
})
|
|
562
|
+
|
|
563
|
+
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
|
|
564
|
+
isFromScan: false,
|
|
565
|
+
defaultTokenIdOverride: undefined,
|
|
566
|
+
forceTokenId: undefined,
|
|
567
|
+
recipient: {
|
|
568
|
+
address: mockAccount3,
|
|
569
|
+
displayNumber: '(415) 555-0123',
|
|
570
|
+
e164PhoneNumber: mockE164Number3,
|
|
571
|
+
recipientType: 'PhoneNumber',
|
|
572
|
+
},
|
|
573
|
+
origin: SendOrigin.AppSendFlow,
|
|
574
|
+
isMiniPayRecipient: true,
|
|
525
575
|
})
|
|
526
576
|
})
|
|
527
577
|
it('navigates to secure send flow when phone number recipient with multiple addresses, first time seeing it', async () => {
|
|
@@ -631,6 +681,7 @@ describe('SendSelectRecipient', () => {
|
|
|
631
681
|
recipientType: 'PhoneNumber',
|
|
632
682
|
},
|
|
633
683
|
origin: SendOrigin.AppSendFlow,
|
|
684
|
+
isMiniPayRecipient: false,
|
|
634
685
|
})
|
|
635
686
|
})
|
|
636
687
|
it.each([{ searchAddress: mockAccount2 }, { searchAddress: mockAccount3 }])(
|
|
@@ -694,6 +745,7 @@ describe('SendSelectRecipient', () => {
|
|
|
694
745
|
thumbnailPath: undefined,
|
|
695
746
|
},
|
|
696
747
|
origin: SendOrigin.AppSendFlow,
|
|
748
|
+
isMiniPayRecipient: false,
|
|
697
749
|
})
|
|
698
750
|
}
|
|
699
751
|
)
|
|
@@ -759,6 +811,7 @@ describe('SendSelectRecipient', () => {
|
|
|
759
811
|
thumbnailPath: undefined,
|
|
760
812
|
},
|
|
761
813
|
origin: SendOrigin.AppSendFlow,
|
|
814
|
+
isMiniPayRecipient: false,
|
|
762
815
|
})
|
|
763
816
|
}
|
|
764
817
|
)
|
|
@@ -21,6 +21,7 @@ import { getAddressFromPhoneNumber } from 'src/identity/contactMapping'
|
|
|
21
21
|
import { AddressValidationType } from 'src/identity/reducer'
|
|
22
22
|
import { getAddressValidationType } from 'src/identity/secureSend'
|
|
23
23
|
import {
|
|
24
|
+
addressToVerifiedBySelector,
|
|
24
25
|
e164NumberToAddressSelector,
|
|
25
26
|
secureSendPhoneNumberMappingSelector,
|
|
26
27
|
} from 'src/identity/selectors'
|
|
@@ -215,6 +216,7 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
215
216
|
const dispatch = useDispatch()
|
|
216
217
|
const secureSendPhoneNumberMapping = useSelector(secureSendPhoneNumberMappingSelector)
|
|
217
218
|
const e164NumberToAddress = useSelector(e164NumberToAddressSelector)
|
|
219
|
+
const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
|
|
218
220
|
const shareUrl = getAppConfig().experimental?.inviteFriends?.shareUrl ?? null
|
|
219
221
|
|
|
220
222
|
const forceTokenId = route.params?.forceTokenId
|
|
@@ -314,6 +316,7 @@ function SendSelectRecipient({ route }: Props) {
|
|
|
314
316
|
address,
|
|
315
317
|
},
|
|
316
318
|
origin: SendOrigin.AppSendFlow,
|
|
319
|
+
isMiniPayRecipient: addressToVerifiedBy?.[address] === 'minipay',
|
|
317
320
|
})
|
|
318
321
|
}
|
|
319
322
|
|
|
@@ -42,6 +42,7 @@ interface StateProps {
|
|
|
42
42
|
validationSuccessful: boolean
|
|
43
43
|
error?: ErrorMessages | null
|
|
44
44
|
validatedAddress?: string
|
|
45
|
+
isMiniPayRecipient: boolean
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
interface State {
|
|
@@ -77,6 +78,8 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
|
|
|
77
78
|
secureSendPhoneNumberMapping
|
|
78
79
|
)
|
|
79
80
|
const validatedAddress = getSecureSendAddress(recipient, secureSendPhoneNumberMapping)
|
|
81
|
+
const isMiniPayRecipient =
|
|
82
|
+
!!validatedAddress && state.identity.addressToVerifiedBy?.[validatedAddress] === 'minipay'
|
|
80
83
|
|
|
81
84
|
return {
|
|
82
85
|
recipient,
|
|
@@ -84,6 +87,7 @@ const mapStateToProps = (state: RootState, ownProps: OwnProps): StateProps => {
|
|
|
84
87
|
addressValidationType,
|
|
85
88
|
error,
|
|
86
89
|
validatedAddress,
|
|
90
|
+
isMiniPayRecipient,
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -121,6 +125,7 @@ export class ValidateRecipientAccount extends React.Component<Props, State> {
|
|
|
121
125
|
isFromScan: false,
|
|
122
126
|
forceTokenId: route.params.forceTokenId,
|
|
123
127
|
defaultTokenIdOverride: route.params.defaultTokenIdOverride,
|
|
128
|
+
isMiniPayRecipient: this.props.isMiniPayRecipient,
|
|
124
129
|
})
|
|
125
130
|
}
|
|
126
131
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BooleanFilterChip } from 'src/components/FilterChipsCarousel'
|
|
2
|
+
import { getDynamicConfigParams } from 'src/statsig'
|
|
3
|
+
import { DynamicConfigs } from 'src/statsig/constants'
|
|
4
|
+
import { StatsigDynamicConfigs } from 'src/statsig/types'
|
|
5
|
+
import { TokenBalance } from 'src/tokens/slice'
|
|
6
|
+
|
|
7
|
+
export default function useSendFilterChips({
|
|
8
|
+
isMiniPayRecipient,
|
|
9
|
+
tokens,
|
|
10
|
+
defaultTokenIdOverride,
|
|
11
|
+
lastUsedTokenId,
|
|
12
|
+
}: {
|
|
13
|
+
isMiniPayRecipient?: boolean
|
|
14
|
+
tokens: TokenBalance[]
|
|
15
|
+
defaultTokenIdOverride?: string
|
|
16
|
+
lastUsedTokenId?: string | null
|
|
17
|
+
}): {
|
|
18
|
+
filterChips: BooleanFilterChip<TokenBalance>[]
|
|
19
|
+
defaultToken: TokenBalance | undefined
|
|
20
|
+
} {
|
|
21
|
+
const { miniPayTokenIds: configTokenIds } = getDynamicConfigParams(
|
|
22
|
+
DynamicConfigs[StatsigDynamicConfigs.SEND_CONFIG]
|
|
23
|
+
)
|
|
24
|
+
const miniPayTokenIds = isMiniPayRecipient && configTokenIds.length > 0 ? configTokenIds : null
|
|
25
|
+
|
|
26
|
+
const filterChips: BooleanFilterChip<TokenBalance>[] = miniPayTokenIds
|
|
27
|
+
? [
|
|
28
|
+
{
|
|
29
|
+
id: 'minipay',
|
|
30
|
+
name: 'MiniPay',
|
|
31
|
+
filterFn: (token: TokenBalance) => miniPayTokenIds.includes(token.tokenId),
|
|
32
|
+
isSelected: true,
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
: []
|
|
36
|
+
|
|
37
|
+
const eligibleTokens = miniPayTokenIds
|
|
38
|
+
? tokens.filter((token) => miniPayTokenIds.includes(token.tokenId))
|
|
39
|
+
: tokens
|
|
40
|
+
const defaultToken =
|
|
41
|
+
eligibleTokens.find((token) => token.tokenId === defaultTokenIdOverride) ??
|
|
42
|
+
eligibleTokens.find((token) => token.tokenId === lastUsedTokenId) ??
|
|
43
|
+
eligibleTokens[0]
|
|
44
|
+
|
|
45
|
+
return { filterChips, defaultToken }
|
|
46
|
+
}
|
package/src/statsig/constants.ts
CHANGED
|
@@ -164,6 +164,12 @@ export const DynamicConfigs = {
|
|
|
164
164
|
inviteRewardsVersion: 'none',
|
|
165
165
|
},
|
|
166
166
|
},
|
|
167
|
+
[StatsigDynamicConfigs.SEND_CONFIG]: {
|
|
168
|
+
configName: StatsigDynamicConfigs.SEND_CONFIG,
|
|
169
|
+
defaultValues: {
|
|
170
|
+
miniPayTokenIds: [] as string[],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
167
173
|
} satisfies {
|
|
168
174
|
[key in StatsigDynamicConfigs]: {
|
|
169
175
|
configName: key
|
package/src/statsig/types.ts
CHANGED