Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 61 additions & 125 deletions app/components/UI/Earn/hooks/useMusdConfirmNavigation.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { StackActions } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import Routes from '../../../../constants/navigation/Routes';
import { useMusdConfirmNavigation } from './useMusdConfirmNavigation';
import { useMusdConversionTokens } from './useMusdConversionTokens';
import { useTransactionPayIsMaxAmount } from '../../../Views/confirmations/hooks/pay/useTransactionPayData';
import { AssetType } from '../../../Views/confirmations/types/token';
import { selectMoneyHubEnabledFlag } from '../../Money/selectors/featureFlags';

const mockNavigate = jest.fn();
const mockGoBack = jest.fn();
const mockCanGoBack = jest.fn();
const mockDispatch = jest.fn();
const mockGetState = jest.fn();
const mockGetParent = jest.fn();

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
Expand All @@ -19,156 +19,92 @@ jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: () => ({
navigate: mockNavigate,
goBack: mockGoBack,
canGoBack: mockCanGoBack,
getParent: mockGetParent,
}),
}));

jest.mock(
'../../../Views/confirmations/hooks/pay/useTransactionPayData',
() => ({
useTransactionPayIsMaxAmount: jest.fn(),
}),
);

jest.mock('./useMusdConversionTokens');

const mockUseTransactionPayIsMaxAmount =
useTransactionPayIsMaxAmount as jest.MockedFunction<
typeof useTransactionPayIsMaxAmount
>;
const mockUseMusdConversionTokens =
useMusdConversionTokens as jest.MockedFunction<
typeof useMusdConversionTokens
>;

const createTokenWithBalance = (
overrides: Partial<AssetType> = {},
): AssetType =>
({
address: '0xToken1',
chainId: '0x1',
symbol: 'USDC',
rawBalance: '0x1000',
...overrides,
}) as AssetType;
const mockUseSelector = useSelector as jest.Mock;

describe('useMusdConfirmNavigation', () => {
const useSelectorMock = useSelector as jest.Mock;
const createParentState = (routeNames: string[]) => ({
routes: routeNames.map((name) => ({ name, key: `${name}-key` })),
});

beforeEach(() => {
jest.resetAllMocks();
mockUseTransactionPayIsMaxAmount.mockReturnValue(false);
mockUseMusdConversionTokens.mockReturnValue({
tokens: [
createTokenWithBalance(),
createTokenWithBalance({ address: '0xToken2', symbol: 'USDT' }),
],
filterAllowedTokens: jest.fn(),
isConversionToken: jest.fn(),
isMusdSupportedOnChain: jest.fn(),
hasConvertibleTokensByChainId: jest.fn(),
});
const setupMoneyHubEnabled = (enabled: boolean) => {
mockUseSelector.mockImplementation((selector: unknown) => {
if (selector === selectMoneyHubEnabledFlag) return enabled;
return undefined;
});
};

it('goes back when quick convert is enabled and navigation can go back', () => {
useSelectorMock.mockReturnValue(true);
mockCanGoBack.mockReturnValue(true);

const { result } = renderHook(() => useMusdConfirmNavigation());

act(() => {
result.current.navigateOnConfirm();
describe('useMusdConfirmNavigation', () => {
beforeEach(() => {
jest.clearAllMocks();
setupMoneyHubEnabled(false);
mockGetParent.mockReturnValue({
dispatch: mockDispatch,
getState: mockGetState,
});

expect(mockGoBack).toHaveBeenCalledTimes(1);
expect(mockNavigate).not.toHaveBeenCalled();
});

it('navigates to wallet view when quick convert is enabled and cannot go back', () => {
useSelectorMock.mockReturnValue(true);
mockCanGoBack.mockReturnValue(false);

const { result } = renderHook(() => useMusdConfirmNavigation());

act(() => {
result.current.navigateOnConfirm();
describe('when Money Hub is enabled', () => {
beforeEach(() => {
setupMoneyHubEnabled(true);
});

expect(mockGoBack).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith(Routes.WALLET_VIEW);
});
it('pops the parent stack when CashTokensFullView is already below', () => {
mockGetState.mockReturnValue(
createParentState(['Home', Routes.WALLET.CASH_TOKENS_FULL_VIEW]),
);

it('navigates to wallet view when quick convert is disabled', () => {
useSelectorMock.mockReturnValue(false);
const { result } = renderHook(() => useMusdConfirmNavigation());

const { result } = renderHook(() => useMusdConfirmNavigation());
act(() => {
result.current.navigateOnConfirm();
});

act(() => {
result.current.navigateOnConfirm();
expect(mockDispatch).toHaveBeenCalledWith(StackActions.pop());
expect(mockNavigate).not.toHaveBeenCalled();
});

expect(mockCanGoBack).not.toHaveBeenCalled();
expect(mockGoBack).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith(Routes.WALLET_VIEW);
});
it('replaces the current route with CashTokensFullView when it is not in the stack', () => {
mockGetState.mockReturnValue(createParentState(['Home']));

it('navigates to wallet view when max converting the last token', () => {
useSelectorMock.mockReturnValue(true);
mockCanGoBack.mockReturnValue(true);
mockUseTransactionPayIsMaxAmount.mockReturnValue(true);
mockUseMusdConversionTokens.mockReturnValue({
tokens: [createTokenWithBalance()],
filterAllowedTokens: jest.fn(),
isConversionToken: jest.fn(),
isMusdSupportedOnChain: jest.fn(),
hasConvertibleTokensByChainId: jest.fn(),
});
const { result } = renderHook(() => useMusdConfirmNavigation());

const { result } = renderHook(() => useMusdConfirmNavigation());
act(() => {
result.current.navigateOnConfirm();
});

act(() => {
result.current.navigateOnConfirm();
expect(mockDispatch).toHaveBeenCalledWith(
StackActions.replace(Routes.WALLET.CASH_TOKENS_FULL_VIEW),
);
expect(mockNavigate).not.toHaveBeenCalled();
});

expect(mockGoBack).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith(Routes.WALLET_VIEW);
});
it('falls back to wallet view when parent navigation is unavailable', () => {
mockGetParent.mockReturnValue(null);

it('goes back when max converting with multiple tokens remaining', () => {
useSelectorMock.mockReturnValue(true);
mockCanGoBack.mockReturnValue(true);
mockUseTransactionPayIsMaxAmount.mockReturnValue(true);
const { result } = renderHook(() => useMusdConfirmNavigation());

const { result } = renderHook(() => useMusdConfirmNavigation());
act(() => {
result.current.navigateOnConfirm();
});

act(() => {
result.current.navigateOnConfirm();
expect(mockDispatch).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith(Routes.WALLET_VIEW);
});

expect(mockGoBack).toHaveBeenCalledTimes(1);
expect(mockNavigate).not.toHaveBeenCalled();
});

it('goes back when custom converting the last token with partial amount', () => {
useSelectorMock.mockReturnValue(true);
mockCanGoBack.mockReturnValue(true);
mockUseTransactionPayIsMaxAmount.mockReturnValue(false);
mockUseMusdConversionTokens.mockReturnValue({
tokens: [createTokenWithBalance()],
filterAllowedTokens: jest.fn(),
isConversionToken: jest.fn(),
isMusdSupportedOnChain: jest.fn(),
hasConvertibleTokensByChainId: jest.fn(),
});
describe('when Money Hub is disabled', () => {
it('navigates to wallet view', () => {
const { result } = renderHook(() => useMusdConfirmNavigation());

const { result } = renderHook(() => useMusdConfirmNavigation());
act(() => {
result.current.navigateOnConfirm();
});

act(() => {
result.current.navigateOnConfirm();
expect(mockNavigate).toHaveBeenCalledWith(Routes.WALLET_VIEW);
expect(mockDispatch).not.toHaveBeenCalled();
});

expect(mockGoBack).toHaveBeenCalledTimes(1);
expect(mockNavigate).not.toHaveBeenCalled();
});
});
62 changes: 34 additions & 28 deletions app/components/UI/Earn/hooks/useMusdConfirmNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
import { useCallback } from 'react';
import { useNavigation } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { StackActions, useNavigation } from '@react-navigation/native';
import Routes from '../../../../constants/navigation/Routes';
import { selectMusdQuickConvertEnabledFlag } from '../selectors/featureFlags';
import { useMusdConversionTokens } from './useMusdConversionTokens';
import { useTransactionPayIsMaxAmount } from '../../../Views/confirmations/hooks/pay/useTransactionPayData';
import { selectMoneyHubEnabledFlag } from '../../Money/selectors/featureFlags';
import { useSelector } from 'react-redux';

export const useMusdConfirmNavigation = () => {
const navigation = useNavigation();
const isMusdQuickConvertEnabled = useSelector(
selectMusdQuickConvertEnabledFlag,
);

const isMaxAmount = useTransactionPayIsMaxAmount();

const { tokens: conversionTokens } = useMusdConversionTokens();
const isMoneyHubEnabled = useSelector(selectMoneyHubEnabledFlag);

// We must operate on the parent (MainNavigator) stack because the
// confirmation screen lives inside a nested EarnScreenStack. A plain
// navigation.navigate() from inside EarnScreenStack would push
// CashTokensFullView on top without removing EarnScreens, leaving the
// stale confirmation screen in the back stack. To prevent that:
// - pop: if CashTokensFullView is already below (entered from Money Hub)
// - replace: if it isn't (entered from TokenListItem or asset detail)
const handleMoneyHubNavigation = useCallback((): boolean => {
const parentNavigation = navigation.getParent();
if (!parentNavigation) {
return false;
}

const isLastConvertibleToken = conversionTokens.length <= 1;
const parentState = parentNavigation.getState();
const isCashTokensFullViewInStack = parentState.routes.some(
(route) => route.name === Routes.WALLET.CASH_TOKENS_FULL_VIEW,
);

if (isCashTokensFullViewInStack) {
parentNavigation.dispatch(StackActions.pop());
} else {
parentNavigation.dispatch(
StackActions.replace(Routes.WALLET.CASH_TOKENS_FULL_VIEW),
);
}
return true;
}, [navigation]);

const navigateOnConfirm = useCallback(() => {
if (isMusdQuickConvertEnabled) {
if (isMaxAmount && isLastConvertibleToken) {
navigation.navigate(Routes.WALLET_VIEW);
return;
}

if (navigation.canGoBack()) {
navigation.goBack();
return;
}
if (isMoneyHubEnabled && handleMoneyHubNavigation()) {
return;
}

navigation.navigate(Routes.WALLET_VIEW);
}, [
isMusdQuickConvertEnabled,
navigation,
isMaxAmount,
isLastConvertibleToken,
]);
}, [handleMoneyHubNavigation, isMoneyHubEnabled, navigation]);

return {
navigateOnConfirm,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import { useSelector } from 'react-redux';
import MoneyConvertStablecoins from './MoneyConvertStablecoins';
import { MoneyConvertStablecoinsTestIds } from './MoneyConvertStablecoins.testIds';
import { strings } from '../../../../../../locales/i18n';
import { AssetType } from '../../../../Views/confirmations/types/token';
import { ConvertTokenRowTestIds } from '../../../Earn/components/Musd/ConvertTokenRow';
import {
selectHasUnapprovedMusdConversion,
selectHasInFlightMusdConversion,
selectMusdConversionStatuses,
} from '../../../Earn/selectors/musdConversionStatus';

jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}));

const mockUseSelector = useSelector as jest.Mock;

jest.mock('../../../../../component-library/base-components/TagBase', () => ({
__esModule: true,
Expand Down Expand Up @@ -100,6 +113,12 @@ const defaultProps = {
describe('MoneyConvertStablecoins', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseSelector.mockImplementation((selector) => {
if (selector === selectHasUnapprovedMusdConversion) return false;
if (selector === selectHasInFlightMusdConversion) return false;
if (selector === selectMusdConversionStatuses) return {};
return undefined;
});
});

describe('with eligible tokens', () => {
Expand Down
Loading
Loading