wallet-stack 1.0.0-alpha.131 → 1.0.0-alpha.133
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 +11 -0
- package/package.json +2 -3
- package/src/analytics/Events.tsx +3 -10
- package/src/analytics/Properties.tsx +9 -25
- package/src/analytics/docs.ts +11 -8
- package/src/app/ErrorMessages.ts +1 -7
- package/src/identity/actions.ts +1 -97
- package/src/identity/contactMapping.test.ts +3 -28
- package/src/identity/contactMapping.ts +2 -88
- package/src/identity/reducer.ts +0 -77
- package/src/identity/saga.ts +2 -85
- package/src/identity/selectors.ts +0 -2
- package/src/images/Images.ts +3 -0
- package/src/images/assets/invite-modal.png +0 -0
- package/src/images/assets/invite-modal@1.5x.png +0 -0
- package/src/images/assets/invite-modal@2x.png +0 -0
- package/src/images/assets/invite-modal@3x.png +0 -0
- package/src/images/assets/invite-modal@4x.png +0 -0
- package/src/images/assets/minipay.png +0 -0
- package/src/images/assets/minipay@1.5x.png +0 -0
- package/src/images/assets/minipay@2x.png +0 -0
- package/src/images/assets/minipay@3x.png +0 -0
- package/src/images/assets/minipay@4x.png +0 -0
- package/src/images/assets/valora.png +0 -0
- package/src/images/assets/valora@1.5x.png +0 -0
- package/src/images/assets/valora@2x.png +0 -0
- package/src/images/assets/valora@3x.png +0 -0
- package/src/images/assets/valora@4x.png +0 -0
- package/src/index.d.ts +0 -1
- package/src/navigator/Navigator.tsx +10 -14
- package/src/navigator/Screens.tsx +2 -2
- package/src/navigator/types.tsx +5 -6
- package/src/qrcode/utils.test.tsx +4 -96
- package/src/qrcode/utils.ts +5 -114
- package/src/redux/migrations.test.ts +13 -0
- package/src/redux/migrations.ts +4 -0
- package/src/redux/store.test.ts +1 -2
- package/src/redux/store.ts +1 -1
- package/src/send/SelectRecipientAddress.test.tsx +146 -0
- package/src/send/SelectRecipientAddress.tsx +166 -0
- package/src/send/SendConfirmation.test.tsx +28 -0
- package/src/send/SendConfirmation.tsx +18 -1
- package/src/send/SendInvite.test.tsx +107 -0
- package/src/send/SendInvite.tsx +99 -0
- package/src/send/SendSelectRecipient.test.tsx +44 -223
- package/src/send/SendSelectRecipient.tsx +41 -149
- package/src/send/actions.ts +0 -26
- package/src/send/saga.ts +1 -6
- package/src/components/AccountNumberCard.tsx +0 -23
- package/src/components/ErrorMessageInline.tsx +0 -78
- package/src/components/SingleDigitInput.tsx +0 -53
- package/src/icons/HamburgerCard.tsx +0 -55
- package/src/identity/saga.test.ts +0 -103
- package/src/identity/secureSend.ts +0 -171
- package/src/send/ValidateRecipientAccount.test.tsx +0 -182
- package/src/send/ValidateRecipientAccount.tsx +0 -392
- package/src/send/ValidateRecipientIntro.test.tsx +0 -61
- package/src/send/ValidateRecipientIntro.tsx +0 -136
- package/src/send/__snapshots__/ValidateRecipientAccount.test.tsx.snap +0 -777
package/src/identity/reducer.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import dotProp from 'dot-prop-immutable'
|
|
2
1
|
import { RehydrateAction } from 'redux-persist'
|
|
3
2
|
import { Actions as AccountActions, ClearStoredAccountAction } from 'src/account/actions'
|
|
4
3
|
import { ActionTypes, Actions } from 'src/identity/actions'
|
|
@@ -32,24 +31,6 @@ export interface ImportContactProgress {
|
|
|
32
31
|
total: number
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
export enum AddressValidationType {
|
|
36
|
-
FULL = 'full',
|
|
37
|
-
PARTIAL = 'partial',
|
|
38
|
-
NONE = 'none',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface SecureSendPhoneNumberMapping {
|
|
42
|
-
[e164Number: string]: SecureSendDetails
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface SecureSendDetails {
|
|
46
|
-
address?: string
|
|
47
|
-
addressValidationType: AddressValidationType
|
|
48
|
-
isFetchingAddresses?: boolean
|
|
49
|
-
lastFetchSuccessful?: boolean
|
|
50
|
-
validationSuccessful?: boolean
|
|
51
|
-
}
|
|
52
|
-
|
|
53
34
|
export interface AddressToVerificationStatus {
|
|
54
35
|
[address: string]: boolean | undefined
|
|
55
36
|
}
|
|
@@ -67,8 +48,6 @@ interface State {
|
|
|
67
48
|
// Has the user already been asked for contacts permission
|
|
68
49
|
askedContactsPermission: boolean
|
|
69
50
|
importContactsProgress: ImportContactProgress
|
|
70
|
-
// Contacts found during the matchmaking process
|
|
71
|
-
secureSendPhoneNumberMapping: SecureSendPhoneNumberMapping
|
|
72
51
|
// Mapping of address to verification status; undefined entries represent a loading state
|
|
73
52
|
addressToVerificationStatus: AddressToVerificationStatus
|
|
74
53
|
// Mapping of address to the entity that verified it (e.g. "valora", "minipay")
|
|
@@ -87,7 +66,6 @@ const initialState: State = {
|
|
|
87
66
|
current: 0,
|
|
88
67
|
total: 0,
|
|
89
68
|
},
|
|
90
|
-
secureSendPhoneNumberMapping: {},
|
|
91
69
|
addressToVerificationStatus: {},
|
|
92
70
|
addressToVerifiedBy: {},
|
|
93
71
|
lastSavedContactsHash: null,
|
|
@@ -159,66 +137,11 @@ export const reducer = (
|
|
|
159
137
|
status: success ? ImportContactsStatus.Done : ImportContactsStatus.Failed,
|
|
160
138
|
},
|
|
161
139
|
}
|
|
162
|
-
case Actions.VALIDATE_RECIPIENT_ADDRESS_SUCCESS:
|
|
163
|
-
return {
|
|
164
|
-
...state,
|
|
165
|
-
// Overwrite the previous mapping when a new address is validated
|
|
166
|
-
secureSendPhoneNumberMapping: dotProp.set(
|
|
167
|
-
state.secureSendPhoneNumberMapping,
|
|
168
|
-
`${action.e164Number}`,
|
|
169
|
-
{
|
|
170
|
-
address: action.validatedAddress,
|
|
171
|
-
addressValidationType: AddressValidationType.NONE,
|
|
172
|
-
validationSuccessful: true,
|
|
173
|
-
}
|
|
174
|
-
),
|
|
175
|
-
}
|
|
176
|
-
case Actions.VALIDATE_RECIPIENT_ADDRESS_RESET:
|
|
177
|
-
return {
|
|
178
|
-
...state,
|
|
179
|
-
secureSendPhoneNumberMapping: dotProp.set(
|
|
180
|
-
state.secureSendPhoneNumberMapping,
|
|
181
|
-
`${action.e164Number}.validationSuccessful`,
|
|
182
|
-
false
|
|
183
|
-
),
|
|
184
|
-
}
|
|
185
|
-
case Actions.REQUIRE_SECURE_SEND:
|
|
186
|
-
return {
|
|
187
|
-
...state,
|
|
188
|
-
// Erase the previous mapping when new validation is required
|
|
189
|
-
secureSendPhoneNumberMapping: dotProp.set(
|
|
190
|
-
state.secureSendPhoneNumberMapping,
|
|
191
|
-
`${action.e164Number}`,
|
|
192
|
-
{
|
|
193
|
-
address: undefined,
|
|
194
|
-
addressValidationType: action.addressValidationType,
|
|
195
|
-
}
|
|
196
|
-
),
|
|
197
|
-
}
|
|
198
|
-
case Actions.FETCH_ADDRESSES_AND_VALIDATION_STATUS:
|
|
199
|
-
return {
|
|
200
|
-
...state,
|
|
201
|
-
secureSendPhoneNumberMapping: dotProp.set(
|
|
202
|
-
state.secureSendPhoneNumberMapping,
|
|
203
|
-
`${action.e164Number}.isFetchingAddresses`,
|
|
204
|
-
true
|
|
205
|
-
),
|
|
206
|
-
}
|
|
207
|
-
case Actions.END_FETCHING_ADDRESSES:
|
|
208
|
-
return {
|
|
209
|
-
...state,
|
|
210
|
-
secureSendPhoneNumberMapping: dotProp.merge(
|
|
211
|
-
state.secureSendPhoneNumberMapping,
|
|
212
|
-
`${action.e164Number}`,
|
|
213
|
-
{ isFetchingAddresses: false, lastFetchSuccessful: action.lastFetchSuccessful }
|
|
214
|
-
),
|
|
215
|
-
}
|
|
216
140
|
case AccountActions.CLEAR_STORED_ACCOUNT:
|
|
217
141
|
return {
|
|
218
142
|
...initialState,
|
|
219
143
|
addressToE164Number: state.addressToE164Number,
|
|
220
144
|
e164NumberToAddress: state.e164NumberToAddress,
|
|
221
|
-
secureSendPhoneNumberMapping: state.secureSendPhoneNumberMapping,
|
|
222
145
|
}
|
|
223
146
|
case Actions.FETCH_ADDRESS_VERIFICATION_STATUS:
|
|
224
147
|
// If the current status is false or does not exist, we set it to undefined
|
package/src/identity/saga.ts
CHANGED
|
@@ -1,94 +1,16 @@
|
|
|
1
|
-
import { showErrorInline } from 'src/alert/actions'
|
|
2
|
-
import { SendEvents } from 'src/analytics/Events'
|
|
3
|
-
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
4
|
-
import { ErrorMessages } from 'src/app/ErrorMessages'
|
|
5
|
-
import {
|
|
6
|
-
Actions,
|
|
7
|
-
ValidateRecipientAddressAction,
|
|
8
|
-
validateRecipientAddressSuccess,
|
|
9
|
-
} from 'src/identity/actions'
|
|
10
1
|
import {
|
|
11
2
|
doImportContactsWrapper,
|
|
12
3
|
fetchAddressVerificationSaga,
|
|
13
4
|
fetchAddressesAndValidateSaga,
|
|
14
5
|
saveContacts,
|
|
15
6
|
} from 'src/identity/contactMapping'
|
|
16
|
-
import {
|
|
17
|
-
import { validateAndReturnMatch } from 'src/identity/secureSend'
|
|
18
|
-
import { e164NumberToAddressSelector } from 'src/identity/selectors'
|
|
19
|
-
import { recipientHasNumber } from 'src/recipients/recipient'
|
|
7
|
+
import { Actions } from 'src/identity/actions'
|
|
20
8
|
import Logger from 'src/utils/Logger'
|
|
21
|
-
import { ensureError } from 'src/utils/ensureError'
|
|
22
9
|
import { safely } from 'src/utils/safely'
|
|
23
|
-
import {
|
|
24
|
-
import { cancelled, put, select, spawn, takeEvery, takeLatest, takeLeading } from 'typed-redux-saga'
|
|
10
|
+
import { cancelled, spawn, takeEvery, takeLatest, takeLeading } from 'typed-redux-saga'
|
|
25
11
|
|
|
26
12
|
const TAG = 'identity/saga'
|
|
27
13
|
|
|
28
|
-
export function* validateRecipientAddressSaga({
|
|
29
|
-
userInputOfFullAddressOrLastFourDigits,
|
|
30
|
-
addressValidationType,
|
|
31
|
-
recipient,
|
|
32
|
-
requesterAddress,
|
|
33
|
-
}: ValidateRecipientAddressAction) {
|
|
34
|
-
Logger.debug(TAG, 'Starting Recipient Address Validation')
|
|
35
|
-
try {
|
|
36
|
-
if (!recipientHasNumber(recipient)) {
|
|
37
|
-
throw Error(`Invalid recipient type for Secure Send, does not have e164Number`)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const userAddress = yield* select(currentAccountSelector)
|
|
41
|
-
if (!userAddress) {
|
|
42
|
-
// This should never happen
|
|
43
|
-
throw Error(`No userAddress set`)
|
|
44
|
-
}
|
|
45
|
-
const e164NumberToAddress = yield* select(e164NumberToAddressSelector)
|
|
46
|
-
const { e164PhoneNumber } = recipient
|
|
47
|
-
const possibleRecievingAddresses = e164NumberToAddress[e164PhoneNumber]
|
|
48
|
-
|
|
49
|
-
// Should never happen - Secure Send is initiated to deal with
|
|
50
|
-
// there being several possible addresses
|
|
51
|
-
if (!possibleRecievingAddresses) {
|
|
52
|
-
throw Error('There are no possible recipient addresses to validate against')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// E164NumberToAddress in redux store only holds verified addresses
|
|
56
|
-
// Need to add the requester address to the option set in the event
|
|
57
|
-
// a request is coming from an unverified account
|
|
58
|
-
if (requesterAddress && !possibleRecievingAddresses.includes(requesterAddress)) {
|
|
59
|
-
possibleRecievingAddresses.push(requesterAddress)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const validatedAddress = validateAndReturnMatch(
|
|
63
|
-
userInputOfFullAddressOrLastFourDigits,
|
|
64
|
-
possibleRecievingAddresses,
|
|
65
|
-
userAddress,
|
|
66
|
-
addressValidationType
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
AppAnalytics.track(SendEvents.send_secure_complete, {
|
|
70
|
-
confirmByScan: false,
|
|
71
|
-
partialAddressValidation: addressValidationType === AddressValidationType.PARTIAL,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
yield* put(validateRecipientAddressSuccess(e164PhoneNumber, validatedAddress))
|
|
75
|
-
} catch (err) {
|
|
76
|
-
const error = ensureError(err)
|
|
77
|
-
AppAnalytics.track(SendEvents.send_secure_incorrect, {
|
|
78
|
-
confirmByScan: false,
|
|
79
|
-
partialAddressValidation: addressValidationType === AddressValidationType.PARTIAL,
|
|
80
|
-
error: error.message,
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
Logger.error(TAG, 'validateRecipientAddressSaga/Address validation error: ', error)
|
|
84
|
-
if (Object.values(ErrorMessages).includes(error.message as ErrorMessages)) {
|
|
85
|
-
yield* put(showErrorInline(error.message as ErrorMessages))
|
|
86
|
-
} else {
|
|
87
|
-
yield* put(showErrorInline(ErrorMessages.ADDRESS_VALIDATION_ERROR))
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
14
|
function* watchContactMapping() {
|
|
93
15
|
yield* takeLeading(Actions.IMPORT_CONTACTS, safely(doImportContactsWrapper))
|
|
94
16
|
yield* takeLatest(
|
|
@@ -97,10 +19,6 @@ function* watchContactMapping() {
|
|
|
97
19
|
)
|
|
98
20
|
}
|
|
99
21
|
|
|
100
|
-
export function* watchValidateRecipientAddress() {
|
|
101
|
-
yield* takeLatest(Actions.VALIDATE_RECIPIENT_ADDRESS, safely(validateRecipientAddressSaga))
|
|
102
|
-
}
|
|
103
|
-
|
|
104
22
|
function* watchFetchAddressVerification() {
|
|
105
23
|
yield* takeEvery(Actions.FETCH_ADDRESS_VERIFICATION_STATUS, safely(fetchAddressVerificationSaga))
|
|
106
24
|
}
|
|
@@ -109,7 +27,6 @@ export function* identitySaga() {
|
|
|
109
27
|
Logger.debug(TAG, 'Initializing identity sagas')
|
|
110
28
|
try {
|
|
111
29
|
yield* spawn(watchContactMapping)
|
|
112
|
-
yield* spawn(watchValidateRecipientAddress)
|
|
113
30
|
yield* spawn(watchFetchAddressVerification)
|
|
114
31
|
yield* spawn(saveContacts) // save contacts on app start
|
|
115
32
|
} catch (error) {
|
|
@@ -5,8 +5,6 @@ export const addressToVerificationStatusSelector = (state: RootState) =>
|
|
|
5
5
|
state.identity.addressToVerificationStatus
|
|
6
6
|
export const addressToE164NumberSelector = (state: RootState) => state.identity.addressToE164Number
|
|
7
7
|
export const addressToVerifiedBySelector = (state: RootState) => state.identity.addressToVerifiedBy
|
|
8
|
-
export const secureSendPhoneNumberMappingSelector = (state: RootState) =>
|
|
9
|
-
state.identity.secureSendPhoneNumberMapping
|
|
10
8
|
export const importContactsProgressSelector = (state: RootState) =>
|
|
11
9
|
state.identity.importContactsProgress
|
|
12
10
|
export const addressToDisplayNameSelector = (state: RootState) =>
|
package/src/images/Images.ts
CHANGED
|
@@ -14,8 +14,11 @@ export const celoEducation4 = require('src/images/assets/celo-education-4.png')
|
|
|
14
14
|
export const email = require('src/images/assets/email.png')
|
|
15
15
|
export const fiatExchange = require('src/images/assets/fiat-exchange.png')
|
|
16
16
|
export const getVerified = require('src/images/assets/get-verified.png')
|
|
17
|
+
export const inviteModal = require('src/images/assets/invite-modal.png')
|
|
17
18
|
export const learnCelo = require('src/images/assets/learn-celo.png')
|
|
19
|
+
export const miniPay = require('src/images/assets/minipay.png')
|
|
18
20
|
export const pointsCardBackground = require('src/images/assets/points-card-background.png')
|
|
19
21
|
export const pointsIllustration = require('src/images/assets/points-illustration.png')
|
|
22
|
+
export const valora = require('src/images/assets/valora.png')
|
|
20
23
|
export const walletSafe = require('src/images/assets/wallet-safe.png')
|
|
21
24
|
export const earnCardBackground = require('src/images/assets/earn-card-background.png')
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/index.d.ts
CHANGED
|
@@ -102,15 +102,11 @@ import PointsIntro from 'src/points/PointsIntro'
|
|
|
102
102
|
import { NavigatorScreen } from 'src/public/navigate'
|
|
103
103
|
import { RootState } from 'src/redux/reducers'
|
|
104
104
|
import { store } from 'src/redux/store'
|
|
105
|
+
import SelectRecipientAddress from 'src/send/SelectRecipientAddress'
|
|
105
106
|
import SendConfirmation, { sendConfirmationScreenNavOptions } from 'src/send/SendConfirmation'
|
|
106
107
|
import SendEnterAmount from 'src/send/SendEnterAmount'
|
|
108
|
+
import SendInvite from 'src/send/SendInvite'
|
|
107
109
|
import SendSelectRecipient from 'src/send/SendSelectRecipient'
|
|
108
|
-
import ValidateRecipientAccount, {
|
|
109
|
-
validateRecipientAccountScreenNavOptions,
|
|
110
|
-
} from 'src/send/ValidateRecipientAccount'
|
|
111
|
-
import ValidateRecipientIntro, {
|
|
112
|
-
validateRecipientIntroScreenNavOptions,
|
|
113
|
-
} from 'src/send/ValidateRecipientIntro'
|
|
114
110
|
import { getFeatureGate } from 'src/statsig'
|
|
115
111
|
import { StatsigFeatureGates } from 'src/statsig/types'
|
|
116
112
|
import styles from 'src/styles/styles'
|
|
@@ -242,20 +238,20 @@ const sendScreens = (Navigator: typeof Stack) => (
|
|
|
242
238
|
options={sendConfirmationScreenNavOptions as NativeStackNavigationOptions}
|
|
243
239
|
/>
|
|
244
240
|
<Navigator.Screen
|
|
245
|
-
name={Screens.
|
|
246
|
-
component={
|
|
247
|
-
options={
|
|
248
|
-
/>
|
|
249
|
-
<Navigator.Screen
|
|
250
|
-
name={Screens.ValidateRecipientAccount}
|
|
251
|
-
component={ValidateRecipientAccount}
|
|
252
|
-
options={validateRecipientAccountScreenNavOptions}
|
|
241
|
+
name={Screens.SelectRecipientAddress}
|
|
242
|
+
component={SelectRecipientAddress}
|
|
243
|
+
options={SelectRecipientAddress.navigationOptions as NativeStackNavigationOptions}
|
|
253
244
|
/>
|
|
254
245
|
<Navigator.Screen
|
|
255
246
|
name={Screens.SendEnterAmount}
|
|
256
247
|
component={SendEnterAmount}
|
|
257
248
|
options={noHeader}
|
|
258
249
|
/>
|
|
250
|
+
<Navigator.Screen
|
|
251
|
+
name={Screens.SendInvite}
|
|
252
|
+
component={SendInvite}
|
|
253
|
+
options={SendInvite.navigationOptions}
|
|
254
|
+
/>
|
|
259
255
|
</>
|
|
260
256
|
)
|
|
261
257
|
|
|
@@ -75,6 +75,8 @@ export enum Screens {
|
|
|
75
75
|
SelectCountry = 'SelectCountry',
|
|
76
76
|
SelectLocalCurrency = 'SelectLocalCurrency',
|
|
77
77
|
SelectProvider = 'SelectProvider',
|
|
78
|
+
SendInvite = 'SendInvite',
|
|
79
|
+
SelectRecipientAddress = 'SelectRecipientAddress',
|
|
78
80
|
SendSelectRecipient = 'SendSelectRecipient',
|
|
79
81
|
SendConfirmation = 'SendConfirmation',
|
|
80
82
|
SendEnterAmount = 'SendEnterAmount',
|
|
@@ -94,8 +96,6 @@ export enum Screens {
|
|
|
94
96
|
TokenImport = 'TokenImport',
|
|
95
97
|
TransactionDetailsScreen = 'TransactionDetailsScreen',
|
|
96
98
|
UpgradeScreen = 'UpgradeScreen',
|
|
97
|
-
ValidateRecipientAccount = 'ValidateRecipientAccount',
|
|
98
|
-
ValidateRecipientIntro = 'ValidateRecipientIntro',
|
|
99
99
|
VerificationCodeInputScreen = 'VerificationCodeInputScreen',
|
|
100
100
|
VerificationStartScreen = 'VerificationStartScreen',
|
|
101
101
|
WalletConnectRequest = 'WalletConnectRequest',
|
package/src/navigator/types.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { KeylessBackupFlow, KeylessBackupOrigin } from 'src/keylessBackup/types'
|
|
|
9
9
|
import { Screens } from 'src/navigator/Screens'
|
|
10
10
|
import { Nft } from 'src/nfts/types'
|
|
11
11
|
import { EarnPosition } from 'src/positions/types'
|
|
12
|
-
import { Recipient } from 'src/recipients/recipient'
|
|
12
|
+
import { MobileRecipient, Recipient } from 'src/recipients/recipient'
|
|
13
13
|
import { QrCode, TransactionDataInput } from 'src/send/types'
|
|
14
14
|
import type { SwapTransaction } from 'src/swap/types'
|
|
15
15
|
import type { SerializedTokenBalance } from 'src/tokens/slice'
|
|
@@ -45,10 +45,9 @@ type SendEnterAmountParams = {
|
|
|
45
45
|
isMiniPayRecipient?: boolean
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
interface
|
|
49
|
-
|
|
48
|
+
interface SelectRecipientAddressParams {
|
|
49
|
+
recipient: MobileRecipient
|
|
50
50
|
origin: SendOrigin
|
|
51
|
-
recipient: Recipient
|
|
52
51
|
forceTokenId?: boolean
|
|
53
52
|
defaultTokenIdOverride?: string
|
|
54
53
|
}
|
|
@@ -261,6 +260,8 @@ export type StackParamList = {
|
|
|
261
260
|
fiat: number
|
|
262
261
|
}
|
|
263
262
|
}
|
|
263
|
+
[Screens.SendInvite]: { recipient: Recipient; shareUrl: string }
|
|
264
|
+
[Screens.SelectRecipientAddress]: SelectRecipientAddressParams
|
|
264
265
|
[Screens.SendSelectRecipient]:
|
|
265
266
|
| {
|
|
266
267
|
forceTokenId?: boolean
|
|
@@ -300,8 +301,6 @@ export type StackParamList = {
|
|
|
300
301
|
transaction: TokenTransaction
|
|
301
302
|
}
|
|
302
303
|
[Screens.UpgradeScreen]: undefined
|
|
303
|
-
[Screens.ValidateRecipientIntro]: ValidateRecipientParams
|
|
304
|
-
[Screens.ValidateRecipientAccount]: ValidateRecipientParams
|
|
305
304
|
[Screens.VerificationStartScreen]:
|
|
306
305
|
| {
|
|
307
306
|
hasOnboarded?: boolean
|
|
@@ -3,45 +3,32 @@ import * as React from 'react'
|
|
|
3
3
|
import 'react-native'
|
|
4
4
|
import { View } from 'react-native'
|
|
5
5
|
import { expectSaga } from 'redux-saga-test-plan'
|
|
6
|
-
import {
|
|
6
|
+
import { select } from 'redux-saga-test-plan/matchers'
|
|
7
7
|
import { showError } from 'src/alert/actions'
|
|
8
8
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
9
9
|
import { QrScreenEvents } from 'src/analytics/Events'
|
|
10
10
|
import { HooksEnablePreviewOrigin, SendOrigin } from 'src/analytics/types'
|
|
11
11
|
import { ErrorMessages } from 'src/app/ErrorMessages'
|
|
12
12
|
import { DEEP_LINK_URL_SCHEME } from 'src/config'
|
|
13
|
-
import {
|
|
14
|
-
e164NumberToAddressSelector,
|
|
15
|
-
secureSendPhoneNumberMappingSelector,
|
|
16
|
-
} from 'src/identity/selectors'
|
|
13
|
+
import { e164NumberToAddressSelector } from 'src/identity/selectors'
|
|
17
14
|
import { navigate } from 'src/navigator/NavigationService'
|
|
18
15
|
import { Screens } from 'src/navigator/Screens'
|
|
19
16
|
import { handleEnableHooksPreviewDeepLink } from 'src/positions/saga'
|
|
20
17
|
import { allowHooksPreviewSelector } from 'src/positions/selectors'
|
|
21
18
|
import { urlFromUriData } from 'src/qrcode/schema'
|
|
22
|
-
import {
|
|
23
|
-
QRCodeTypes,
|
|
24
|
-
handleQRCodeDefault,
|
|
25
|
-
handleQRCodeSecureSend,
|
|
26
|
-
handleSecureSend,
|
|
27
|
-
useQRContent,
|
|
28
|
-
} from 'src/qrcode/utils'
|
|
19
|
+
import { QRCodeTypes, handleQRCodeDefault, useQRContent } from 'src/qrcode/utils'
|
|
29
20
|
import { RecipientType } from 'src/recipients/recipient'
|
|
30
21
|
import { recipientInfoSelector } from 'src/recipients/reducer'
|
|
31
|
-
import { handleQRCodeDetected
|
|
22
|
+
import { handleQRCodeDetected } from 'src/send/actions'
|
|
32
23
|
import { QrCode } from 'src/send/types'
|
|
33
24
|
import { createMockStore } from 'test/utils'
|
|
34
25
|
import {
|
|
35
26
|
mockAccount,
|
|
36
|
-
mockAccount2,
|
|
37
27
|
mockAccount3,
|
|
38
28
|
mockE164Number,
|
|
39
29
|
mockE164Number3,
|
|
40
|
-
mockE164NumberToAddress,
|
|
41
|
-
mockEthTokenId,
|
|
42
30
|
mockName,
|
|
43
31
|
mockQrCodeData,
|
|
44
|
-
mockRecipient,
|
|
45
32
|
mockRecipientInfo,
|
|
46
33
|
} from 'test/values'
|
|
47
34
|
|
|
@@ -212,82 +199,3 @@ describe('handleQRCodeDefault', () => {
|
|
|
212
199
|
expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, qrCode)
|
|
213
200
|
})
|
|
214
201
|
})
|
|
215
|
-
|
|
216
|
-
describe('handleQRCodeSecureSend', () => {
|
|
217
|
-
it('handles a valid address and navigates to send enter amount when there is no transaction data', async () => {
|
|
218
|
-
const data: QrCode = { type: QRCodeTypes.QR_CODE, data: mockAccount }
|
|
219
|
-
await expectSaga(
|
|
220
|
-
handleQRCodeSecureSend,
|
|
221
|
-
handleQRCodeDetectedSecureSend(data, mockRecipient, mockAccount2, false, mockEthTokenId)
|
|
222
|
-
)
|
|
223
|
-
.provide([
|
|
224
|
-
[select(e164NumberToAddressSelector), mockE164NumberToAddress],
|
|
225
|
-
[
|
|
226
|
-
select(secureSendPhoneNumberMappingSelector),
|
|
227
|
-
{
|
|
228
|
-
[mockRecipient.e164PhoneNumber]: {
|
|
229
|
-
address: mockAccount,
|
|
230
|
-
addressValidationType: undefined,
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
[
|
|
235
|
-
call(
|
|
236
|
-
handleSecureSend,
|
|
237
|
-
mockAccount.toLowerCase(),
|
|
238
|
-
mockE164NumberToAddress,
|
|
239
|
-
mockRecipient,
|
|
240
|
-
mockAccount2
|
|
241
|
-
),
|
|
242
|
-
true,
|
|
243
|
-
],
|
|
244
|
-
])
|
|
245
|
-
.run()
|
|
246
|
-
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
|
|
247
|
-
origin: SendOrigin.AppSendFlow,
|
|
248
|
-
recipient: {
|
|
249
|
-
...mockRecipient,
|
|
250
|
-
address: mockAccount,
|
|
251
|
-
},
|
|
252
|
-
isFromScan: true,
|
|
253
|
-
forceTokenId: false,
|
|
254
|
-
defaultTokenIdOverride: mockEthTokenId,
|
|
255
|
-
})
|
|
256
|
-
expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, data)
|
|
257
|
-
})
|
|
258
|
-
it('handles an invalid address', async () => {
|
|
259
|
-
const data: QrCode = { type: QRCodeTypes.QR_CODE, data: 'invalid-address' }
|
|
260
|
-
await expectSaga(
|
|
261
|
-
handleQRCodeSecureSend,
|
|
262
|
-
handleQRCodeDetectedSecureSend(data, mockRecipient, mockAccount2)
|
|
263
|
-
)
|
|
264
|
-
.provide([[select(e164NumberToAddressSelector), mockE164NumberToAddress]])
|
|
265
|
-
.put(showError(ErrorMessages.QR_FAILED_INVALID_ADDRESS))
|
|
266
|
-
.run()
|
|
267
|
-
expect(navigate).not.toHaveBeenCalled()
|
|
268
|
-
expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, data)
|
|
269
|
-
})
|
|
270
|
-
it('handles failed address lookup', async () => {
|
|
271
|
-
const data: QrCode = { type: QRCodeTypes.QR_CODE, data: mockAccount }
|
|
272
|
-
await expectSaga(
|
|
273
|
-
handleQRCodeSecureSend,
|
|
274
|
-
handleQRCodeDetectedSecureSend(data, mockRecipient, mockAccount2)
|
|
275
|
-
)
|
|
276
|
-
.provide([
|
|
277
|
-
[select(e164NumberToAddressSelector), mockE164NumberToAddress],
|
|
278
|
-
[
|
|
279
|
-
call(
|
|
280
|
-
handleSecureSend,
|
|
281
|
-
mockAccount.toLowerCase(),
|
|
282
|
-
mockE164NumberToAddress,
|
|
283
|
-
mockRecipient,
|
|
284
|
-
mockAccount2
|
|
285
|
-
),
|
|
286
|
-
false,
|
|
287
|
-
],
|
|
288
|
-
])
|
|
289
|
-
.run()
|
|
290
|
-
expect(navigate).not.toHaveBeenCalled()
|
|
291
|
-
expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, data)
|
|
292
|
-
})
|
|
293
|
-
})
|
package/src/qrcode/utils.ts
CHANGED
|
@@ -1,40 +1,18 @@
|
|
|
1
1
|
import * as RNFS from '@valora/react-native-fs'
|
|
2
2
|
import { useMemo } from 'react'
|
|
3
3
|
import Share from 'react-native-share'
|
|
4
|
-
import { showError
|
|
4
|
+
import { showError } from 'src/alert/actions'
|
|
5
5
|
import AppAnalytics from 'src/analytics/AppAnalytics'
|
|
6
|
-
import { QrScreenEvents
|
|
7
|
-
import {
|
|
8
|
-
HooksEnablePreviewOrigin,
|
|
9
|
-
SendOrigin,
|
|
10
|
-
WalletConnectPairingOrigin,
|
|
11
|
-
} from 'src/analytics/types'
|
|
6
|
+
import { QrScreenEvents } from 'src/analytics/Events'
|
|
7
|
+
import { HooksEnablePreviewOrigin, WalletConnectPairingOrigin } from 'src/analytics/types'
|
|
12
8
|
import { ErrorMessages } from 'src/app/ErrorMessages'
|
|
13
9
|
import { DEEP_LINK_URL_SCHEME } from 'src/config'
|
|
14
|
-
import { validateRecipientAddressSuccess } from 'src/identity/actions'
|
|
15
|
-
import { E164NumberToAddressType } from 'src/identity/reducer'
|
|
16
|
-
import { getSecureSendAddress } from 'src/identity/secureSend'
|
|
17
|
-
import {
|
|
18
|
-
e164NumberToAddressSelector,
|
|
19
|
-
secureSendPhoneNumberMappingSelector,
|
|
20
|
-
} from 'src/identity/selectors'
|
|
21
|
-
import { navigate } from 'src/navigator/NavigationService'
|
|
22
|
-
import { Screens } from 'src/navigator/Screens'
|
|
23
10
|
import { handleEnableHooksPreviewDeepLink } from 'src/positions/saga'
|
|
24
11
|
import { allowHooksPreviewSelector } from 'src/positions/selectors'
|
|
25
12
|
import { UriData, uriDataFromUrl } from 'src/qrcode/schema'
|
|
26
|
-
import {
|
|
27
|
-
Recipient,
|
|
28
|
-
RecipientInfo,
|
|
29
|
-
getRecipientFromAddress,
|
|
30
|
-
recipientHasNumber,
|
|
31
|
-
} from 'src/recipients/recipient'
|
|
13
|
+
import { RecipientInfo, getRecipientFromAddress } from 'src/recipients/recipient'
|
|
32
14
|
import { recipientInfoSelector } from 'src/recipients/reducer'
|
|
33
|
-
import {
|
|
34
|
-
HandleQRCodeDetectedAction,
|
|
35
|
-
HandleQRCodeDetectedSecureSendAction,
|
|
36
|
-
SVG,
|
|
37
|
-
} from 'src/send/actions'
|
|
15
|
+
import { HandleQRCodeDetectedAction, SVG } from 'src/send/actions'
|
|
38
16
|
import { QrCode } from 'src/send/types'
|
|
39
17
|
import { handleSendPaymentData } from 'src/send/utils'
|
|
40
18
|
import Logger from 'src/utils/Logger'
|
|
@@ -85,48 +63,6 @@ export async function shareSVGImage(svg: SVG) {
|
|
|
85
63
|
})
|
|
86
64
|
}
|
|
87
65
|
|
|
88
|
-
export function* handleSecureSend(
|
|
89
|
-
address: string,
|
|
90
|
-
e164NumberToAddress: E164NumberToAddressType,
|
|
91
|
-
recipient: Recipient,
|
|
92
|
-
requesterAddress?: string
|
|
93
|
-
) {
|
|
94
|
-
if (!recipientHasNumber(recipient)) {
|
|
95
|
-
throw Error('Invalid recipient type for Secure Send, has no mobile number')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const userScannedAddress = address.toLowerCase()
|
|
99
|
-
const { e164PhoneNumber } = recipient
|
|
100
|
-
const possibleReceivingAddresses = e164NumberToAddress[e164PhoneNumber]
|
|
101
|
-
// This should never happen. Secure Send is triggered when there are
|
|
102
|
-
// multiple addresses for a given phone number
|
|
103
|
-
if (!possibleReceivingAddresses) {
|
|
104
|
-
throw Error("No addresses associated with recipient's phone number")
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Need to add the requester address to the option set in the event
|
|
108
|
-
// a request is coming from an unverified account
|
|
109
|
-
if (requesterAddress && !possibleReceivingAddresses.includes(requesterAddress)) {
|
|
110
|
-
possibleReceivingAddresses.push(requesterAddress)
|
|
111
|
-
}
|
|
112
|
-
const possibleReceivingAddressesFormatted = possibleReceivingAddresses.map((addr) =>
|
|
113
|
-
addr.toLowerCase()
|
|
114
|
-
)
|
|
115
|
-
if (!possibleReceivingAddressesFormatted.includes(userScannedAddress)) {
|
|
116
|
-
const error = ErrorMessages.QR_FAILED_INVALID_RECIPIENT
|
|
117
|
-
AppAnalytics.track(SendEvents.send_secure_incorrect, {
|
|
118
|
-
confirmByScan: true,
|
|
119
|
-
error,
|
|
120
|
-
})
|
|
121
|
-
yield* put(showMessage(error))
|
|
122
|
-
return false
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
AppAnalytics.track(SendEvents.send_secure_complete, { confirmByScan: true })
|
|
126
|
-
yield* put(validateRecipientAddressSuccess(e164PhoneNumber, userScannedAddress))
|
|
127
|
-
return true
|
|
128
|
-
}
|
|
129
|
-
|
|
130
66
|
function* extractQRAddressData(qrCode: QrCode) {
|
|
131
67
|
// strip network prefix if present
|
|
132
68
|
const qrAddress = qrCode.data.split(':').at(-1) || qrCode.data
|
|
@@ -183,48 +119,3 @@ export function* handleQRCodeDefault({
|
|
|
183
119
|
defaultTokenIdOverride,
|
|
184
120
|
})
|
|
185
121
|
}
|
|
186
|
-
|
|
187
|
-
export function* handleQRCodeSecureSend({
|
|
188
|
-
qrCode,
|
|
189
|
-
requesterAddress,
|
|
190
|
-
recipient,
|
|
191
|
-
forceTokenId,
|
|
192
|
-
defaultTokenIdOverride,
|
|
193
|
-
}: HandleQRCodeDetectedSecureSendAction) {
|
|
194
|
-
const e164NumberToAddress = yield* select(e164NumberToAddressSelector)
|
|
195
|
-
AppAnalytics.track(QrScreenEvents.qr_scanned, qrCode)
|
|
196
|
-
|
|
197
|
-
const qrData = yield* call(extractQRAddressData, qrCode)
|
|
198
|
-
if (!qrData) {
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const success: boolean = yield* call(
|
|
203
|
-
handleSecureSend,
|
|
204
|
-
qrData.address,
|
|
205
|
-
e164NumberToAddress,
|
|
206
|
-
recipient,
|
|
207
|
-
requesterAddress
|
|
208
|
-
)
|
|
209
|
-
if (!success) {
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const secureSendPhoneNumberMapping = yield* select(secureSendPhoneNumberMappingSelector)
|
|
214
|
-
const address = getSecureSendAddress(recipient, secureSendPhoneNumberMapping)
|
|
215
|
-
if (!address) {
|
|
216
|
-
// should never happen b/c if handleSecureSend succeeds then address should be there
|
|
217
|
-
Logger.error(TAG, `No secure send address found for recipient ${recipient}`)
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
navigate(Screens.SendEnterAmount, {
|
|
221
|
-
origin: SendOrigin.AppSendFlow,
|
|
222
|
-
recipient: {
|
|
223
|
-
...recipient,
|
|
224
|
-
address,
|
|
225
|
-
},
|
|
226
|
-
isFromScan: true,
|
|
227
|
-
forceTokenId,
|
|
228
|
-
defaultTokenIdOverride,
|
|
229
|
-
})
|
|
230
|
-
}
|