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 { EarnEvents } from 'src/analytics/Events' import EarnConfirmationScreen from 'src/earn/EarnConfirmationScreen' import { prepareClaimTransactions, prepareWithdrawAndClaimTransactions, prepareWithdrawTransactions, } from 'src/earn/prepareTransactions' import { withdrawStart } from 'src/earn/slice' import { isGasSubsidizedForNetwork } from 'src/earn/utils' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { getFeatureGate } from 'src/statsig' import { StatsigFeatureGates } from 'src/statsig/types' import { NetworkId } from 'src/transactions/types' import { PreparedTransactionsPossible } from 'src/viem/prepareTransactions' import { getSerializablePreparedTransactions } from 'src/viem/preparedTransactionSerialization' import MockedNavigator from 'test/MockedNavigator' import { createMockStore, mockStoreBalancesToTokenBalances } from 'test/utils' import { mockAaveArbUsdcAddress, mockAaveArbUsdcTokenId, mockAccount, mockArbArbTokenId, mockArbEthTokenId, mockArbUsdcTokenId, mockEarnPositions, mockPositions, mockRewardsPositions, mockTokenBalances, } from 'test/values' const mockStoreTokens = { tokenBalances: { ...mockTokenBalances, [mockAaveArbUsdcTokenId]: { networkId: NetworkId['arbitrum-sepolia'], address: mockAaveArbUsdcAddress, tokenId: mockAaveArbUsdcTokenId, symbol: 'aArbSepUSDC', priceUsd: '1', balance: '10.75', priceFetchedAt: Date.now(), }, }, } const store = createMockStore({ tokens: mockStoreTokens, positions: { positions: [...mockPositions, ...mockRewardsPositions], }, }) jest.mock('src/statsig') jest.mock('src/earn/utils', () => ({ ...(jest.requireActual('src/earn/utils') as any), isGasSubsidizedForNetwork: jest.fn(), })) jest.mock('src/earn/prepareTransactions') const mockPreparedTransaction: PreparedTransactionsPossible = { type: 'possible' as const, transactions: [ { from: '0xfrom', to: '0xto', data: '0xdata', gas: BigInt(5e16), _baseFeePerGas: BigInt(1), maxFeePerGas: BigInt(1), maxPriorityFeePerGas: undefined, }, { from: '0xfrom', to: '0xto', data: '0xdata', gas: BigInt(1e16), _baseFeePerGas: BigInt(1), maxFeePerGas: BigInt(1), maxPriorityFeePerGas: undefined, }, ], feeCurrency: { ...mockTokenBalances[mockArbEthTokenId], balance: new BigNumber(10), priceUsd: new BigNumber(1), lastKnownPriceUsd: new BigNumber(1), }, } describe('EarnConfirmationScreen', () => { beforeEach(() => { jest.clearAllMocks() jest.mocked(prepareWithdrawAndClaimTransactions).mockResolvedValue(mockPreparedTransaction) jest.mocked(prepareClaimTransactions).mockResolvedValue(mockPreparedTransaction) jest.mocked(prepareWithdrawTransactions).mockResolvedValue(mockPreparedTransaction) jest .mocked(getFeatureGate) .mockImplementation( (gateName: StatsigFeatureGates) => gateName === StatsigFeatureGates.SHOW_POSITIONS ) jest.mocked(isGasSubsidizedForNetwork).mockReturnValue(false) store.clearActions() }) it('renders total balance, rewards and gas after fetching rewards and preparing tx', async () => { const { getByText, getByTestId, queryByTestId } = render( ) expect(getByText('earnFlow.collect.titleCollect')).toBeTruthy() expect(getByText('earnFlow.collect.total')).toBeTruthy() expect(getByTestId(`EarnConfirmation/${mockArbUsdcTokenId}/CryptoAmount`)).toHaveTextContent( '11.83 USDC' ) expect(getByTestId(`EarnConfirmation/${mockArbUsdcTokenId}/FiatAmount`)).toHaveTextContent( '₱15.73' ) expect(getByTestId('EarnConfirmation/GasLoading')).toBeTruthy() expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled() expect(getByText('earnFlow.collect.reward')).toBeTruthy() expect(getByTestId(`EarnConfirmation/${mockArbArbTokenId}/CryptoAmount`)).toHaveTextContent( '0.01 ARB' ) expect(getByTestId(`EarnConfirmation/${mockArbArbTokenId}/FiatAmount`)).toHaveTextContent( '₱0.016' ) await waitFor(() => { expect(queryByTestId('EarnConfirmation/GasLoading')).toBeFalsy() }) expect(getByTestId('EarnConfirmation/GasFeeCryptoAmount')).toHaveTextContent('0.06 ETH') expect(getByTestId('EarnConfirmation/GasFeeFiatAmount')).toHaveTextContent('₱119.70') expect(queryByTestId('EarnConfirmation/GasSubsidized')).toBeFalsy() expect(getByTestId('EarnConfirmationScreen/CTA')).toBeEnabled() expect(prepareWithdrawAndClaimTransactions).toHaveBeenCalledWith({ feeCurrencies: mockStoreBalancesToTokenBalances([mockTokenBalances[mockArbEthTokenId]]), pool: { ...mockEarnPositions[0], balance: '10.75' }, rewardsPositions: [mockRewardsPositions[1]], walletAddress: mockAccount.toLowerCase(), hooksApiUrl: 'https://api.alfajores.valora.xyz/hooks-api', amount: '10.75', useMax: true, }) expect(store.getActions()).toEqual([]) }) it('renders total balance, rewards and gas after fetching rewards and preparing tx for partial withdrawal', async () => { const inputAmount = (10.75 * +mockEarnPositions[0].pricePerShare) / 2 // Input amount is half of the balance const txAmount = '5.37500000000000045455' // inputAmount divided by pricePerShare but with more precision const { getByText, getByTestId, queryByTestId, queryByText } = render( ) expect(getByText('earnFlow.collect.titleWithdraw')).toBeTruthy() expect(getByText('earnFlow.collect.total')).toBeTruthy() expect(getByTestId(`EarnConfirmation/${mockArbUsdcTokenId}/CryptoAmount`)).toHaveTextContent( '5.91 USDC' ) expect(getByTestId(`EarnConfirmation/${mockArbUsdcTokenId}/FiatAmount`)).toHaveTextContent( '₱7.86' ) expect(queryByText('earnFlow.collect.reward')).toBeFalsy() await waitFor(() => { expect(queryByTestId('EarnConfirmation/GasLoading')).toBeFalsy() }) expect(getByTestId('EarnConfirmation/GasFeeCryptoAmount')).toHaveTextContent('0.06 ETH') expect(getByTestId('EarnConfirmation/GasFeeFiatAmount')).toHaveTextContent('₱119.70') expect(queryByTestId('EarnConfirmation/GasSubsidized')).toBeFalsy() expect(getByTestId('EarnConfirmationScreen/CTA')).toBeEnabled() expect(prepareWithdrawTransactions).toHaveBeenCalledWith({ feeCurrencies: mockStoreBalancesToTokenBalances([mockTokenBalances[mockArbEthTokenId]]), pool: { ...mockEarnPositions[0], balance: '10.75' }, rewardsPositions: [mockRewardsPositions[1]], walletAddress: mockAccount.toLowerCase(), hooksApiUrl: 'https://api.alfajores.valora.xyz/hooks-api', amount: txAmount, }) expect(queryByText('earnFlow.collect.reward')).toBeFalsy() expect(store.getActions()).toEqual([]) }) it('renders rewards and gas after fetching rewards and preparing tx for claim rewards', async () => { const { getByText, getByTestId, queryByTestId } = render( ) expect(getByText('earnFlow.collect.titleClaim')).toBeTruthy() expect(getByTestId('EarnConfirmation/GasLoading')).toBeTruthy() expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled() expect(getByText('earnFlow.collect.reward')).toBeTruthy() expect(getByTestId(`EarnConfirmation/${mockArbArbTokenId}/CryptoAmount`)).toHaveTextContent( '0.01 ARB' ) expect(getByTestId(`EarnConfirmation/${mockArbArbTokenId}/FiatAmount`)).toHaveTextContent( '₱0.016' ) await waitFor(() => { expect(queryByTestId('EarnConfirmation/GasLoading')).toBeFalsy() }) expect(getByTestId('EarnConfirmation/GasFeeCryptoAmount')).toHaveTextContent('0.06 ETH') expect(getByTestId('EarnConfirmation/GasFeeFiatAmount')).toHaveTextContent('₱119.70') expect(queryByTestId('EarnConfirmation/GasSubsidized')).toBeFalsy() expect(getByTestId('EarnConfirmationScreen/CTA')).toBeEnabled() expect(prepareClaimTransactions).toHaveBeenCalledWith({ feeCurrencies: mockStoreBalancesToTokenBalances([mockTokenBalances[mockArbEthTokenId]]), pool: { ...mockEarnPositions[0], balance: '10.75' }, walletAddress: mockAccount.toLowerCase(), hooksApiUrl: 'https://api.alfajores.valora.xyz/hooks-api', amount: '10.75', useMax: true, rewardsPositions: [mockRewardsPositions[1]], }) expect(store.getActions()).toEqual([]) }) it('skips rewards section when no rewards', async () => { const { getByText, getByTestId, queryByTestId, queryByText } = render( position.positionId !== 'arbitrum-sepolia:0x460b97bd498e1157530aeb3086301d5225b91216:supply-incentives' ), }, })} > ) expect(getByText('earnFlow.collect.titleWithdraw')).toBeTruthy() expect(getByText('earnFlow.collect.total')).toBeTruthy() expect(getByTestId(`EarnConfirmation/${mockArbUsdcTokenId}/CryptoAmount`)).toHaveTextContent( '11.83 USDC' ) expect(getByTestId(`EarnConfirmation/${mockArbUsdcTokenId}/FiatAmount`)).toHaveTextContent( '₱15.73' ) expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled() expect(queryByText('earnFlow.collect.reward')).toBeFalsy() expect(queryByTestId(`EarnConfirmation/${mockArbArbTokenId}/CryptoAmount`)).toBeFalsy() expect(queryByTestId(`EarnConfirmation/${mockArbArbTokenId}/FiatAmount`)).toBeFalsy() await waitFor(() => { expect(queryByTestId('EarnConfirmationScreen/CTA')).toBeEnabled() }) await waitFor(() => { expect(queryByTestId('EarnConfirmation/GasLoading')).toBeFalsy() }) }) it('shows error and keeps cta disabled if prepare tx fails', async () => { jest.mocked(prepareWithdrawTransactions).mockRejectedValue(new Error('Failed to prepare')) const { getByText, getByTestId, queryByTestId } = render( ) expect(getByText('earnFlow.collect.titleWithdraw')).toBeTruthy() expect(getByText('earnFlow.collect.total')).toBeTruthy() expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled() await waitFor(() => { expect(queryByTestId('EarnConfirmation/GasLoading')).toBeFalsy() }) expect(getByText('earnFlow.collect.errorTitle')).toBeTruthy() expect(getByTestId('EarnConfirmation/GasError')).toBeTruthy() expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled() }) it('disables cta if not enough balance for gas', async () => { jest.mocked(prepareWithdrawTransactions).mockResolvedValue({ type: 'not-enough-balance-for-gas', feeCurrencies: [mockPreparedTransaction.feeCurrency], }) const { getByText, getByTestId, queryByTestId } = render( ) expect(getByText('earnFlow.collect.titleWithdraw')).toBeTruthy() expect(getByText('earnFlow.collect.total')).toBeTruthy() expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled() await waitFor(() => { expect(queryByTestId('EarnConfirmation/GasLoading')).toBeFalsy() }) expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled() expect(getByTestId('EarnConfirmation/GasError')).toBeTruthy() }) it('pressing cta dispatches withdraw action and fires analytics event', async () => { const { getByTestId } = render( ) await waitFor(() => { expect(getByTestId('EarnConfirmationScreen/CTA')).toBeEnabled() }) fireEvent.press(getByTestId('EarnConfirmationScreen/CTA')) expect(store.getActions()).toEqual([ { type: withdrawStart.type, payload: { amount: '11.825', pool: { ...mockEarnPositions[0], balance: '10.75' }, preparedTransactions: getSerializablePreparedTransactions( mockPreparedTransaction.transactions ), rewardsTokens: mockRewardsPositions[1].tokens, mode: 'withdraw', }, }, ]) expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_collect_earnings_press, { depositTokenId: mockArbUsdcTokenId, tokenAmount: '11.825', networkId: NetworkId['arbitrum-sepolia'], providerId: mockEarnPositions[0].appId, rewards: [{ amount: '0.01', tokenId: mockArbArbTokenId }], poolId: mockEarnPositions[0].positionId, mode: 'withdraw', }) }) it('disables cta and shows loading spinner when withdraw is submitted', async () => { const store = createMockStore({ earn: { withdrawStatus: 'loading' }, tokens: mockStoreTokens, positions: { positions: [...mockPositions, ...mockRewardsPositions], }, }) const { getByTestId, queryByTestId } = render( ) await waitFor(() => { expect(queryByTestId('EarnConfirmation/GasLoading')).toBeFalsy() }) expect(getByTestId('EarnConfirmationScreen/CTA')).toBeDisabled() expect(getByTestId('EarnConfirmationScreen/CTA')).toContainElement( getByTestId('Button/Loading') ) }) it('navigate and fire analytics on no gas CTA press', async () => { jest.mocked(prepareWithdrawTransactions).mockResolvedValue({ type: 'not-enough-balance-for-gas', feeCurrencies: [mockPreparedTransaction.feeCurrency], }) const { getByText, queryByTestId } = render( ) await waitFor(() => { expect(queryByTestId('EarnConfirmation/RewardsLoading')).toBeFalsy() }) expect( getByText('earnFlow.collect.noGasCta, {"symbol":"ETH","network":"Arbitrum Sepolia"}') ).toBeTruthy() fireEvent.press( getByText('earnFlow.collect.noGasCta, {"symbol":"ETH","network":"Arbitrum Sepolia"}') ) expect(navigate).toBeCalledWith(Screens.FiatExchangeAmount, { flow: 'CashIn', tokenId: mockArbEthTokenId, tokenSymbol: 'ETH', }) expect(AppAnalytics.track).toBeCalledWith(EarnEvents.earn_withdraw_add_gas_press, { gasTokenId: mockArbEthTokenId, networkId: NetworkId['arbitrum-sepolia'], poolId: mockEarnPositions[0].positionId, providerId: mockEarnPositions[0].appId, depositTokenId: mockArbUsdcTokenId, }) }) it('shows gas subsidized copy when feature gate is true', async () => { jest.mocked(isGasSubsidizedForNetwork).mockReturnValue(true) const { getByTestId } = render( ) expect(getByTestId('EarnConfirmation/GasSubsidized')).toBeTruthy() }) it.each([ ['claim-rewards', 'earnFlow.collect.titleClaim'], ['withdraw', 'earnFlow.collect.titleWithdraw'], ['exit', 'earnFlow.collect.titleCollect'], ])('shows correct header text for %s', async (mode, expectedHeader) => { const { getByText } = render( ) expect(getByText(expectedHeader)).toBeTruthy() }) })