import { fireEvent, render, waitFor } from '@testing-library/react-native'
import { FetchMock } from 'jest-fetch-mock/types'
import * as React from 'react'
import { Provider } from 'react-redux'
import { MockStoreEnhanced } from 'redux-mock-store'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { FiatExchangeEvents } from 'src/analytics/Events'
import SelectProviderScreen from 'src/fiatExchanges/SelectProvider'
import {
CICOFlow,
PaymentMethod,
SelectProviderExchangesLink,
SelectProviderExchangesText,
} from 'src/fiatExchanges/types'
import {
LegacyMobileMoneyProvider,
fetchCicoQuotes,
fetchExchanges,
fetchLegacyMobileMoneyProviders,
getProviderSelectionAnalyticsData,
} from 'src/fiatExchanges/utils'
import { LocalCurrencyCode } from 'src/localCurrency/consts'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { getExperimentParams, getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import { NetworkId } from 'src/transactions/types'
import { CiCoCurrency } from 'src/utils/currencies'
import { createMockStore, getMockStackScreenProps } from 'test/utils'
import {
mockAccount,
mockCicoQuotes,
mockCusdTokenId,
mockExchanges,
mockFiatConnectQuotes,
} from 'test/values'
import mocked = jest.mocked
const AMOUNT_TO_CASH_IN = 100
const MOCK_IP_ADDRESS = '1.1.1.7'
const FAKE_APP_ID = 'fake app id'
jest.mock('./utils', () => ({
...(jest.requireActual('./utils') as any),
fetchCicoQuotes: jest.fn(),
fetchLegacyMobileMoneyProviders: jest.fn(),
fetchExchanges: jest.fn(),
getProviderSelectionAnalyticsData: jest.fn(),
}))
jest.mock('src/firebase/firebase', () => ({
readOnceFromFirebase: jest.fn().mockResolvedValue(FAKE_APP_ID),
}))
jest.mock('src/statsig', () => ({
getExperimentParams: jest.fn(),
getFeatureGate: jest.fn(),
getDynamicConfigParams: jest.fn().mockReturnValue({
links: {
funding: 'https://www.example.com/funding',
},
}),
}))
jest.mock('src/localCurrency/selectors', () => ({
...(jest.requireActual('src/localCurrency/selectors') as any),
getDefaultLocalCurrencyCode: jest.fn().mockReturnValue('MXN'),
}))
const mockLegacyProviders: LegacyMobileMoneyProvider[] = [
{
name: 'CryptoProvider',
celo: {
cashIn: true,
cashOut: true,
countries: ['MX'],
url: 'https://www.fakecryptoprovider.com/celo',
},
cusd: {
cashIn: true,
cashOut: true,
countries: ['MX'],
url: 'https://www.fakecryptoprovider.com/celo',
},
},
]
const mockScreenProps = (flow: CICOFlow = CICOFlow.CashIn, tokenId: string = mockCusdTokenId) =>
getMockStackScreenProps(Screens.SelectProvider, {
flow,
tokenId,
amount: {
crypto: AMOUNT_TO_CASH_IN,
fiat: AMOUNT_TO_CASH_IN,
},
})
const MOCK_STORE_DATA = {
localCurrency: {
preferredCurrencyCode: LocalCurrencyCode.USD,
},
networkInfo: {
userLocationData: {
countryCodeAlpha2: 'MX',
region: null,
ipAddress: MOCK_IP_ADDRESS,
},
},
web3: {
account: mockAccount,
},
fiatConnect: {
quotesError: null,
quotesLoading: false,
quotes: [],
},
}
describe(SelectProviderScreen, () => {
const mockFetch = fetch as FetchMock
let mockStore: MockStoreEnhanced
beforeEach(() => {
jest.useRealTimers()
jest.clearAllMocks()
mockFetch.resetMocks()
mockStore = createMockStore(MOCK_STORE_DATA)
jest.mocked(getExperimentParams).mockReturnValue({
addFundsExchangesText: SelectProviderExchangesText.CryptoExchange,
addFundsExchangesLink: SelectProviderExchangesLink.ExternalExchangesScreen,
})
jest.mocked(getFeatureGate).mockReturnValue(false)
})
it('calls fetchCicoQuotes correctly', async () => {
render(
)
await waitFor(() =>
expect(fetchCicoQuotes).toHaveBeenCalledWith({
fiatAmount: AMOUNT_TO_CASH_IN.toString(),
fiatCurrency: LocalCurrencyCode.USD,
txType: 'cashIn',
userLocation: {
countryCodeAlpha2: 'MX',
region: null,
ipAddress: MOCK_IP_ADDRESS,
},
address: mockAccount.toLowerCase(),
tokenId: mockCusdTokenId,
})
)
})
it('calls fetchExchanges correctly', async () => {
render(
)
await waitFor(() => expect(fetchExchanges).toHaveBeenCalledWith('MX', mockCusdTokenId))
})
it('shows an additional disclaimer for UK compliance', async () => {
jest
.mocked(getFeatureGate)
.mockImplementation((feature) => feature === StatsigFeatureGates.SHOW_UK_COMPLIANT_VARIANT)
const { getByText } = render(
)
await waitFor(() => expect(getByText('selectProviderScreen.disclaimerUK')).toBeTruthy())
})
it('shows spinner and avoids publishing analytics event if quotes still loading', async () => {
const { getByTestId } = render(
)
expect(getByTestId('QuotesLoading')).toBeTruthy()
expect(AppAnalytics.track).not.toHaveBeenCalled()
})
it('publishes analytics event if quotes done loading', async () => {
const mockAnalyticsData = {
centralizedExchangesAvailable: true,
totalOptions: 1,
paymentMethodsAvailable: {
[PaymentMethod.Bank]: false,
[PaymentMethod.Card]: false,
[PaymentMethod.MobileMoney]: false,
[PaymentMethod.FiatConnectMobileMoney]: false,
[PaymentMethod.Airtime]: false,
},
transferCryptoAmount: 100,
cryptoType: CiCoCurrency.cUSD,
lowestFeeKycRequired: undefined,
lowestFeePaymentMethod: undefined,
lowestFeeProvider: undefined,
lowestFeeCryptoAmount: undefined,
networkId: NetworkId['celo-alfajores'],
}
mocked(getProviderSelectionAnalyticsData).mockReturnValue(mockAnalyticsData)
render(
)
await waitFor(() =>
expect(AppAnalytics.track).toHaveBeenCalledWith(
FiatExchangeEvents.cico_providers_fetch_quotes_result,
{
...mockAnalyticsData,
transferCryptoAmount: undefined,
fiatType: LocalCurrencyCode.USD,
defaultFiatType: LocalCurrencyCode.MXN,
flow: CICOFlow.CashIn,
cryptoAmount: undefined,
fiatAmount: AMOUNT_TO_CASH_IN,
}
)
)
})
it('shows the provider sections (bank, card, mobile money, airtime), legacy mobile money, and exchange section', async () => {
jest.mocked(fetchCicoQuotes).mockResolvedValue({ quotes: mockCicoQuotes })
jest.mocked(fetchLegacyMobileMoneyProviders).mockResolvedValue(mockLegacyProviders)
jest.mocked(fetchExchanges).mockResolvedValue(mockExchanges)
mockStore = createMockStore({
...MOCK_STORE_DATA,
fiatConnect: {
quotesError: null,
quotesLoading: false,
quotes: [mockFiatConnectQuotes[4]],
},
})
const { queryByText, getByTestId, getByText } = render(
)
await waitFor(() => expect(fetchLegacyMobileMoneyProviders).toHaveBeenCalled())
expect(getByText('selectProviderScreen.bank')).toBeTruthy()
expect(getByText('selectProviderScreen.card')).toBeTruthy()
expect(getByText('selectProviderScreen.mobileMoney')).toBeTruthy()
expect(getByText('selectProviderScreen.airtime')).toBeTruthy()
expect(getByTestId('Exchanges')).toBeTruthy()
expect(getByTestId('LegacyMobileMoneySection')).toBeTruthy()
expect(queryByText('selectProviderScreen.somePaymentsUnavailable')).toBeFalsy()
expect(getByText('selectProviderScreen.disclaimer')).toBeTruthy()
})
it('shows you will pay fiat amount for cash ins', async () => {
jest.mocked(fetchCicoQuotes).mockResolvedValue({ quotes: mockCicoQuotes })
jest.mocked(fetchLegacyMobileMoneyProviders).mockResolvedValue(mockLegacyProviders)
jest.mocked(fetchExchanges).mockResolvedValue(mockExchanges)
jest.mocked(getFeatureGate).mockReturnValue(true)
mockStore = createMockStore({
...MOCK_STORE_DATA,
fiatConnect: {
quotesError: null,
quotesLoading: false,
quotes: [mockFiatConnectQuotes[4]],
},
})
const { getByTestId, getByText } = render(
)
await waitFor(() => expect(fetchLegacyMobileMoneyProviders).toHaveBeenCalled())
expect(getByTestId('AmountSpentInfo')).toBeTruthy()
expect(getByText(/selectProviderScreen.cashIn.amountSpentInfo/)).toBeTruthy()
expect(getByTestId('AmountSpentInfo/Fiat/value')).toBeTruthy()
})
it('shows you will withdraw crypto amount for cash outs', async () => {
jest.mocked(fetchCicoQuotes).mockResolvedValue({ quotes: mockCicoQuotes })
jest.mocked(fetchLegacyMobileMoneyProviders).mockResolvedValue(mockLegacyProviders)
jest.mocked(fetchExchanges).mockResolvedValue(mockExchanges)
jest.mocked(getFeatureGate).mockReturnValue(true)
mockStore = createMockStore({
...MOCK_STORE_DATA,
fiatConnect: {
quotesError: null,
quotesLoading: false,
quotes: [mockFiatConnectQuotes[4]],
},
})
const { getByTestId, getByText } = render(
)
await waitFor(() => expect(fetchLegacyMobileMoneyProviders).toHaveBeenCalled())
expect(getByTestId('AmountSpentInfo')).toBeTruthy()
expect(getByText(/selectProviderScreen.cashOut.amountSpentInfo/)).toBeTruthy()
expect(getByTestId('AmountSpentInfo/Crypto')).toBeTruthy()
})
it('shows the limit payment methods dialog when one of the provider types has no options', async () => {
jest.mocked(fetchCicoQuotes).mockResolvedValue({ quotes: [] })
jest.mocked(fetchLegacyMobileMoneyProviders).mockResolvedValue(mockLegacyProviders)
jest.mocked(fetchExchanges).mockResolvedValue(mockExchanges)
const { getByText } = render(
)
await waitFor(() => expect(fetchLegacyMobileMoneyProviders).toHaveBeenCalled())
// Visible because there are no card providers
expect(getByText('selectProviderScreen.disclaimerWithSomePaymentsUnavailable')).toBeTruthy()
})
it('does not show exchange section if no exchanges', async () => {
jest.mocked(fetchCicoQuotes).mockResolvedValue({ quotes: mockCicoQuotes })
jest.mocked(fetchLegacyMobileMoneyProviders).mockResolvedValue(mockLegacyProviders)
jest.mocked(fetchExchanges).mockResolvedValue([])
const { queryByText } = render(
)
await waitFor(() => expect(fetchLegacyMobileMoneyProviders).toHaveBeenCalled())
// Exchange card is not visible as no exchanges are available
expect(queryByText('selectProviderScreen.cryptoExchange')).toBeFalsy()
})
it('shows no payment screen when no providers or exchanges are available', async () => {
jest.mocked(fetchCicoQuotes).mockResolvedValue({ quotes: [] })
jest.mocked(fetchLegacyMobileMoneyProviders).mockResolvedValue([])
jest.mocked(fetchExchanges).mockResolvedValue([])
const { queryByText, getByTestId } = render(
)
await waitFor(() => expect(fetchLegacyMobileMoneyProviders).toHaveBeenCalled())
// Only no payment method screen components are visible when no exchanges or providers are available
expect(getByTestId('NoPaymentMethods')).toBeTruthy()
expect(queryByText('selectProviderScreen.bank')).toBeFalsy()
expect(queryByText('selectProviderScreen.card')).toBeFalsy()
expect(queryByText('selectProviderScreen.cryptoExchange')).toBeFalsy()
expect(queryByText('selectProviderScreen.mobileMoney')).toBeFalsy()
})
describe('Exchanges section', () => {
beforeAll(() => {
jest.mocked(fetchCicoQuotes).mockResolvedValue({ quotes: [] })
jest.mocked(fetchLegacyMobileMoneyProviders).mockResolvedValue([])
jest.mocked(fetchExchanges).mockResolvedValue(mockExchanges)
})
it('renders for cash outs', async () => {
const { queryByText, getByText, getByTestId } = render(
)
await waitFor(() => expect(fetchLegacyMobileMoneyProviders).toHaveBeenCalled())
expect(getByTestId('Exchanges')).toBeTruthy()
expect(queryByText('selectProviderScreen.cryptoExchange')).toBeTruthy()
expect(queryByText('selectProviderScreen.feesVary')).toBeTruthy()
expect(queryByText('selectProviderScreen.viewExchanges')).toBeTruthy()
expect(queryByText('selectProviderScreen.depositFrom')).toBeFalsy()
expect(queryByText('selectProviderScreen.cryptoExchangeOrWallet')).toBeFalsy()
expect(getExperimentParams).not.toHaveBeenCalled()
fireEvent.press(getByText('selectProviderScreen.viewExchanges'))
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.ExternalExchanges, {
tokenId: mockCusdTokenId,
exchanges: mockExchanges,
})
})
it('renders for cash ins', async () => {
const { queryByText, getByText, getByTestId } = render(
)
await waitFor(() => expect(fetchLegacyMobileMoneyProviders).toHaveBeenCalled())
expect(getByTestId('Exchanges')).toBeTruthy()
expect(queryByText('selectProviderScreen.cryptoExchange')).toBeFalsy()
expect(queryByText('selectProviderScreen.feesVary')).toBeFalsy()
expect(queryByText('selectProviderScreen.viewExchanges')).toBeFalsy()
expect(queryByText('selectProviderScreen.depositFrom')).toBeTruthy()
expect(queryByText('selectProviderScreen.cryptoExchangeOrWallet')).toBeTruthy()
fireEvent.press(getByText('selectProviderScreen.cryptoExchangeOrWallet'))
expect(navigate).toHaveBeenCalledTimes(1)
expect(navigate).toHaveBeenCalledWith(Screens.ExchangeQR, {
flow: CICOFlow.CashIn,
exchanges: mockExchanges,
})
})
})
})