import { fireEvent, render, waitFor } from '@testing-library/react-native'
import BigNumber from 'bignumber.js'
import React from 'react'
import { Provider } from 'react-redux'
import AppAnalytics from 'src/analytics/AppAnalytics'
import { JumpstartEvents } from 'src/analytics/Events'
import { createJumpstartLink } from 'src/firebase/dynamicLinks'
import JumpstartEnterAmount from 'src/jumpstart/JumpstartEnterAmount'
import { depositTransactionFlowStarted, jumpstartIntroSeen } from 'src/jumpstart/slice'
import { usePrepareJumpstartTransactions } from 'src/jumpstart/usePrepareJumpstartTransactions'
import { navigate } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
import { getDynamicConfigParams } from 'src/statsig'
import { StoredTokenBalance, TokenBalance } from 'src/tokens/slice'
import { TransactionRequest } from 'src/viem/prepareTransactions'
import { getSerializablePreparedTransactions } from 'src/viem/preparedTransactionSerialization'
import { createMockStore } from 'test/utils'
import {
mockAccount,
mockCeloTokenId,
mockCeurTokenId,
mockCusdTokenId,
mockPoofTokenId,
mockTokenBalances,
mockUSDCTokenId,
} from 'test/values'
jest.mock('src/statsig')
jest.mock('src/jumpstart/usePrepareJumpstartTransactions')
jest.mock('viem/accounts', () => ({
...jest.requireActual('viem/accounts'),
generatePrivateKey: jest
.fn()
.mockReturnValue('0x859c770be6bada3b0ae071d5368afaf9eb445584b35d914771dbf351db1e3df3'),
}))
jest.mock('src/firebase/dynamicLinks')
const mockPublicKey = '0x2CEc3C5e83eE37261F9f9BB050B2Fbf59d13eEc0' // matches mock private key
const mockStoreBalancesToTokenBalances = (storeBalances: StoredTokenBalance[]): TokenBalance[] => {
return storeBalances.map(
(token): TokenBalance => ({
...token,
balance: new BigNumber(token.balance ?? 0),
priceUsd: new BigNumber(token.priceUsd ?? 0),
lastKnownPriceUsd: token.priceUsd ? new BigNumber(token.priceUsd) : null,
})
)
}
const tokenBalances = {
[mockCeloTokenId]: { ...mockTokenBalances[mockCeloTokenId], address: null }, // filtered out for no address
[mockCusdTokenId]: { ...mockTokenBalances[mockCusdTokenId], balance: '10' },
[mockUSDCTokenId]: mockTokenBalances[mockUSDCTokenId], // filtered out for networkId
[mockPoofTokenId]: { ...mockTokenBalances[mockPoofTokenId], balance: '0' }, // filtered out for no balance
[mockCeurTokenId]: { ...mockTokenBalances[mockCeurTokenId], balance: '100' },
}
const feeCurrencies = [
tokenBalances[mockCeloTokenId],
tokenBalances[mockCeurTokenId],
tokenBalances[mockCusdTokenId],
]
const store = createMockStore({
tokens: {
tokenBalances,
},
jumpstart: {
introHasBeenSeen: true,
},
})
const executeSpy = jest.fn()
const mockTransactions: TransactionRequest[] = [
{
from: '0xfrom',
to: '0xto',
data: '0xdata',
gas: BigInt(5e15), // 0.005 CELO
maxFeePerGas: BigInt(1),
maxPriorityFeePerGas: undefined,
_baseFeePerGas: BigInt(1),
},
]
jest.mocked(usePrepareJumpstartTransactions).mockReturnValue({
execute: executeSpy,
reset: jest.fn(),
loading: false,
result: {
type: 'possible',
transactions: mockTransactions,
feeCurrency: tokenBalances[mockCeloTokenId],
},
error: undefined,
status: 'not-requested',
} as any)
describe('JumpstartEnterAmount', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.mocked(getDynamicConfigParams).mockReturnValue({
jumpstartContracts: {
'celo-alfajores': {
contractAddress: '0xjumpstart',
},
},
maxAllowedSendAmountUsd: 50,
})
store.clearActions()
})
it('should render only jumpstart tokens', () => {
const { getAllByTestId } = render(
)
const tokens = getAllByTestId('TokenBalanceItem')
expect(tokens).toHaveLength(2)
expect(tokens[0]).toHaveTextContent('cEUR')
expect(tokens[1]).toHaveTextContent('cUSD')
})
it('should prepare transactions with the expected inputs', async () => {
const { getByTestId } = render(
)
fireEvent.changeText(getByTestId('SendEnterAmount/TokenAmountInput'), '.25')
await waitFor(() => expect(executeSpy).toHaveBeenCalledTimes(1))
expect(executeSpy).toHaveBeenCalledWith({
sendTokenAmountInSmallestUnit: new BigNumber('250000000000000000'),
token: mockStoreBalancesToTokenBalances([tokenBalances[mockCeurTokenId]])[0],
walletAddress: mockAccount.toLowerCase(),
publicKey: mockPublicKey,
feeCurrencies: mockStoreBalancesToTokenBalances(feeCurrencies),
})
})
it('should show a blocking warning if the send amount exceeds the threshold', async () => {
const { getByTestId, getByText, queryByText } = render(
)
// default selected token is cEUR, priceUsd: '1.16' so max send amount will be 50 / 1.16 = 43.10
fireEvent.changeText(getByTestId('SendEnterAmount/TokenAmountInput'), '43.5')
await waitFor(() => expect(executeSpy).toHaveBeenCalledTimes(1))
expect(getByText('review')).toBeDisabled()
expect(
getByText(
'jumpstartEnterAmountScreen.maxAmountWarning.title, {"amountInLocalCurrency":"66.5","formatParams":{"amountInLocalCurrency":{"currency":"PHP","locale":"es-419"}}}'
)
).toBeTruthy()
expect(getByText('jumpstartEnterAmountScreen.maxAmountWarning.description')).toBeTruthy()
fireEvent.changeText(getByTestId('SendEnterAmount/TokenAmountInput'), '43')
await waitFor(() => expect(getByText('review')).not.toBeDisabled())
expect(queryByText('jumpstartEnterAmountScreen.maxAmountWarning.title')).toBeFalsy()
})
it('should navigate to the next screen on tap continue', async () => {
const mockLink = 'https://vlra.app/abc123'
jest.mocked(createJumpstartLink).mockResolvedValue(mockLink)
const { getByTestId, getByText } = render(
)
fireEvent.changeText(getByTestId('SendEnterAmount/TokenAmountInput'), '.25')
await waitFor(() => expect(executeSpy).toHaveBeenCalledTimes(1))
fireEvent.press(getByText('review'))
await waitFor(() =>
expect(navigate).toHaveBeenCalledWith(Screens.JumpstartSendConfirmation, {
link: mockLink,
sendAmount: '0.25',
tokenId: mockCeurTokenId,
serializablePreparedTransactions: getSerializablePreparedTransactions(mockTransactions),
beneficiaryAddress: mockPublicKey,
})
)
expect(AppAnalytics.track).toHaveBeenCalledWith(
JumpstartEvents.jumpstart_send_amount_continue,
{
amountInUsd: '0.29',
localCurrency: 'PHP',
localCurrencyExchangeRate: '1.33',
networkId: 'celo-alfajores',
tokenAmount: '0.25',
tokenId: mockCeurTokenId,
tokenSymbol: 'cEUR',
amountEnteredIn: 'token',
}
)
})
it('should block the continue button if there is an in-flight jumpstart transaction', async () => {
const { getByTestId, rerender } = render(
)
expect(store.getActions()).toEqual([depositTransactionFlowStarted()])
fireEvent.changeText(getByTestId('SendEnterAmount/TokenAmountInput'), '.25')
await waitFor(() => expect(executeSpy).toHaveBeenCalledTimes(1))
expect(getByTestId('SendEnterAmount/ReviewButton')).toBeEnabled()
const updatedStore = createMockStore({
tokens: {
tokenBalances,
},
jumpstart: {
depositStatus: 'success',
introHasBeenSeen: true,
},
})
rerender(
)
// depositTransactionFlowStarted should not be dispatched
expect(updatedStore.getActions()).toEqual([])
fireEvent.changeText(getByTestId('SendEnterAmount/TokenAmountInput'), '.30')
// prepare transaction for a second time on this screen
await waitFor(() => expect(executeSpy).toHaveBeenCalledTimes(2))
// review button should remain disabled
expect(getByTestId('SendEnterAmount/ReviewButton')).toBeDisabled()
})
it('should render the add assets flow if the user has no jumpstart tokens', () => {
const { getByText, getByTestId } = render(
)
expect(getByText('jumpstartIntro.title')).toBeTruthy()
expect(getByText('jumpstartIntro.description')).toBeTruthy()
expect(getByText('jumpstartIntro.noFundsHint')).toBeTruthy()
expect(getByText('jumpstartIntro.addFundsCelo.cta')).toBeTruthy()
expect(getByTestId('JumpstartIntro/noFundsButton')).toBeTruthy()
})
it('should show intro screen when user visits jumpstart for the first time', async () => {
const { getByText, getByTestId } = render(
)
expect(getByText('jumpstartIntro.title')).toBeTruthy()
expect(getByText('jumpstartIntro.description')).toBeTruthy()
expect(getByText('jumpstartIntro.haveFundsButton')).toBeTruthy()
expect(getByTestId('JumpstartEnterAmount/haveFundsButton')).toBeTruthy()
})
it('should track in analytics when user sees intro for the first time', async () => {
const updatedStore = createMockStore({
tokens: { tokenBalances },
jumpstart: { introHasBeenSeen: false },
})
const { getByText, getByTestId, rerender } = render(
)
expect(getByText('jumpstartIntro.title')).toBeTruthy()
expect(getByText('jumpstartIntro.description')).toBeTruthy()
expect(getByText('jumpstartIntro.haveFundsButton')).toBeTruthy()
expect(getByTestId('JumpstartEnterAmount/haveFundsButton')).toBeTruthy()
expect(AppAnalytics.track).toHaveBeenCalledWith(JumpstartEvents.jumpstart_intro_seen)
rerender(
)
expect(AppAnalytics.track).toBeCalledTimes(1)
})
it('should show intro screen every time until user clicks cta', async () => {
const updatedStore = createMockStore({
tokens: { tokenBalances },
jumpstart: { introHasBeenSeen: false },
})
const { getByText, getByTestId, rerender } = render(
)
expect(getByText('jumpstartIntro.title')).toBeTruthy()
expect(getByText('jumpstartIntro.description')).toBeTruthy()
expect(getByText('jumpstartIntro.haveFundsButton')).toBeTruthy()
expect(getByTestId('JumpstartEnterAmount/haveFundsButton')).toBeTruthy()
rerender(
)
// do not expect for jumpstartIntroSeen() to run
expect(updatedStore.getActions()).toEqual([depositTransactionFlowStarted()])
expect(getByText('jumpstartIntro.title')).toBeTruthy()
expect(getByText('jumpstartIntro.description')).toBeTruthy()
expect(getByText('jumpstartIntro.haveFundsButton')).toBeTruthy()
expect(getByTestId('JumpstartEnterAmount/haveFundsButton')).toBeTruthy()
})
it('should proceed to enter amount screen once the intro is seen', async () => {
let updatedStore = createMockStore({
tokens: { tokenBalances },
jumpstart: { introHasBeenSeen: false },
})
const { getByText, getByTestId, rerender } = render(
)
expect(getByText('jumpstartIntro.title')).toBeTruthy()
expect(getByText('jumpstartIntro.description')).toBeTruthy()
expect(getByText('jumpstartIntro.haveFundsButton')).toBeTruthy()
expect(getByTestId('JumpstartEnterAmount/haveFundsButton')).toBeTruthy()
fireEvent.press(getByTestId('JumpstartEnterAmount/haveFundsButton'))
expect(updatedStore.getActions()).toEqual([
depositTransactionFlowStarted(),
jumpstartIntroSeen(),
])
updatedStore = createMockStore({
tokens: { tokenBalances },
jumpstart: { introHasBeenSeen: true },
})
rerender(
)
expect(getByTestId('SendEnterAmount/TokenAmountInput')).toBeTruthy()
})
})