Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
bf19c03
refactor(perps): use params in the controller and remove unused route…
michalconsensys Jan 30, 2026
3075736
test: mUSD conversion component view test (#25169)
racitores Jan 30, 2026
630e397
chore: Bump transaction-pay-controller to ^12.0.2 and other related c…
dan437 Jan 30, 2026
7748289
fix: component view test in main (#25428)
racitores Jan 30, 2026
57729e1
release: Bump main version to 7.65.0 (#25411)
metamaskbot Jan 30, 2026
85f52c7
fix: strengthen explore portfolio site condition (#25433)
Prithpal-Sooriya Jan 30, 2026
a9c05f5
chore: simplify local e2e testing setup (#25402)
juanmigdr Jan 30, 2026
f43252c
fix(confirmations): handle batch predict deposits and quote‑based ale…
pedronfigueiredo Jan 30, 2026
fb7cf08
test: add musd conversion feature e2e (#25183)
racitores Jan 30, 2026
3b5ab86
chore: codeowners update for token details (#25440)
bergarces Jan 30, 2026
2cdcf0a
chore: remove legacy accounts component code (pre BIP-44) (#24886)
gantunesr Jan 30, 2026
a44c0c5
chore: removed usage of token-search-discovery-controller (#25435)
juanmigdr Jan 30, 2026
971cfc4
feat: added deeplinking to the NFT screen (#25426)
juanmigdr Jan 30, 2026
910d769
feat: return actual host for known public domains in analytics cp-7.6…
cryptodev-2s Jan 30, 2026
58eab76
chore: add more metrics to trending flow (#25375)
sahar-fehri Jan 30, 2026
62ccea0
chore: Updated headers for settings general page (#25356)
brianacnguyen Jan 30, 2026
6f89dec
fix(perps): potential rate limit on close positions cp-7.63.0 cp-7.64…
abretonc7s Jan 30, 2026
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
5 changes: 1 addition & 4 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,6 @@ app/components/Views/Settings/NotificationsSettings @MetaMask/notifications
ses.cjs @MetaMask/supply-chain
patches/react-native+0.*.patch @MetaMask/supply-chain

# Portfolio Team
app/components/hooks/useTokenSearchDiscovery @MetaMask/portfolio
app/core/Engine/controllers/TokenSearchDiscoveryController @MetaMask/portfolio

# Core Platform Team
**/snaps/** @MetaMask/core-platform
**/Snaps/** @MetaMask/core-platform
Expand Down Expand Up @@ -187,6 +183,7 @@ app/components/UI/CollectibleOverview @MetaMask/metamask-assets
app/components/UI/ConfirmAddAsset @MetaMask/metamask-assets
app/components/UI/DeFiPositions @MetaMask/metamask-assets
app/components/UI/Tokens @MetaMask/metamask-assets
app/components/UI/TokenDetails @MetaMask/metamask-assets
app/components/Views/AddAsset @MetaMask/metamask-assets
app/components/Views/Asset @MetaMask/metamask-assets
app/components/Views/AssetDetails @MetaMask/metamask-assets
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.64.0"
versionName "7.65.0"
versionCode 3418
testBuildType System.getProperty('testBuildType', 'debug')
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down
8 changes: 8 additions & 0 deletions app/component-library/components/Navigation/TabBar/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const TabBar = ({ state, descriptors, navigation }: TabBarProps) => {
selectAssetsTrendingTokensEnabled,
);
const tabBarRef = useRef(null);
const previousTabIndexRef = useRef<number>(state.index);
const tw = useTailwind();

const renderTabBarItem = useCallback(
Expand All @@ -54,6 +55,13 @@ const TabBar = ({ state, descriptors, navigation }: TabBarProps) => {
const labelKey = LABEL_BY_TAB_BAR_ICON_KEY[tabBarIconKey];
const labelText = labelKey ? strings(labelKey) : '';
const onPress = () => {
// Call onLeave callback for the previous tab before switching
if (previousTabIndexRef.current !== index) {
const previousRoute = state.routes[previousTabIndexRef.current];
const previousOptions = descriptors[previousRoute?.key]?.options;
previousOptions?.onLeave?.();
previousTabIndexRef.current = index;
}
callback?.();
switch (rootScreenName) {
case Routes.WALLET_VIEW:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export interface ExtendedBottomTabDescriptor extends BottomTabDescriptor {
rootScreenName: string;
isSelected?: (rootScreenName: string) => boolean;
isHidden?: boolean;
/**
* Callback fired when leaving this tab (switching to another tab).
* Useful for cleanup actions like ending analytics sessions.
*/
onLeave?: () => void;
};
}

Expand Down
21 changes: 16 additions & 5 deletions app/components/Nav/Main/MainNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import ActivityView from '../../Views/ActivityView';
import RewardsNavigator from '../../UI/Rewards/RewardsNavigator';
import { ExploreFeed } from '../../Views/TrendingView/TrendingView';
import ExploreSearchScreen from '../../Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen';
import TrendingFeedSessionManager from '../../UI/Trending/services/TrendingFeedSessionManager';
import CollectiblesDetails from '../../UI/CollectibleModal';
import OptinMetrics from '../../UI/OptinMetrics';

Expand Down Expand Up @@ -362,7 +363,7 @@ const SettingsFlow = () => (
<Stack.Screen
name="GeneralSettings"
component={GeneralSettings}
options={GeneralSettings.navigationOptions}
options={{ headerShown: false }}
/>
<Stack.Screen
name="AdvancedSettings"
Expand Down Expand Up @@ -595,6 +596,19 @@ const HomeTabs = () => {
MetaMetricsEvents.NAVIGATION_TAPS_TRENDING,
).build(),
);
// Re-enable AppState listener when returning to trending tab
// (it was disabled when leaving to prevent phantom sessions)
TrendingFeedSessionManager.getInstance().enableAppStateListener();
// Start a new session when returning to trending tab
// The session manager will ignore if a session is already active
TrendingFeedSessionManager.getInstance().startSession('tab_press');
},
onLeave: () => {
// End trending session when user switches to another tab
TrendingFeedSessionManager.getInstance().endSession();
// Disable AppState listener to prevent phantom sessions when app backgrounds/foregrounds
// while user is on a different tab (since TrendingView stays mounted with unmountOnBlur: false)
TrendingFeedSessionManager.getInstance().disableAppStateListener();
},
rootScreenName: Routes.TRENDING_VIEW,
unmountOnBlur: false,
Expand Down Expand Up @@ -1281,10 +1295,7 @@ const MainNavigator = () => {
<Stack.Screen
name="GeneralSettings"
component={GeneralSettings}
options={{
headerShown: true,
...GeneralSettings.navigationOptions,
}}
options={{ headerShown: false }}
/>
{process.env.METAMASK_ENVIRONMENT !== 'production' && (
<Stack.Screen
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ exports[`MainNavigator Tab Bar Visibility hides tab bar when browser is active 1
name="GeneralSettings"
options={
{
"headerShown": true,
"headerShown": false,
}
}
/>
Expand Down Expand Up @@ -593,7 +593,7 @@ exports[`MainNavigator Tab Bar Visibility shows tab bar when not in browser 1`]
name="GeneralSettings"
options={
{
"headerShown": true,
"headerShown": false,
}
}
/>
Expand Down Expand Up @@ -911,7 +911,7 @@ exports[`MainNavigator matches rendered snapshot 1`] = `
name="GeneralSettings"
options={
{
"headerShown": true,
"headerShown": false,
}
}
/>
Expand Down
25 changes: 4 additions & 21 deletions app/components/UI/AddressCopy/AddressCopy.test.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import React from 'react';
import { InternalAccount } from '@metamask/keyring-internal-api';

import AddressCopy from './AddressCopy';
import { WalletViewSelectorsIDs } from '../../Views/Wallet/WalletView.testIds';
import renderWithProvider from '../../../util/test/renderWithProvider';
import { createMockInternalAccount } from '../../../util/test/accountsControllerTestUtils';
import { ToastContext } from '../../../component-library/components/Toast';

// Mock navigation before importing renderWithProvider
jest.mock('@react-navigation/native', () => ({
Expand All @@ -15,32 +12,18 @@ jest.mock('@react-navigation/native', () => ({
}),
}));

const mockShowToast = jest.fn();
const mockCloseToast = jest.fn();
const mockToastRef = {
current: { showToast: mockShowToast, closeToast: mockCloseToast },
};

const renderWithAddressCopy = (account: InternalAccount) =>
renderWithProvider(
<ToastContext.Provider value={{ toastRef: mockToastRef }}>
<AddressCopy account={account} />
</ToastContext.Provider>,
);
const renderAddressCopy = () => renderWithProvider(<AddressCopy />);

describe('AddressCopy', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders correctly the component', () => {
const component = renderWithAddressCopy(
createMockInternalAccount('0xaddress', 'Account 1'),
);
it('renders the copy button', () => {
const { getByTestId } = renderAddressCopy();

expect(component).toBeDefined();
expect(
component.getByTestId(WalletViewSelectorsIDs.ACCOUNT_COPY_BUTTON),
getByTestId(WalletViewSelectorsIDs.ACCOUNT_COPY_BUTTON),
).toBeDefined();
});
});
78 changes: 4 additions & 74 deletions app/components/UI/AddressCopy/AddressCopy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Third parties dependencies
import React, { useCallback, useContext } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { AccountGroupId } from '@metamask/account-api';
Expand All @@ -11,27 +11,15 @@ import {
ButtonIconSize,
IconName,
} from '@metamask/design-system-react-native';
import ClipboardManager from '../../../core/ClipboardManager';
import { protectWalletModalVisible } from '../../../actions/user';
import {
ToastContext,
ToastVariants,
} from '../../../component-library/components/Toast';
import { IconName as ComponentLibraryIconName } from '../../../component-library/components/Icons/Icon';

import { strings } from '../../../../locales/i18n';
import { MetaMetricsEvents } from '../../../core/Analytics';
import { useStyles } from '../../../component-library/hooks';
import { WalletViewSelectorsIDs } from '../../Views/Wallet/WalletView.testIds';
import { selectMultichainAccountsState2Enabled } from '../../../selectors/featureFlagController/multichainAccounts/enabledMultichainAccounts';
import { selectSelectedAccountGroupId } from '../../../selectors/multichainAccounts/accountTreeController';
import { createAddressListNavigationDetails } from '../../Views/MultichainAccounts/AddressList';

// Internal dependencies
import styleSheet from './AddressCopy.styles';
import { useMetrics } from '../../../components/hooks/useMetrics';
import { useTheme } from '../../../util/theme';
import { getFormattedAddressFromInternalAccount } from '../../../core/Multichain/utils';
import type { AddressCopyProps } from './AddressCopy.types';
import {
endTrace,
Expand All @@ -40,59 +28,13 @@ import {
TraceOperation,
} from '../../../util/trace';

const AddressCopy = ({ account, iconColor, hitSlop }: AddressCopyProps) => {
const AddressCopy = ({ iconColor, hitSlop }: AddressCopyProps) => {
const { styles } = useStyles(styleSheet, {});
const { navigate } = useNavigation();
const { colors } = useTheme();

const dispatch = useDispatch();
const { trackEvent, createEventBuilder } = useMetrics();
const { toastRef } = useContext(ToastContext);

const isMultichainAccountsState2Enabled = useSelector(
selectMultichainAccountsState2Enabled,
);
const selectedAccountGroupId = useSelector(selectSelectedAccountGroupId);

const handleProtectWalletModalVisible = useCallback(
() => dispatch(protectWalletModalVisible()),
[dispatch],
);

/**
* A string that represents the selected address
*/

const copyAccountToClipboard = useCallback(async () => {
await ClipboardManager.setString(
getFormattedAddressFromInternalAccount(account),
);
toastRef?.current?.showToast({
variant: ToastVariants.Icon,
iconName: ComponentLibraryIconName.CheckBold,
iconColor: colors.accent03.dark,
backgroundColor: colors.accent03.normal,
labelOptions: [
{ label: strings('account_details.account_copied_to_clipboard') },
],
hasNoTimeout: false,
});
setTimeout(() => handleProtectWalletModalVisible(), 2000);

trackEvent(
createEventBuilder(MetaMetricsEvents.WALLET_COPIED_ADDRESS).build(),
);
}, [
account,
colors.accent03.dark,
colors.accent03.normal,
createEventBuilder,
handleProtectWalletModalVisible,
toastRef,
trackEvent,
]);

const navigateToAddressList = useCallback(() => {
const handleOnPress = useCallback(() => {
// Start the trace before navigating to the address list to include the
// navigation and render times in the trace.
trace({
Expand All @@ -116,18 +58,6 @@ const AddressCopy = ({ account, iconColor, hitSlop }: AddressCopyProps) => {
);
}, [navigate, selectedAccountGroupId]);

const handleOnPress = useCallback(() => {
if (isMultichainAccountsState2Enabled) {
navigateToAddressList();
} else {
copyAccountToClipboard();
}
}, [
copyAccountToClipboard,
isMultichainAccountsState2Enabled,
navigateToAddressList,
]);

return (
<View style={styles.address}>
<ButtonIcon
Expand Down
2 changes: 0 additions & 2 deletions app/components/UI/AddressCopy/AddressCopy.types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { InternalAccount } from '@metamask/keyring-internal-api';
import { IconColor } from '@metamask/design-system-react-native';

export interface AddressCopyProps {
account: InternalAccount;
iconColor?: IconColor;
hitSlop?: {
top?: number;
Expand Down
Loading
Loading