wallet-stack 1.0.0-alpha.136 → 1.0.0-alpha.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/locales/base/translation.json +1 -0
- package/package.json +1 -1
- package/src/components/ReviewTransaction.test.tsx +26 -0
- package/src/components/ReviewTransaction.tsx +28 -7
- package/src/navigator/types.tsx +3 -1
- package/src/recipients/verifier.ts +5 -3
- package/src/send/EnterAmount.tsx +17 -0
- package/src/send/SelectRecipientAddress.test.tsx +4 -4
- package/src/send/SelectRecipientAddress.tsx +5 -56
- package/src/send/SelectRecipientAddressList.test.tsx +68 -0
- package/src/send/SelectRecipientAddressList.tsx +117 -0
- package/src/send/SelectedRecipientCard.test.tsx +96 -0
- package/src/send/SelectedRecipientCard.tsx +197 -0
- package/src/send/SendConfirmation.test.tsx +23 -2
- package/src/send/SendConfirmation.tsx +1 -4
- package/src/send/SendEnterAmount.test.tsx +22 -29
- package/src/send/SendEnterAmount.tsx +39 -4
- package/src/send/SendSelectRecipient.test.tsx +39 -44
- package/src/send/SendSelectRecipient.tsx +6 -4
- package/src/send/useRecipientLookup.test.tsx +139 -0
- package/src/send/useRecipientLookup.ts +74 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react-native'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { Provider } from 'react-redux'
|
|
4
|
+
import {
|
|
5
|
+
fetchAddressVerification,
|
|
6
|
+
fetchAddressesAndValidate,
|
|
7
|
+
recipientLookupResolved,
|
|
8
|
+
updateE164PhoneNumberAddresses,
|
|
9
|
+
} from 'src/identity/actions'
|
|
10
|
+
import { Recipient, RecipientType } from 'src/recipients/recipient'
|
|
11
|
+
import { setupStore } from 'src/redux/store'
|
|
12
|
+
import { useRecipientLookup } from 'src/send/useRecipientLookup'
|
|
13
|
+
import { getMockStoreData } from 'test/utils'
|
|
14
|
+
import { mockAccount, mockAccount2, mockE164Number, mockName } from 'test/values'
|
|
15
|
+
|
|
16
|
+
jest.mock('src/redux/sagas', () => ({
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
18
|
+
rootSaga: jest.fn(function* () {}),
|
|
19
|
+
}))
|
|
20
|
+
|
|
21
|
+
const phoneRecipient: Recipient & { address: string } = {
|
|
22
|
+
name: mockName,
|
|
23
|
+
contactId: 'contactId',
|
|
24
|
+
e164PhoneNumber: mockE164Number,
|
|
25
|
+
recipientType: RecipientType.PhoneNumber,
|
|
26
|
+
address: mockAccount,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const addressRecipient: Recipient & { address: string } = {
|
|
30
|
+
address: mockAccount,
|
|
31
|
+
recipientType: RecipientType.Address,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function setupHook(
|
|
35
|
+
recipient: Recipient & { address: string },
|
|
36
|
+
options?: { skipFetch?: boolean },
|
|
37
|
+
storeOverrides: Parameters<typeof getMockStoreData>[0] = {}
|
|
38
|
+
) {
|
|
39
|
+
const { store } = setupStore(getMockStoreData(storeOverrides))
|
|
40
|
+
const dispatchSpy = jest.spyOn(store, 'dispatch')
|
|
41
|
+
const { result, rerender } = renderHook(() => useRecipientLookup(recipient, options), {
|
|
42
|
+
wrapper: ({ children }: { children: React.ReactNode }) => (
|
|
43
|
+
<Provider store={store}>{children}</Provider>
|
|
44
|
+
),
|
|
45
|
+
})
|
|
46
|
+
return { result, rerender, store, dispatchSpy }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('useRecipientLookup', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
jest.clearAllMocks()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('dispatches fetchAddressesAndValidate for phone recipients', () => {
|
|
55
|
+
const { dispatchSpy } = setupHook(phoneRecipient)
|
|
56
|
+
expect(dispatchSpy).toHaveBeenCalledWith(fetchAddressesAndValidate(mockE164Number))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('dispatches fetchAddressVerification for address-only recipients', () => {
|
|
60
|
+
const { dispatchSpy } = setupHook(addressRecipient)
|
|
61
|
+
expect(dispatchSpy).toHaveBeenCalledWith(fetchAddressVerification(mockAccount.toLowerCase()))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('does not dispatch when skipFetch is set (caller already triggered the lookup)', () => {
|
|
65
|
+
const { dispatchSpy } = setupHook(phoneRecipient, { skipFetch: true })
|
|
66
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(fetchAddressesAndValidate(mockE164Number))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('reports loading while the lookup is in flight and resolves to unknown when no verifier is recorded', () => {
|
|
70
|
+
const { result, store } = setupHook(addressRecipient)
|
|
71
|
+
expect(result.current.status).toBe('loading')
|
|
72
|
+
|
|
73
|
+
act(() => {
|
|
74
|
+
store.dispatch(recipientLookupResolved())
|
|
75
|
+
})
|
|
76
|
+
expect(result.current.status).toBe('unknown')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('returns "unverified" for a phone recipient when the lookup resolved with no verified addresses', () => {
|
|
80
|
+
const { result, store } = setupHook(phoneRecipient, { skipFetch: true })
|
|
81
|
+
act(() => {
|
|
82
|
+
store.dispatch(updateE164PhoneNumberAddresses({ [mockE164Number]: null }, {}))
|
|
83
|
+
})
|
|
84
|
+
expect(result.current.status).toBe('unverified')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('returns "verified" when the address has a known verifier', () => {
|
|
88
|
+
const { result } = setupHook(
|
|
89
|
+
addressRecipient,
|
|
90
|
+
{ skipFetch: true },
|
|
91
|
+
{
|
|
92
|
+
identity: {
|
|
93
|
+
addressToVerifiedBy: { [mockAccount.toLowerCase()]: 'valora' },
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
expect(result.current.status).toBe('verified')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('returns "unverified" when the address is known to be unverified (verifier=null)', () => {
|
|
101
|
+
const { result } = setupHook(
|
|
102
|
+
addressRecipient,
|
|
103
|
+
{ skipFetch: true },
|
|
104
|
+
{
|
|
105
|
+
identity: {
|
|
106
|
+
addressToVerifiedBy: { [mockAccount.toLowerCase()]: null },
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
expect(result.current.status).toBe('unverified')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('returns the verified addresses linked to a phone recipient, filtering out unverified ones', () => {
|
|
114
|
+
const { result, store } = setupHook(
|
|
115
|
+
phoneRecipient,
|
|
116
|
+
{ skipFetch: true },
|
|
117
|
+
{
|
|
118
|
+
identity: {
|
|
119
|
+
addressToVerifiedBy: {
|
|
120
|
+
[mockAccount.toLowerCase()]: 'valora',
|
|
121
|
+
[mockAccount2.toLowerCase()]: null,
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
act(() => {
|
|
127
|
+
store.dispatch(
|
|
128
|
+
updateE164PhoneNumberAddresses(
|
|
129
|
+
{ [mockE164Number]: [mockAccount.toLowerCase(), mockAccount2.toLowerCase()] },
|
|
130
|
+
{}
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(result.current.verifiedAddresses).toEqual([
|
|
136
|
+
{ address: mockAccount.toLowerCase(), verifier: 'valora' },
|
|
137
|
+
])
|
|
138
|
+
})
|
|
139
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { fetchAddressVerification, fetchAddressesAndValidate } from 'src/identity/actions'
|
|
3
|
+
import type { AddressToVerifiedByType } from 'src/identity/reducer'
|
|
4
|
+
import {
|
|
5
|
+
addressToVerifiedBySelector,
|
|
6
|
+
e164NumberToAddressSelector,
|
|
7
|
+
recipientLookupLoadingSelector,
|
|
8
|
+
} from 'src/identity/selectors'
|
|
9
|
+
import { Recipient } from 'src/recipients/recipient'
|
|
10
|
+
import { type Verifier, isKnownVerifier } from 'src/recipients/verifier'
|
|
11
|
+
import { useDispatch, useSelector } from 'src/redux/hooks'
|
|
12
|
+
|
|
13
|
+
export type RecipientLookupStatus = 'unknown' | 'loading' | 'verified' | 'unverified'
|
|
14
|
+
|
|
15
|
+
export interface VerifiedAddressEntry {
|
|
16
|
+
address: string
|
|
17
|
+
verifier: Verifier
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getVerifiedAddresses(
|
|
21
|
+
addresses: readonly string[],
|
|
22
|
+
addressToVerifiedBy: AddressToVerifiedByType
|
|
23
|
+
): VerifiedAddressEntry[] {
|
|
24
|
+
return addresses
|
|
25
|
+
.map((address) => ({ address, verifier: addressToVerifiedBy[address] }))
|
|
26
|
+
.filter((entry): entry is VerifiedAddressEntry => isKnownVerifier(entry.verifier))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Dispatches a verification lookup for `recipient` and returns the status plus any verified
|
|
31
|
+
* addresses linked to the phone number. Pass `skipFetch` when the caller has already triggered
|
|
32
|
+
* a fresh lookup and re-fetching would be wasteful.
|
|
33
|
+
*/
|
|
34
|
+
export function useRecipientLookup(
|
|
35
|
+
recipient: Recipient & { address: string },
|
|
36
|
+
options: { skipFetch?: boolean } = {}
|
|
37
|
+
): { status: RecipientLookupStatus; verifiedAddresses: VerifiedAddressEntry[] } {
|
|
38
|
+
const dispatch = useDispatch()
|
|
39
|
+
const e164PhoneNumber = recipient.e164PhoneNumber
|
|
40
|
+
const recipientAddress = recipient.address.toLowerCase()
|
|
41
|
+
const skipFetch = !!options.skipFetch
|
|
42
|
+
|
|
43
|
+
const e164NumberToAddress = useSelector(e164NumberToAddressSelector)
|
|
44
|
+
const addressToVerifiedBy = useSelector(addressToVerifiedBySelector)
|
|
45
|
+
const recipientLookupLoading = useSelector(recipientLookupLoadingSelector)
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (skipFetch || !e164PhoneNumber) return
|
|
49
|
+
dispatch(fetchAddressesAndValidate(e164PhoneNumber))
|
|
50
|
+
}, [dispatch, e164PhoneNumber, skipFetch])
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (skipFetch || e164PhoneNumber) return
|
|
54
|
+
dispatch(fetchAddressVerification(recipientAddress))
|
|
55
|
+
}, [dispatch, e164PhoneNumber, recipientAddress, skipFetch])
|
|
56
|
+
|
|
57
|
+
const cachedAddresses = e164PhoneNumber ? e164NumberToAddress[e164PhoneNumber] : undefined
|
|
58
|
+
const verifiedAddresses = getVerifiedAddresses(cachedAddresses ?? [], addressToVerifiedBy)
|
|
59
|
+
|
|
60
|
+
const verifier = addressToVerifiedBy[recipientAddress]
|
|
61
|
+
|
|
62
|
+
let status: RecipientLookupStatus
|
|
63
|
+
if (recipientLookupLoading) {
|
|
64
|
+
status = 'loading'
|
|
65
|
+
} else if (isKnownVerifier(verifier)) {
|
|
66
|
+
status = 'verified'
|
|
67
|
+
} else if (verifier === null || cachedAddresses === null) {
|
|
68
|
+
status = 'unverified'
|
|
69
|
+
} else {
|
|
70
|
+
status = 'unknown'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { status, verifiedAddresses }
|
|
74
|
+
}
|