import { render } from '@testing-library/react-native' import * as React from 'react' import 'react-native' import { View } from 'react-native' import { expectSaga } from 'redux-saga-test-plan' import { call, select } from 'redux-saga-test-plan/matchers' import { showError } from 'src/alert/actions' import AppAnalytics from 'src/analytics/AppAnalytics' import { QrScreenEvents } from 'src/analytics/Events' import { HooksEnablePreviewOrigin, SendOrigin } from 'src/analytics/types' import { ErrorMessages } from 'src/app/ErrorMessages' import { DEEP_LINK_URL_SCHEME } from 'src/config' import { e164NumberToAddressSelector, secureSendPhoneNumberMappingSelector, } from 'src/identity/selectors' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { handleEnableHooksPreviewDeepLink } from 'src/positions/saga' import { allowHooksPreviewSelector } from 'src/positions/selectors' import { urlFromUriData } from 'src/qrcode/schema' import { QRCodeTypes, handleQRCodeDefault, handleQRCodeSecureSend, handleSecureSend, useQRContent, } from 'src/qrcode/utils' import { RecipientType } from 'src/recipients/recipient' import { recipientInfoSelector } from 'src/recipients/reducer' import { handleQRCodeDetected, handleQRCodeDetectedSecureSend } from 'src/send/actions' import { QrCode } from 'src/send/types' import { createMockStore } from 'test/utils' import { mockAccount, mockAccount2, mockAccount3, mockE164Number, mockE164Number3, mockE164NumberToAddress, mockEthTokenId, mockName, mockQrCodeData, mockRecipient, mockRecipientInfo, } from 'test/values' jest.mock('src/positions/saga') beforeEach(() => { jest.clearAllMocks() }) describe('useQRContent', () => { const data = { address: mockAccount, displayName: mockName, e164PhoneNumber: mockE164Number, } const MockComponent = (props: { data: { address: string displayName: string | undefined e164PhoneNumber: string | undefined } }) => { const qrCodeString = useQRContent(props.data) return {qrCodeString} } it('returns an address when dataType is address', () => { const { getByTestId } = render() expect(getByTestId('qrCodeString').children[0]).toEqual(data.address) }) }) describe('handleQRCodeDefault', () => { it('handles hooks enable preview links', async () => { const link = `${DEEP_LINK_URL_SCHEME}://wallet/hooks/enablePreview?hooksApiUrl=https://192.168.0.42:18000` const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data: link } await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode })) .provide([[select(allowHooksPreviewSelector), true]]) .run() expect(handleEnableHooksPreviewDeepLink).toHaveBeenCalledWith( link, HooksEnablePreviewOrigin.Scan ) expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, qrCode) }) it('navigates to the send amount screen with a valid QR code', async () => { const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data: urlFromUriData(mockQrCodeData) } await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode })) .withState(createMockStore({}).getState()) .provide([[select(recipientInfoSelector), mockRecipientInfo]]) .run() expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, { origin: SendOrigin.AppSendFlow, isFromScan: true, recipient: { address: mockAccount.toLowerCase(), name: mockName, e164PhoneNumber: mockE164Number, contactId: 'contactId', displayNumber: '14155550000', thumbnailPath: undefined, recipientType: RecipientType.Address, }, forceTokenId: false, }) expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, qrCode) }) it.each([mockAccount, `ethereum:${mockAccount}`, `celo:${mockAccount}`])( 'navigates to the send amount screen with a qr code with address as the data', async (data) => { const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data } await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode })) .withState(createMockStore({}).getState()) .provide([[select(recipientInfoSelector), mockRecipientInfo]]) .run() expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, { origin: SendOrigin.AppSendFlow, isFromScan: true, recipient: { address: mockAccount.toLowerCase(), name: mockName, contactId: 'contactId', displayNumber: '14155550000', thumbnailPath: undefined, recipientType: RecipientType.Address, }, forceTokenId: false, }) expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, qrCode) } ) it('throws an error when the QR code data is invalid', async () => { const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data: mockAccount.replace('0x', '') } await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode })) .withState(createMockStore({}).getState()) .put(showError(ErrorMessages.QR_FAILED_INVALID_ADDRESS)) .run() }) it('navigates to the send amount screen with a qr code with an empty display name and token override', async () => { const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data: urlFromUriData({ address: mockAccount3, e164PhoneNumber: mockE164Number3, }), } await expectSaga( handleQRCodeDefault, handleQRCodeDetected({ qrCode, defaultTokenIdOverride: 'some-token-id' }) ) .withState(createMockStore({}).getState()) .provide([ [select(e164NumberToAddressSelector), {}], [select(recipientInfoSelector), mockRecipientInfo], ]) .silentRun() expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, { origin: SendOrigin.AppSendFlow, isFromScan: true, recipient: { address: mockAccount3.toLowerCase(), e164PhoneNumber: mockE164Number3, contactId: undefined, thumbnailPath: undefined, recipientType: RecipientType.Address, }, defaultTokenIdOverride: 'some-token-id', forceTokenId: false, }) expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, qrCode) }) it('navigates to the send amount screen with a qr code with an empty phone number', async () => { const qrCode: QrCode = { type: QRCodeTypes.QR_CODE, data: urlFromUriData({ address: mockAccount3, displayName: mockQrCodeData.displayName, }), } await expectSaga(handleQRCodeDefault, handleQRCodeDetected({ qrCode })) .withState(createMockStore({}).getState()) .provide([ [select(e164NumberToAddressSelector), {}], [select(recipientInfoSelector), mockRecipientInfo], ]) .silentRun() expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, { origin: SendOrigin.AppSendFlow, isFromScan: true, recipient: { address: mockAccount3.toLowerCase(), name: mockName, displayNumber: undefined, e164PhoneNumber: undefined, contactId: undefined, thumbnailPath: undefined, recipientType: RecipientType.Address, }, forceTokenId: false, }) expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, qrCode) }) }) describe('handleQRCodeSecureSend', () => { it('handles a valid address and navigates to send enter amount when there is no transaction data', async () => { const data: QrCode = { type: QRCodeTypes.QR_CODE, data: mockAccount } await expectSaga( handleQRCodeSecureSend, handleQRCodeDetectedSecureSend(data, mockRecipient, mockAccount2, false, mockEthTokenId) ) .provide([ [select(e164NumberToAddressSelector), mockE164NumberToAddress], [ select(secureSendPhoneNumberMappingSelector), { [mockRecipient.e164PhoneNumber]: { address: mockAccount, addressValidationType: undefined, }, }, ], [ call( handleSecureSend, mockAccount.toLowerCase(), mockE164NumberToAddress, mockRecipient, mockAccount2 ), true, ], ]) .run() expect(navigate).toHaveBeenCalledWith(Screens.SendEnterAmount, { origin: SendOrigin.AppSendFlow, recipient: { ...mockRecipient, address: mockAccount, }, isFromScan: true, forceTokenId: false, defaultTokenIdOverride: mockEthTokenId, }) expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, data) }) it('handles an invalid address', async () => { const data: QrCode = { type: QRCodeTypes.QR_CODE, data: 'invalid-address' } await expectSaga( handleQRCodeSecureSend, handleQRCodeDetectedSecureSend(data, mockRecipient, mockAccount2) ) .provide([[select(e164NumberToAddressSelector), mockE164NumberToAddress]]) .put(showError(ErrorMessages.QR_FAILED_INVALID_ADDRESS)) .run() expect(navigate).not.toHaveBeenCalled() expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, data) }) it('handles failed address lookup', async () => { const data: QrCode = { type: QRCodeTypes.QR_CODE, data: mockAccount } await expectSaga( handleQRCodeSecureSend, handleQRCodeDetectedSecureSend(data, mockRecipient, mockAccount2) ) .provide([ [select(e164NumberToAddressSelector), mockE164NumberToAddress], [ call( handleSecureSend, mockAccount.toLowerCase(), mockE164NumberToAddress, mockRecipient, mockAccount2 ), false, ], ]) .run() expect(navigate).not.toHaveBeenCalled() expect(AppAnalytics.track).toHaveBeenCalledWith(QrScreenEvents.qr_scanned, data) }) })