import Clipboard from '@react-native-clipboard/clipboard'
import { act, fireEvent, render, waitFor } from '@testing-library/react-native'
import * as React from 'react'
import { Provider } from 'react-redux'
import { SendEvents } from 'src/analytics/Events'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { SendOrigin } from 'src/analytics/types'
import { fetchAddressVerification, fetchAddressesAndValidate } from 'src/identity/actions'
import { AddressValidationType } from 'src/identity/reducer'
import { RecipientVerificationStatus } from 'src/identity/types'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { RecipientType, getRecipientVerificationStatus } from 'src/recipients/recipient'
import SendSelectRecipient from 'src/send/SendSelectRecipient'
import { createMockStore, getMockStackScreenProps } from 'test/utils'
import {
mockAccount,
mockAccount2,
mockAccount3,
mockAddressRecipient,
mockDisplayNumber2Invite,
mockE164Number2Invite,
mockE164Number3,
mockPhoneRecipientCache,
mockRecipient,
mockRecipient2,
} from 'test/values'
jest.mock('@react-native-clipboard/clipboard')
jest.mock('src/utils/IosVersionUtils')
jest.mock('src/recipients/resolve-id')
jest.mock('src/recipients/recipient', () => ({
...(jest.requireActual('src/recipients/recipient') as any),
getRecipientVerificationStatus: jest.fn(),
}))
jest.mock('react-native-device-info', () => ({ getFontScaleSync: () => 1 }))
const mockScreenProps = ({
defaultTokenIdOverride,
forceTokenId,
}: {
defaultTokenIdOverride?: string
forceTokenId?: boolean
}) =>
getMockStackScreenProps(Screens.SendSelectRecipient, {
defaultTokenIdOverride,
forceTokenId,
})
const defaultStore = {
send: {
recentRecipients: [mockRecipient, mockRecipient2],
},
recipients: {
phoneRecipientCache: mockPhoneRecipientCache,
},
}
const storeWithPhoneVerified = {
...defaultStore,
app: { phoneNumberVerified: true },
}
describe('SendSelectRecipient', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.mocked(Clipboard.getString).mockResolvedValue('')
jest.mocked(Clipboard.hasString).mockResolvedValue(false)
})
it('shows contacts when send to contacts button is pressed and conditions are satisfied', async () => {
const store = createMockStore(storeWithPhoneVerified)
const { getByTestId, queryByTestId } = render(
)
await act(() => {
fireEvent.press(getByTestId('SelectRecipient/Contacts'))
})
expect(getByTestId('SelectRecipient/ContactRecipientPicker')).toBeTruthy()
expect(queryByTestId('SelectRecipient/QR')).toBeFalsy()
expect(queryByTestId('SelectRecipient/Contacts')).toBeFalsy()
expect(queryByTestId('SelectRecipient/GetStarted')).toBeFalsy()
expect(queryByTestId('SelectRecipient/RecentRecipientPicker')).toBeFalsy()
})
it('does not show contacts when send to contacts button is pressed and conditions are not satisfied', async () => {
const store = createMockStore(defaultStore)
const { getByTestId, queryByTestId } = render(
)
await act(() => {
fireEvent.press(getByTestId('SelectRecipient/Contacts'))
})
expect(getByTestId('SelectRecipient/RecentRecipientPicker')).toBeTruthy()
expect(queryByTestId('SelectRecipient/ContactRecipientPicker')).toBeFalsy()
})
it('navigates to QR screen when QR button is pressed', async () => {
const store = createMockStore(defaultStore)
const { getByTestId } = render(
)
fireEvent.press(getByTestId('SelectRecipient/QR'))
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_scan_qr)
expect(navigate).toHaveBeenCalledWith(Screens.QRNavigator, {
screen: Screens.QRScanner,
params: {
defaultTokenIdOverride: undefined,
},
})
})
it('navigates to QR screen with an override when QR button is pressed', async () => {
const store = createMockStore(defaultStore)
const { getByTestId } = render(
)
fireEvent.press(getByTestId('SelectRecipient/QR'))
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_scan_qr)
expect(navigate).toHaveBeenCalledWith(Screens.QRNavigator, {
screen: Screens.QRScanner,
params: {
defaultTokenIdOverride: 'some-token-id',
},
})
})
it('shows QR, sync contacts and get started section when no prior recipients', async () => {
const store = createMockStore({})
const { getByTestId } = render(
)
expect(getByTestId('SelectRecipient/Contacts')).toBeTruthy()
expect(getByTestId('SelectRecipient/QR')).toBeTruthy()
expect(getByTestId('SelectRecipient/GetStarted')).toBeTruthy()
})
it('shows QR, sync contacts and recents when prior recipients exist', async () => {
const store = createMockStore(defaultStore)
const { getByTestId } = render(
)
expect(getByTestId('SelectRecipient/Contacts')).toBeTruthy()
expect(getByTestId('SelectRecipient/QR')).toBeTruthy()
expect(getByTestId('SelectRecipient/RecentRecipientPicker')).toBeTruthy()
})
it('shows search when text is entered and result is present', async () => {
const store = createMockStore(defaultStore)
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, 'John Doe')
})
expect(getByTestId('SelectRecipient/AllRecipientsPicker')).toBeTruthy()
})
it('shows no results available when text is entered and no results', async () => {
const store = createMockStore(defaultStore)
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, 'Fake Name')
})
expect(getByTestId('SelectRecipient/NoResults')).toBeTruthy()
})
it('navigates to send amount when search result next button is pressed', async () => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
const store = createMockStore(storeWithPhoneVerified)
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, 'George Bogart')
})
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
expect(getByTestId('SendOrInviteButton')).toHaveTextContent('sendSelectRecipient.buttons.send')
await act(() => {
fireEvent.press(getByTestId('SendOrInviteButton'))
})
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
recipientType: RecipientType.PhoneNumber,
})
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
isFromScan: false,
defaultTokenIdOverride: undefined,
forceTokenId: undefined,
recipient: expect.any(Object),
origin: SendOrigin.AppSendFlow,
})
})
it('navigates to send amount when address recipient is pressed', async () => {
const store = createMockStore(defaultStore)
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, mockAddressRecipient.address)
})
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
expect(getByTestId('SendOrInviteButton')).toHaveTextContent('sendSelectRecipient.buttons.send')
await act(() => {
fireEvent.press(getByTestId('SendOrInviteButton'))
})
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
recipientType: RecipientType.Address,
})
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
isFromScan: false,
defaultTokenIdOverride: undefined,
forceTokenId: undefined,
recipient: expect.any(Object),
origin: SendOrigin.AppSendFlow,
})
})
it('navigates to invite modal when search result next button is pressed', async () => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
const store = createMockStore(storeWithPhoneVerified)
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, 'George Bogart')
})
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
expect(getByTestId('SendOrInviteButton')).toHaveTextContent(
'sendSelectRecipient.buttons.invite'
)
await act(() => {
fireEvent.press(getByTestId('SendOrInviteButton'))
})
expect(getByTestId('InviteModalContainer')).toBeTruthy()
})
it('does not show unknown address info text when searching for known app address', async () => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
const store = createMockStore(storeWithPhoneVerified)
const { getByTestId, queryByTestId } = render(
)
await waitFor(() => {
expect(getByTestId('SendSelectRecipientSearchInput')).toBeTruthy()
})
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, mockAccount2)
})
expect(getByTestId('RecipientItem')).toHaveTextContent(
'feedItemAddress, {"address":"0x1ff4...bc42"}'
)
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(store.getActions()).toEqual([fetchAddressVerification(mockAccount2.toLowerCase())])
expect(queryByTestId('UnknownAddressInfo')).toBeFalsy()
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
})
it('does not show unknown address info text when searching for phone number', async () => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
const store = createMockStore(storeWithPhoneVerified)
const { getByTestId, queryByTestId } = render(
)
await waitFor(() => {
expect(getByTestId('SendSelectRecipientSearchInput')).toBeTruthy()
})
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, mockE164Number2Invite)
})
expect(getByTestId('RecipientItem')).toHaveTextContent(mockDisplayNumber2Invite)
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(store.getActions()).toEqual([fetchAddressesAndValidate(mockE164Number2Invite)])
expect(queryByTestId('UnknownAddressInfo')).toBeFalsy()
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
})
it('shows unknown address info text when searching for unknown address after making address verification request', async () => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
const store = createMockStore(storeWithPhoneVerified)
const { getByTestId } = render(
)
await waitFor(() => {
expect(getByTestId('SendSelectRecipientSearchInput')).toBeTruthy()
})
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, mockAccount2)
})
// ensure its an address recipient (not an address that's tied to a contact)
expect(getByTestId('RecipientItem')).toHaveTextContent(
'feedItemAddress, {"address":"0x1ff4...bc42"}'
)
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(store.getActions()).toEqual([fetchAddressVerification(mockAccount2.toLowerCase())])
expect(getByTestId('UnknownAddressInfo')).toBeTruthy()
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
})
it('shows unknown address info text and skips CPV request when searching for any address if PN not verified', async () => {
const store = createMockStore(defaultStore)
const { getByTestId } = render(
)
await waitFor(() => {
expect(getByTestId('SendSelectRecipientSearchInput')).toBeTruthy()
})
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, mockAccount2)
})
// ensure its an address recipient (not an address that's tied to a contact)
expect(getByTestId('RecipientItem')).toHaveTextContent(
'feedItemAddress, {"address":"0x1ff4...bc42"}'
)
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(store.getActions()).toEqual([])
expect(getByTestId('UnknownAddressInfo')).toBeTruthy()
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
})
it('shows unknown address info text and send button when searching for address with cached phone number but no longer connected to the phone number', async () => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.UNVERIFIED)
const store = createMockStore(storeWithPhoneVerified)
const { getByTestId } = render(
)
await waitFor(() => {
expect(getByTestId('SendSelectRecipientSearchInput')).toBeTruthy()
})
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, mockAccount)
})
expect(getByTestId('RecipientItem')).toHaveTextContent(mockRecipient.name)
expect(getByTestId('RecipientItem')).toHaveTextContent(mockRecipient.displayNumber)
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(store.getActions()).toEqual([fetchAddressVerification(mockAccount)])
expect(getByTestId('UnknownAddressInfo')).toBeTruthy()
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
expect(getByTestId('SendOrInviteButton')).toHaveTextContent('send')
})
it('shows paste button if clipboard has address content', async () => {
const store = createMockStore(defaultStore)
const { findByTestId } = render(
)
await act(() => {
jest.mocked(Clipboard.getString).mockResolvedValue(mockAccount)
jest.mocked(Clipboard.hasString).mockResolvedValue(true)
})
jest.runOnlyPendingTimers()
const pasteButton = await findByTestId('PasteAddressButton')
expect(pasteButton).toBeTruthy()
await act(() => {
fireEvent.press(pasteButton)
})
const pasteButtonAfterPress = findByTestId('PasteAddressButton')
await expect(pasteButtonAfterPress).rejects.toThrow()
})
describe('Invite Rewards', () => {
it('shows invite rewards card when invite rewards are active and number is verified', async () => {
const store = createMockStore({
...storeWithPhoneVerified,
send: {
...storeWithPhoneVerified.send,
inviteRewardsVersion: 'v5',
},
})
const { findByTestId } = render(
)
const inviteRewardsCard = await findByTestId('InviteRewardsCard')
expect(inviteRewardsCard).toHaveTextContent('inviteRewardsBannerCUSD.title')
expect(inviteRewardsCard).toHaveTextContent('inviteRewardsBannerCUSD.body')
})
it('does not show invite rewards card when invite rewards are not active', async () => {
const store = createMockStore({
...storeWithPhoneVerified,
send: {
...storeWithPhoneVerified.send,
inviteRewardsVersion: 'none',
},
})
const { queryByTestId } = render(
)
expect(queryByTestId('InviteRewardsCard')).toBeFalsy()
})
it('does not show invite rewards card when invite rewards are active and number is not verified', async () => {
const store = createMockStore({
...defaultStore,
send: {
...defaultStore.send,
inviteRewardsVersion: 'v5',
},
})
const { queryByTestId } = render(
)
expect(queryByTestId('InviteRewardsCard')).toBeFalsy()
})
})
it('navigates to send amount when phone number recipient with single address', async () => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
const store = createMockStore({
...storeWithPhoneVerified,
identity: {
secureSendPhoneNumberMapping: {
[mockE164Number3]: { addressValidationType: AddressValidationType.NONE },
},
e164NumberToAddress: { [mockE164Number3]: [mockAccount3] },
},
})
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, mockE164Number3)
})
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
await act(() => {
fireEvent.press(getByTestId('SendOrInviteButton'))
})
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
recipientType: RecipientType.PhoneNumber,
})
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
isFromScan: false,
defaultTokenIdOverride: undefined,
forceTokenId: undefined,
recipient: {
address: mockAccount3,
displayNumber: '(415) 555-0123',
e164PhoneNumber: mockE164Number3,
recipientType: 'PhoneNumber',
},
origin: SendOrigin.AppSendFlow,
})
})
it('navigates to secure send flow when phone number recipient with multiple addresses, first time seeing it', async () => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
const store = createMockStore({
...storeWithPhoneVerified,
identity: {
secureSendPhoneNumberMapping: {
[mockE164Number3]: { addressValidationType: AddressValidationType.PARTIAL },
},
e164NumberToAddress: {
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
},
addressToE164Number: {
[mockAccount2.toLowerCase()]: mockE164Number3,
[mockAccount3.toLowerCase()]: mockE164Number3,
},
},
})
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, mockE164Number3)
})
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
await act(() => {
fireEvent.press(getByTestId('SendOrInviteButton'))
})
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
recipientType: RecipientType.PhoneNumber,
})
expect(navigate).toHaveBeenCalledWith(Screens.ValidateRecipientIntro, {
defaultTokenIdOverride: undefined,
forceTokenId: undefined,
recipient: expect.any(Object),
origin: SendOrigin.AppSendFlow,
})
})
it('navigates to send enter amount when phone number recipient with multiple addresses, already done secure send', async () => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
const store = createMockStore({
...storeWithPhoneVerified,
identity: {
secureSendPhoneNumberMapping: {
[mockE164Number3]: {
addressValidationType: AddressValidationType.NONE,
address: mockAccount3,
},
},
e164NumberToAddress: {
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
},
addressToE164Number: {
[mockAccount2.toLowerCase()]: mockE164Number3,
[mockAccount3.toLowerCase()]: mockE164Number3,
},
},
})
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, mockE164Number3)
})
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
await act(() => {
fireEvent.press(getByTestId('SendOrInviteButton'))
})
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
recipientType: RecipientType.PhoneNumber,
})
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
isFromScan: false,
defaultTokenIdOverride: undefined,
forceTokenId: undefined,
recipient: {
address: mockAccount3,
displayNumber: '(415) 555-0123',
e164PhoneNumber: mockE164Number3,
recipientType: 'PhoneNumber',
},
origin: SendOrigin.AppSendFlow,
})
})
it.each([{ searchAddress: mockAccount2 }, { searchAddress: mockAccount3 }])(
'navigates to send enter amount with correct address if a an address is entered which also has a phone number with secure send not done',
async ({ searchAddress }) => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
const store = createMockStore({
...storeWithPhoneVerified,
identity: {
secureSendPhoneNumberMapping: {
[mockE164Number3]: {
addressValidationType: AddressValidationType.PARTIAL,
},
},
e164NumberToAddress: {
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
},
addressToE164Number: {
[mockAccount2.toLowerCase()]: mockE164Number3,
[mockAccount3.toLowerCase()]: mockE164Number3,
},
},
})
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, searchAddress)
})
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
await act(() => {
fireEvent.press(getByTestId('SendOrInviteButton'))
})
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
recipientType: RecipientType.Address,
})
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
isFromScan: false,
defaultTokenIdOverride: undefined,
forceTokenId: undefined,
recipient: {
address: searchAddress.toLowerCase(),
e164PhoneNumber: mockE164Number3,
recipientType: RecipientType.Address,
contactId: undefined,
displayNumber: undefined,
name: undefined,
thumbnailPath: undefined,
},
origin: SendOrigin.AppSendFlow,
})
}
)
it.each([{ searchAddress: mockAccount2 }, { searchAddress: mockAccount3 }])(
'navigates to send enter amount with correct address if a an address is entered which also has a phone number with secure send done with different address',
async ({ searchAddress }) => {
jest
.mocked(getRecipientVerificationStatus)
.mockReturnValue(RecipientVerificationStatus.VERIFIED)
const store = createMockStore({
...storeWithPhoneVerified,
identity: {
secureSendPhoneNumberMapping: {
[mockE164Number3]: {
addressValidationType: AddressValidationType.NONE,
address: mockAccount3,
},
},
e164NumberToAddress: {
[mockE164Number3]: [mockAccount2.toLowerCase(), mockAccount3.toLowerCase()],
},
addressToE164Number: {
[mockAccount2.toLowerCase()]: mockE164Number3,
[mockAccount3.toLowerCase()]: mockE164Number3,
},
},
})
const { getByTestId } = render(
)
const searchInput = getByTestId('SendSelectRecipientSearchInput')
await act(() => {
fireEvent.changeText(searchInput, searchAddress)
})
await act(() => {
fireEvent.press(getByTestId('RecipientItem'))
})
expect(getByTestId('SendOrInviteButton')).toBeTruthy()
await act(() => {
fireEvent.press(getByTestId('SendOrInviteButton'))
})
expect(AppAnalytics.track).toHaveBeenCalledWith(SendEvents.send_select_recipient_send_press, {
recipientType: RecipientType.Address,
})
expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, {
isFromScan: false,
defaultTokenIdOverride: undefined,
forceTokenId: undefined,
recipient: {
address: searchAddress.toLowerCase(),
e164PhoneNumber: mockE164Number3,
recipientType: RecipientType.Address,
contactId: undefined,
displayNumber: undefined,
name: undefined,
thumbnailPath: undefined,
},
origin: SendOrigin.AppSendFlow,
})
}
)
})