Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
77 changes: 3 additions & 74 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,81 +167,10 @@ module.exports = {
'no-console': 'off',
},
},
{
// Temporary rollout strategy:
// Keep color-no-hex disabled for all tests by default, then re-enable it
// for specific folders in small PR batches. Once migration is complete,
// remove this override and enforce across all tests in:
// - app/components/
// - app/component-library/
files: ['**/*.test.{js,ts,tsx}', '**/*.stories.{js,ts,tsx}'],
rules: {
'@metamask/design-tokens/color-no-hex': 'off',
},
},
{
files: [
// @MetaMask/card
'app/components/UI/Card/**/*.{js,jsx,ts,tsx}',
// @MetaMask/core-platform
'app/components/Snaps/**/*.{js,jsx,ts,tsx}',
// @MetaMask/predict
'app/components/UI/Predict/**/*.{js,jsx,ts,tsx}',
// @MetaMask/ramp
'app/components/UI/Ramp/**/*.{js,jsx,ts,tsx}',
// @MetaMask/rewards
'app/components/UI/Rewards/**/*.{js,jsx,ts,tsx}',
// @MetaMask/perps
'app/components/UI/Perps/**/*.{js,jsx,ts,tsx}',
// @MetaMask/metamask-earn
'app/components/UI/Earn/**/*.{js,jsx,ts,tsx}',
'app/components/UI/Stake/**/*.{js,jsx,ts,tsx}',
// @MetaMask/metamask-assets
'app/components/UI/Assets/**/*.{js,jsx,ts,tsx}',
'app/components/UI/Tokens/**/*.{js,jsx,ts,tsx}',
'app/components/UI/AssetOverview/**/*.{js,jsx,ts,tsx}',
'app/components/UI/Collectibles/**/*.{js,jsx,ts,tsx}',
'app/components/UI/CollectibleContractElement/**/*.{js,jsx,ts,tsx}',
'app/components/UI/CollectibleContractInformation/**/*.{js,jsx,ts,tsx}',
'app/components/UI/CollectibleContractOverview/**/*.{js,jsx,ts,tsx}',
'app/components/UI/CollectibleContracts/**/*.{js,jsx,ts,tsx}',
'app/components/UI/CollectibleDetectionModal/**/*.{js,jsx,ts,tsx}',
'app/components/UI/CollectibleMedia/**/*.{js,jsx,ts,tsx}',
'app/components/UI/CollectibleModal/**/*.{js,jsx,ts,tsx}',
'app/components/UI/CollectibleOverview/**/*.{js,jsx,ts,tsx}',
'app/components/UI/ConfirmAddAsset/**/*.{js,jsx,ts,tsx}',
'app/components/UI/DeFiPositions/**/*.{js,jsx,ts,tsx}',
'app/components/UI/TokenDetails/**/*.{js,jsx,ts,tsx}',
'app/components/Views/AddAsset/**/*.{js,jsx,ts,tsx}',
'app/components/Views/Asset/**/*.{js,jsx,ts,tsx}',
'app/components/Views/AssetDetails/**/*.{js,jsx,ts,tsx}',
'app/components/Views/AssetHideConfirmation/**/*.{js,jsx,ts,tsx}',
'app/components/Views/AssetOptions/**/*.{js,jsx,ts,tsx}',
'app/components/Views/Collectible/**/*.{js,jsx,ts,tsx}',
'app/components/Views/CollectibleView/**/*.{js,jsx,ts,tsx}',
'app/components/Views/DetectedTokens/**/*.{js,jsx,ts,tsx}',
'app/components/Views/NFTAutoDetectionModal/**/*.{js,jsx,ts,tsx}',
'app/components/Views/NftDetails/**/*.{js,jsx,ts,tsx}',
// @MetaMask/mobile-core-ux
'app/components/Views/AccountActions/**/*.{js,jsx,ts,tsx}',
'app/components/Views/AccountSelector/**/*.{js,jsx,ts,tsx}',
'app/components/Views/AccountsMenu/**/*.{js,jsx,ts,tsx}',
'app/components/Views/AddressQRCode/**/*.{js,jsx,ts,tsx}',
'app/components/Views/EditAccountName/**/*.{js,jsx,ts,tsx}',
'app/components/Views/LockScreen/**/*.{js,jsx,ts,tsx}',
'app/components/Views/Login/**/*.{js,jsx,ts,tsx}',
'app/components/Views/MultichainTransactionsView/**/*.{js,jsx,ts,tsx}',
'app/components/Views/NetworkConnect/**/*.{js,jsx,ts,tsx}',
'app/components/Views/NetworkSelector/**/*.{js,jsx,ts,tsx}',
'app/components/Views/QRAccountDisplay/**/*.{js,jsx,ts,tsx}',
'app/components/Views/QRScanner/**/*.{js,jsx,ts,tsx}',
'app/components/Views/Settings/**/*.{js,jsx,ts,tsx}',
'app/components/Views/TermsAndConditions/**/*.{js,jsx,ts,tsx}',
'app/components/Views/UnifiedTransactionsView/**/*.{js,jsx,ts,tsx}',
'app/components/UI/MultichainTransactionListItem/**/*.{js,jsx,ts,tsx}',
'app/components/UI/TransactionActionModal/**/*.{js,jsx,ts,tsx}',
'app/components/UI/TransactionElement/**/*.{js,jsx,ts,tsx}',
'app/components/UI/Transactions/**/*.{js,jsx,ts,tsx}',
'app/components/**/*.{js,jsx,ts,tsx}',
'app/component-library/**/*.{js,jsx,ts,tsx}',
],
rules: {
'@metamask/design-tokens/color-no-hex': 'error',
Expand Down Expand Up @@ -674,7 +603,7 @@ module.exports = {
'react/no-string-refs': 'error',
'react/no-unused-prop-types': 'error',
'react/prefer-es6-class': 'error',
'@metamask/design-tokens/color-no-hex': 'warn',
'@metamask/design-tokens/color-no-hex': 'off',
radix: 'off',

// These rule modifications are removing changes to our shared ESLint config made after
Expand Down
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ android {
applicationId "io.metamask"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.72.0"
versionName "7.73.0"
versionCode 4138
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down
33 changes: 33 additions & 0 deletions app/components/UI/Bridge/Views/BridgeView/BridgeView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ jest.mock(
);

const mockNavigate = jest.fn();
const mockSetParams = jest.fn();
const mockFocusEffects: (() => void | (() => void))[] = [];
const mockRoute = {
params: {
sourcePage: 'test',
Expand All @@ -233,8 +235,12 @@ jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native');
return {
...actualNav,
useFocusEffect: jest.fn((callback: () => void | (() => void)) => {
mockFocusEffects.push(callback);
}),
useNavigation: () => ({
navigate: mockNavigate,
setParams: mockSetParams,
setOptions: jest.fn(),
}),
useRoute: () => mockRoute,
Expand Down Expand Up @@ -355,6 +361,10 @@ describe('BridgeView', () => {

beforeEach(() => {
jest.clearAllMocks();
mockFocusEffects.length = 0;
mockRoute.params = {
sourcePage: 'test',
} as BridgeRouteParams;
});

it('renders source and destination token areas', async () => {
Expand All @@ -372,6 +382,29 @@ describe('BridgeView', () => {
).toBeTruthy();
});

it('scrolls to top and clears the route param when requested on focus', () => {
mockRoute.params = {
sourcePage: 'test',
scrollToTopOnNav: true,
} as BridgeRouteParams;

renderScreen(
BridgeView,
{
name: Routes.BRIDGE.ROOT,
},
{ state: mockState },
);

act(() => {
mockFocusEffects[mockFocusEffects.length - 1]?.();
});

expect(mockSetParams).toHaveBeenCalledWith({
scrollToTopOnNav: undefined,
});
});

it('should open BridgeTokenSelector when clicking source token', async () => {
const { findByText } = renderScreen(
BridgeView,
Expand Down
13 changes: 13 additions & 0 deletions app/components/UI/Bridge/Views/BridgeView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import {
useNavigation,
useRoute,
useFocusEffect,
type RouteProp,
} from '@react-navigation/native';
import { getBridgeNavbar } from '../../../Navbar';
Expand Down Expand Up @@ -113,6 +114,7 @@ const BridgeView = () => {
const route = useRoute<RouteProp<{ params: BridgeRouteParams }, 'params'>>();
const { colors } = useTheme();
const keypadRef = useRef<SwapsKeypadRef>(null);
const scrollViewRef = useRef<ScrollView>(null);

// Needed to get gas fee estimates
const selectedNetworkClientId = useSelector(selectSelectedNetworkClientId);
Expand Down Expand Up @@ -186,6 +188,16 @@ const BridgeView = () => {

useBridgeViewOnFocus({ inputRef, keypadRef });

// Scroll to top when navigating to the bridge view if requested
useFocusEffect(
useCallback(() => {
if (route.params?.scrollToTopOnNav && scrollViewRef.current) {
scrollViewRef.current.scrollTo({ y: 0, animated: false });
navigation.setParams({ scrollToTopOnNav: undefined });
}
}, [navigation, route.params?.scrollToTopOnNav]),
);

useEffect(() => {
if (route.params?.bridgeViewMode && bridgeViewMode === undefined) {
dispatch(setBridgeViewMode(route.params?.bridgeViewMode));
Expand Down Expand Up @@ -403,6 +415,7 @@ const BridgeView = () => {
}}
>
<ScrollView
ref={scrollViewRef}
testID={BridgeViewSelectorsIDs.BRIDGE_VIEW_SCROLL}
style={styles.scrollView}
contentContainerStyle={styles.scrollViewContent}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,12 @@ jest.mock('../TokenSelectorItem', () => ({
onPress,
children,
}: {
token: { symbol: string; address: string; chainId: string };
token: {
symbol: string;
address: string;
chainId: string;
isVerified?: boolean;
};
onPress: (token: {
symbol: string;
address: string;
Expand All @@ -440,6 +445,13 @@ jest.mock('../TokenSelectorItem', () => ({
TouchableOpacity,
{ onPress: () => onPress(token), testID: `token-${token.symbol}` },
createElement(Text, null, token.symbol),
token.isVerified
? createElement(
Text,
{ testID: `verified-${token.symbol}` },
'verified',
)
: null,
createElement(View, null, children),
);
},
Expand Down Expand Up @@ -561,10 +573,14 @@ describe('tokenToIncludeAsset', () => {
});
});

const createSearchToken = (symbol: string) =>
const createSearchToken = (
symbol: string,
overrides: Partial<ReturnType<typeof createMockPopularToken>> = {},
) =>
createMockPopularToken({
assetId: `eip155:1/erc20:0x${symbol.toLowerCase()}` as never,
symbol,
...overrides,
});

describe('BridgeTokenSelector', () => {
Expand Down Expand Up @@ -628,6 +644,23 @@ describe('BridgeTokenSelector', () => {
);
await waitFor(() => expect(getByTestId('token-USDC')).toBeTruthy());
});

it('passes verified popular tokens through to selector rows', async () => {
mockPopularTokensState = {
popularTokens: [
createMockPopularToken({
symbol: 'ETH',
name: 'Ethereum',
isVerified: true,
}),
],
isLoading: false,
};

const { getByTestId } = renderWithReduxProvider(<BridgeTokenSelector />);

await waitFor(() => expect(getByTestId('verified-ETH')).toBeTruthy());
});
});

describe('search', () => {
Expand All @@ -648,6 +681,19 @@ describe('BridgeTokenSelector', () => {
await waitFor(() => expect(getByTestId('token-WETH')).toBeTruthy());
});

it('passes verified search results through to selector rows', async () => {
mockSearchTokensState = {
...mockSearchTokensState,
searchResults: [createSearchToken('WETH', { isVerified: true })],
currentSearchQuery: 'WET',
};
const { getByTestId } = renderWithReduxProvider(<BridgeTokenSelector />);

fireEvent.changeText(getByTestId('bridge-token-search-input'), 'WET');

await waitFor(() => expect(getByTestId('verified-WETH')).toBeTruthy());
});

it('clears search when clear button is pressed', async () => {
const { getByTestId, queryByTestId } = renderWithReduxProvider(
<BridgeTokenSelector />,
Expand Down
6 changes: 6 additions & 0 deletions app/components/UI/Bridge/components/InputStepper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export const InputStepper = ({
maxAmount,
postValue,
placeholder = '0',
selection,
onSelectionChange,
}: InputStepperProps) => {
const fontSize = calculateInputFontSize(value.length);
const { styles } = useStyles(inputStepperStyles, { fontSize });
Expand Down Expand Up @@ -62,6 +64,10 @@ export const InputStepper = ({
value={displayedAmount}
style={styles.input}
testID="input-stepper-input"
// Slippage controls selection so keypad edits can target the
// displayed caret position instead of always appending.
selection={selection}
onSelectionChange={onSelectionChange}
/>
</View>
{postValue && (
Expand Down
11 changes: 11 additions & 0 deletions app/components/UI/Bridge/components/InputStepper/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import {
IconSize,
TextColor,
} from '@metamask/design-system-react-native';
import {
type NativeSyntheticEvent,
type TextInputSelectionChangeEventData,
} from 'react-native';

export interface InputStepperProps {
value: string;
Expand All @@ -22,4 +26,11 @@ export interface InputStepperProps {
maxAmount: number;
postValue?: string;
placeholder?: string;
selection?: {
start: number;
end: number;
};
onSelectionChange?: (
event: NativeSyntheticEvent<TextInputSelectionChangeEventData>,
) => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { strings } from '../../../../../../locales/i18n';
import { BridgeToken } from '../../types';
import { CHAIN_IDS } from '@metamask/transaction-controller';

jest.mock('../../../../../util/theme', () => ({
useTheme: jest.fn(() => ({
colors: {
success: { default: '#28A745' },
},
})),
}));
jest.mock('../../../../../util/theme', () => {
const actualTheme = jest.requireActual('../../../../../util/theme');
return {
...actualTheme,
useTheme: jest.fn(() => ({
colors: {
success: { default: actualTheme.mockTheme.colors.success.default },
},
})),
};
});

const mockDestToken: BridgeToken = {
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
Expand Down
Loading
Loading