Skip to content

chore(deps): upgrade @metamask/design-system-react-native to v0.16.0 (design system v31.0.0)#28612

Open
georgewrmarshall wants to merge 7 commits intomainfrom
chore/upgrade-design-system-v31
Open

chore(deps): upgrade @metamask/design-system-react-native to v0.16.0 (design system v31.0.0)#28612
georgewrmarshall wants to merge 7 commits intomainfrom
chore/upgrade-design-system-v31

Conversation

@georgewrmarshall
Copy link
Copy Markdown
Contributor

@georgewrmarshall georgewrmarshall commented Apr 9, 2026

Description

Upgrades @metamask/design-system-react-native from ^0.13.0 to ^0.16.0, corresponding to the MetaMask Design System v31.0.0 release.

What changed in v31.0.0

New components available:

  • HeaderSearch — search header component (we have a local version; the DSRN version can be evaluated for future migration)
  • KeyValueColumn — vertical key/value layout component

Breaking changes audited:

Breaking Change Status
BoxHorizontalBoxRow No usages found in codebase — no action needed
BoxVerticalBoxColumn No usages found in codebase — no action needed
BoxHorizontalPropsSharedBoxRowPropsShared No usages found — no action needed
BoxVerticalPropsSharedBoxColumnPropsShared No usages found — no action needed
KeyValueRow stub-based composition removed Mobile uses local KeyValueRow in components-temp/ — not affected

Changelog

CHANGELOG entry: null

Related issues

Fixes:

Manual testing steps

Feature: Design system package upgrade

  Scenario: App builds and renders correctly after upgrade
    Given the updated design-system-react-native package is installed

    When the app is built and launched
    Then all screens using design system components render correctly
    And no TypeScript errors are introduced

Screenshots/Recordings

Before

After

Pre-merge author checklist

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Upgrades a core UI dependency, which can subtly change component behavior/styling across the app despite minimal local code changes. Snapshot updates indicate expected rendering/prop differences (e.g., icon sizing, tailwind class output).

Overview
Upgrades @metamask/design-system-react-native to ^0.16.0 (and @metamask/utils to ^11.11.0), updating the lockfile accordingly.

Adds deprecation notices and migration links to several in-repo component-library wrappers (Avatar, Badge, BadgeStatus, TextFieldSearch, SensitiveText, and temp HeaderSearch/KeyValueRow) to steer consumers toward the design-system equivalents.

Adjusts usage/tests to match the new design-system outputs: usePredictShare.utils now sources IconSize from the local Icon component, and Jest snapshots are updated for icon size tokens (e.g., "24""lg") and tailwind class formatting (e.g., w-[32px] h-[32px]w-8 h-8).

Reviewed by Cursor Bugbot for commit 5740e79. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 9, 2026

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

@metamaskbot metamaskbot added the team-design-system All issues relating to design system in Mobile label Apr 9, 2026
@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 9, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatednpm/​@​metamask/​design-system-react-native@​0.14.0 ⏵ 0.16.0981008498 +1100

View full report

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Unsatisfied peer dependency for @metamask/utils
    • Updated package.json to require @metamask/utils ^11.11.0 and reinstalled to unify the lockfile resolution.

Create PR

Or push these changes by commenting:

@cursor push 919d132504
Preview (919d132504)
diff --git a/package.json b/package.json
--- a/package.json
+++ b/package.json
@@ -326,7 +326,7 @@
     "@metamask/transaction-controller": "^64.0.0",
     "@metamask/transaction-pay-controller": "^19.1.0",
     "@metamask/tron-wallet-snap": "^1.25.1",
-    "@metamask/utils": "^11.8.1",
+    "@metamask/utils": "^11.11.0",
     "@myx-trade/sdk": "^0.1.265",
     "@ngraveio/bc-ur": "^1.1.6",
     "@nktkas/hyperliquid": "^0.30.2",

diff --git a/yarn.lock b/yarn.lock
--- a/yarn.lock
+++ b/yarn.lock
@@ -35686,7 +35686,7 @@
     "@metamask/transaction-controller": "npm:^64.0.0"
     "@metamask/transaction-pay-controller": "npm:^19.1.0"
     "@metamask/tron-wallet-snap": "npm:^1.25.1"
-    "@metamask/utils": "npm:^11.8.1"
+    "@metamask/utils": "npm:^11.11.0"
     "@myx-trade/sdk": "npm:^0.1.265"
     "@ngraveio/bc-ur": "npm:^1.1.6"
     "@nktkas/hyperliquid": "npm:^0.30.2"

You can send follow-ups to the cloud agent here.

@github-actions github-actions bot added size-S and removed size-XS labels Apr 9, 2026
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Unused private method #hasExtendedMarketsForLeague added
    • Removed the unused private method from PolymarketProvider to eliminate dead code.

Create PR

Or push these changes by commenting:

@cursor push 30c6ac2cbc
Preview (30c6ac2cbc)
diff --git a/app/component-library/components-temp/HeaderSearch/HeaderSearch.tsx b/app/component-library/components-temp/HeaderSearch/HeaderSearch.tsx
--- a/app/component-library/components-temp/HeaderSearch/HeaderSearch.tsx
+++ b/app/component-library/components-temp/HeaderSearch/HeaderSearch.tsx
@@ -23,6 +23,10 @@
 } from './HeaderSearch.types';
 
 /**
+ * @deprecated Please update your code to use `HeaderSearch` from `@metamask/design-system-react-native`.
+ * The API may have changed — compare props before migrating.
+ * @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/HeaderSearch/README.md}
+ *
  * HeaderSearch is a header component that combines a search field
  * with either a back button (screen variant) or cancel button (inline variant).
  *

diff --git a/app/component-library/components-temp/KeyValueRow/KeyValueRow.tsx b/app/component-library/components-temp/KeyValueRow/KeyValueRow.tsx
--- a/app/component-library/components-temp/KeyValueRow/KeyValueRow.tsx
+++ b/app/component-library/components-temp/KeyValueRow/KeyValueRow.tsx
@@ -13,6 +13,11 @@
 import KeyValueRowRoot from './KeyValueRoot/KeyValueRoot';
 
 /**
+ * @deprecated Please update your code to use `KeyValueRow` from `@metamask/design-system-react-native`.
+ * The API has changed — the new component uses flat props (`keyLabel`, `value`, `variant`) instead of nested `field`/`value` objects.
+ * @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/KeyValueRow/README.md}
+ * @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/MIGRATION.md#keyvaluerow-api Migration docs}
+ *
  * Prebuilt convenience component to format and render a key/value KeyValueRowLabel pair.
  * The KeyValueRowLabel component has props to display a tooltip and icon.
  *

diff --git a/app/component-library/components/Avatars/Avatar/Avatar.tsx b/app/component-library/components/Avatars/Avatar/Avatar.tsx
--- a/app/component-library/components/Avatars/Avatar/Avatar.tsx
+++ b/app/component-library/components/Avatars/Avatar/Avatar.tsx
@@ -16,6 +16,12 @@
 // Internal dependencies.
 import { AvatarProps, AvatarVariant } from './Avatar.types';
 
+/**
+ * @deprecated Please update your code to use the individual avatar components from `@metamask/design-system-react-native`
+ * such as `AvatarAccount`, `AvatarFavicon`, `AvatarIcon`, `AvatarNetwork`, or `AvatarToken`.
+ * The API may have changed — compare props before migrating.
+ * @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/AvatarAccount/README.md}
+ */
 const Avatar = ({ variant, ...props }: AvatarProps) => {
   switch (variant) {
     case AvatarVariant.Account:

diff --git a/app/component-library/components/Badges/Badge/Badge.tsx b/app/component-library/components/Badges/Badge/Badge.tsx
--- a/app/component-library/components/Badges/Badge/Badge.tsx
+++ b/app/component-library/components/Badges/Badge/Badge.tsx
@@ -16,6 +16,12 @@
   BADGE_BADGENOTIFICATIONS_TEST_ID,
 } from './Badge.constants';
 
+/**
+ * @deprecated Please update your code to use the individual badge components from `@metamask/design-system-react-native`
+ * such as `BadgeNetwork`, `BadgeStatus`, `BadgeCount`, or `BadgeIcon`.
+ * The API may have changed — compare props before migrating.
+ * @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/BadgeNetwork/README.md}
+ */
 const Badge = ({ variant, ...props }: BadgeProps) => {
   switch (variant) {
     case BadgeVariant.Network:

diff --git a/app/component-library/components/Badges/Badge/variants/BadgeStatus/BadgeStatus.tsx b/app/component-library/components/Badges/Badge/variants/BadgeStatus/BadgeStatus.tsx
--- a/app/component-library/components/Badges/Badge/variants/BadgeStatus/BadgeStatus.tsx
+++ b/app/component-library/components/Badges/Badge/variants/BadgeStatus/BadgeStatus.tsx
@@ -16,6 +16,11 @@
   DEFAULT_BADGESTATUS_STATE,
 } from './BadgeStatus.constants';
 
+/**
+ * @deprecated Please update your code to use `BadgeStatus` from `@metamask/design-system-react-native`.
+ * The API may have changed — compare props before migrating.
+ * @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/BadgeStatus/README.md}
+ */
 const BadgeStatus = ({
   style,
   state = DEFAULT_BADGESTATUS_STATE,

diff --git a/app/component-library/components/Form/TextFieldSearch/TextFieldSearch.tsx b/app/component-library/components/Form/TextFieldSearch/TextFieldSearch.tsx
--- a/app/component-library/components/Form/TextFieldSearch/TextFieldSearch.tsx
+++ b/app/component-library/components/Form/TextFieldSearch/TextFieldSearch.tsx
@@ -13,6 +13,11 @@
 import { TEXTFIELDSEARCH_TEST_ID } from './TextFieldSearch.constants';
 import styles from './TextFieldSearch.styles';
 
+/**
+ * @deprecated Please update your code to use `TextFieldSearch` from `@metamask/design-system-react-native`.
+ * The API may have changed — compare props before migrating.
+ * @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/TextFieldSearch/README.md}
+ */
 const TextFieldSearch: React.FC<TextFieldSearchProps> = ({
   onPressClearButton,
   clearButtonProps,

diff --git a/app/component-library/components/Texts/SensitiveText/SensitiveText.tsx b/app/component-library/components/Texts/SensitiveText/SensitiveText.tsx
--- a/app/component-library/components/Texts/SensitiveText/SensitiveText.tsx
+++ b/app/component-library/components/Texts/SensitiveText/SensitiveText.tsx
@@ -5,6 +5,11 @@
 // internal dependencies
 import { SensitiveTextProps, SensitiveTextLength } from './SensitiveText.types';
 
+/**
+ * @deprecated Please update your code to use `SensitiveText` from `@metamask/design-system-react-native`.
+ * The API may have changed — compare props before migrating.
+ * @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/SensitiveText/README.md}
+ */
 const SensitiveText: React.FC<SensitiveTextProps> = ({
   isHidden = false,
   children = '',

diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js
--- a/app/components/UI/Navbar/index.js
+++ b/app/components/UI/Navbar/index.js
@@ -1130,7 +1130,7 @@
 
   return getHeaderCompactStandardNavbarOptions({
     title,
-    onClose: () => navigation.getParent()?.pop(),
+    onBack: () => navigation.goBack(),
     includesTopInset: true,
   });
 }

diff --git a/app/components/UI/Navbar/index.test.js b/app/components/UI/Navbar/index.test.js
--- a/app/components/UI/Navbar/index.test.js
+++ b/app/components/UI/Navbar/index.test.js
@@ -696,14 +696,11 @@
     });
   });
 
-  describe('getBridgeNavbar with getParent', () => {
-    it('calls navigation.getParent().pop() when close button is pressed', () => {
-      const mockParentPop = jest.fn();
+  describe('getBridgeNavbar back button behavior', () => {
+    it('calls navigation.goBack() when back button is pressed', () => {
       const navigationWithParent = {
         ...mockNavigation,
-        getParent: jest.fn(() => ({
-          pop: mockParentPop,
-        })),
+        getParent: jest.fn(),
       };
       const options = getBridgeNavbar(
         navigationWithParent,
@@ -715,8 +712,8 @@
       const Header = options.header;
       const { getByTestId } = render(<Header />);
       fireEvent.press(getByTestId('button-icon'));
-      expect(navigationWithParent.getParent).toHaveBeenCalled();
-      expect(mockParentPop).toHaveBeenCalled();
+      expect(navigationWithParent.goBack).toHaveBeenCalled();
+      expect(navigationWithParent.getParent).not.toHaveBeenCalled();
     });
   });
 

diff --git a/app/components/UI/Predict/constants/flags.ts b/app/components/UI/Predict/constants/flags.ts
--- a/app/components/UI/Predict/constants/flags.ts
+++ b/app/components/UI/Predict/constants/flags.ts
@@ -1,4 +1,5 @@
 import {
+  PredictExtendedSportsMarketsFlag,
   PredictFeeCollection,
   PredictHotTabFlag,
   PredictLiveSportsFlag,
@@ -23,6 +24,13 @@
   leagues: [],
 };
 
+export const DEFAULT_EXTENDED_SPORTS_MARKETS_FLAG: PredictExtendedSportsMarketsFlag =
+  {
+    enabled: false,
+    minimumVersion: '',
+    leagues: [],
+  };
+
 export const DEFAULT_MARKET_HIGHLIGHTS_FLAG: PredictMarketHighlightsFlag = {
   enabled: false,
   highlights: [],

diff --git a/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts b/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts
--- a/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts
+++ b/app/components/UI/Predict/providers/polymarket/PolymarketProvider.test.ts
@@ -252,6 +252,7 @@
   const defaultFeatureFlags: PredictFeatureFlags = {
     feeCollection: DEFAULT_FEE_COLLECTION_FLAG,
     liveSportsLeagues: [],
+    extendedSportsMarketsLeagues: [],
     marketHighlightsFlag: {
       enabled: false,
       highlights: [],

diff --git a/app/components/UI/Predict/selectors/featureFlags/index.test.ts b/app/components/UI/Predict/selectors/featureFlags/index.test.ts
--- a/app/components/UI/Predict/selectors/featureFlags/index.test.ts
+++ b/app/components/UI/Predict/selectors/featureFlags/index.test.ts
@@ -1,4 +1,5 @@
 import {
+  selectExtendedSportsMarketsLeagues,
   selectPredictEnabledFlag,
   selectPredictFakOrdersEnabledFlag,
   selectPredictFeatureFlags,
@@ -1255,4 +1256,122 @@
       expect(result).toBe(false);
     });
   });
+
+  describe('selectExtendedSportsMarketsLeagues', () => {
+    it('returns leagues when flag is enabled and version check passes', () => {
+      mockHasMinimumRequiredVersion.mockReturnValue(true);
+      const state = {
+        engine: {
+          backgroundState: {
+            RemoteFeatureFlagController: {
+              remoteFeatureFlags: {
+                predictExtendedSportsMarkets: {
+                  enabled: true,
+                  minimumVersion: '1.0.0',
+                  leagues: ['nba', 'ucl'],
+                },
+              },
+              cacheTimestamp: 0,
+            },
+          },
+        },
+      };
+
+      const result = selectExtendedSportsMarketsLeagues(state);
+
+      expect(result).toEqual(['nba', 'ucl']);
+    });
+
+    it('returns empty array when flag is disabled', () => {
+      mockHasMinimumRequiredVersion.mockReturnValue(true);
+      const state = {
+        engine: {
+          backgroundState: {
+            RemoteFeatureFlagController: {
+              remoteFeatureFlags: {
+                predictExtendedSportsMarkets: {
+                  enabled: false,
+                  minimumVersion: '1.0.0',
+                  leagues: ['nba', 'ucl'],
+                },
+              },
+              cacheTimestamp: 0,
+            },
+          },
+        },
+      };
+
+      const result = selectExtendedSportsMarketsLeagues(state);
+
+      expect(result).toEqual([]);
+    });
+
+    it('returns empty array when app version is below minimum required version', () => {
+      mockHasMinimumRequiredVersion.mockReturnValue(false);
+      const state = {
+        engine: {
+          backgroundState: {
+            RemoteFeatureFlagController: {
+              remoteFeatureFlags: {
+                predictExtendedSportsMarkets: {
+                  enabled: true,
+                  minimumVersion: '99.0.0',
+                  leagues: ['nba'],
+                },
+              },
+              cacheTimestamp: 0,
+            },
+          },
+        },
+      };
+
+      const result = selectExtendedSportsMarketsLeagues(state);
+
+      expect(result).toEqual([]);
+    });
+
+    it('returns empty array when remote feature flags are empty', () => {
+      const result = selectExtendedSportsMarketsLeagues(mockedEmptyFlagsState);
+
+      expect(result).toEqual([]);
+    });
+
+    it('returns empty array when controller is undefined', () => {
+      const state = {
+        engine: {
+          backgroundState: {
+            RemoteFeatureFlagController: undefined,
+          },
+        },
+      };
+
+      const result = selectExtendedSportsMarketsLeagues(state);
+
+      expect(result).toEqual([]);
+    });
+
+    it('filters out unsupported leagues', () => {
+      mockHasMinimumRequiredVersion.mockReturnValue(true);
+      const state = {
+        engine: {
+          backgroundState: {
+            RemoteFeatureFlagController: {
+              remoteFeatureFlags: {
+                predictExtendedSportsMarkets: {
+                  enabled: true,
+                  minimumVersion: '1.0.0',
+                  leagues: ['nba', 'fake_league', 'ucl'],
+                },
+              },
+              cacheTimestamp: 0,
+            },
+          },
+        },
+      };
+
+      const result = selectExtendedSportsMarketsLeagues(state);
+
+      expect(result).toEqual(['nba', 'ucl']);
+    });
+  });
 });

diff --git a/app/components/UI/Predict/selectors/featureFlags/index.ts b/app/components/UI/Predict/selectors/featureFlags/index.ts
--- a/app/components/UI/Predict/selectors/featureFlags/index.ts
+++ b/app/components/UI/Predict/selectors/featureFlags/index.ts
@@ -122,6 +122,11 @@
     resolvePredictFeatureFlags({ remoteFeatureFlags, localOverrides }),
 );
 
+export const selectExtendedSportsMarketsLeagues = createSelector(
+  selectPredictFeatureFlags,
+  (flags) => flags.extendedSportsMarketsLeagues,
+);
+
 export const selectPredictFeeCollectionFlag = createSelector(
   selectPredictFeatureFlags,
   (flags) => flags.feeCollection,

diff --git a/app/components/UI/Predict/types/flags.ts b/app/components/UI/Predict/types/flags.ts
--- a/app/components/UI/Predict/types/flags.ts
+++ b/app/components/UI/Predict/types/flags.ts
@@ -18,9 +18,15 @@
   highlights: PredictMarketHighlight[];
 }
 
+export interface PredictExtendedSportsMarketsFlag
+  extends VersionGatedFeatureFlag {
+  leagues: string[];
+}
+
 export interface PredictFeatureFlags {
   feeCollection: PredictFeeCollection;
   liveSportsLeagues: string[];
+  extendedSportsMarketsLeagues: string[];
   marketHighlightsFlag: PredictMarketHighlightsFlag;
   fakOrdersEnabled: boolean;
   predictWithAnyTokenEnabled: boolean;

diff --git a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts
--- a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts
+++ b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts
@@ -1,5 +1,6 @@
 import { validatedVersionGatedFeatureFlag } from '../../../../util/remoteFeatureFlag';
 import {
+  DEFAULT_EXTENDED_SPORTS_MARKETS_FLAG,
   DEFAULT_FEE_COLLECTION_FLAG,
   DEFAULT_MARKET_HIGHLIGHTS_FLAG,
 } from '../constants/flags';
@@ -25,6 +26,7 @@
     expect(result).toEqual({
       feeCollection: DEFAULT_FEE_COLLECTION_FLAG,
       liveSportsLeagues: [],
+      extendedSportsMarketsLeagues: [],
       marketHighlightsFlag: DEFAULT_MARKET_HIGHLIGHTS_FLAG,
       fakOrdersEnabled: false,
       predictWithAnyTokenEnabled: false,
@@ -183,4 +185,123 @@
     expect(result.fakOrdersEnabled).toBe(true);
     expect(result.predictWithAnyTokenEnabled).toBe(false);
   });
+
+  describe('extendedSportsMarketsLeagues', () => {
+    it('returns empty array when flag is missing', () => {
+      const result = resolvePredictFeatureFlags({});
+
+      expect(result.extendedSportsMarketsLeagues).toEqual([]);
+    });
+
+    it('returns empty array when flag is disabled', () => {
+      const result = resolvePredictFeatureFlags({
+        remoteFeatureFlags: {
+          predictExtendedSportsMarkets: {
+            ...DEFAULT_EXTENDED_SPORTS_MARKETS_FLAG,
+            enabled: false,
+            leagues: ['nba', 'ucl'],
+          },
+        },
+      });
+
+      expect(result.extendedSportsMarketsLeagues).toEqual([]);
+    });
+
+    it('returns empty array when version check fails', () => {
+      mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => {
+        if (flag && typeof flag === 'object' && 'leagues' in flag) {
+          return false;
+        }
+        return undefined;
+      });
+
+      const result = resolvePredictFeatureFlags({
+        remoteFeatureFlags: {
+          predictExtendedSportsMarkets: {
+            enabled: true,
+            minimumVersion: '99.0.0',
+            leagues: ['nba', 'ucl'],
+          },
+        },
+      });
+
+      expect(result.extendedSportsMarketsLeagues).toEqual([]);
+    });
+
+    it('returns filtered leagues when enabled and version check passes', () => {
+      mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => {
+        if (flag && typeof flag === 'object' && 'leagues' in flag) {
+          return true;
+        }
+        return undefined;
+      });
+
+      const result = resolvePredictFeatureFlags({
+        remoteFeatureFlags: {
+          predictExtendedSportsMarkets: {
+            enabled: true,
+            minimumVersion: '1.0.0',
+            leagues: ['nba', 'ucl', 'fake_league'],
+          },
+        },
+      });
+
+      expect(result.extendedSportsMarketsLeagues).toEqual(['nba', 'ucl']);
+    });
+
+    it('unwraps progressive rollout shape', () => {
+      mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => {
+        if (flag && typeof flag === 'object' && 'leagues' in flag) {
+          return true;
+        }
+        return undefined;
+      });
+
+      const result = resolvePredictFeatureFlags({
+        remoteFeatureFlags: {
+          predictExtendedSportsMarkets: {
+            name: 'group-a',
+            value: {
+              enabled: true,
+              minimumVersion: '1.0.0',
+              leagues: ['nba', 'epl'],
+            },
+          },
+        },
+      });
+
+      expect(result.extendedSportsMarketsLeagues).toEqual(['nba', 'epl']);
+    });
+
+    it('applies local override over remote flag', () => {
+      mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) =>
+        Boolean(
+          flag &&
+            typeof flag === 'object' &&
+            'enabled' in flag &&
+            'leagues' in flag &&
+            (flag as { enabled: boolean }).enabled,
+        ),
+      );
+
+      const result = resolvePredictFeatureFlags({
+        remoteFeatureFlags: {
+          predictExtendedSportsMarkets: {
+            enabled: true,
+            minimumVersion: '1.0.0',
+            leagues: ['nba', 'ucl'],
+          },
+        },
+        localOverrides: {
+          predictExtendedSportsMarkets: {
+            enabled: false,
+            minimumVersion: '1.0.0',
+            leagues: ['nba', 'ucl'],
+          },
+        },
+      });
+
+      expect(result.extendedSportsMarketsLeagues).toEqual([]);
+    });
+  });
 });

diff --git a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts
--- a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts
+++ b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts
@@ -3,6 +3,7 @@
   validatedVersionGatedFeatureFlag,
 } from '../../../../util/remoteFeatureFlag';
 import {
+  DEFAULT_EXTENDED_SPORTS_MARKETS_FLAG,
   DEFAULT_FEE_COLLECTION_FLAG,
   DEFAULT_LIVE_SPORTS_FLAG,
   DEFAULT_MARKET_HIGHLIGHTS_FLAG,
@@ -10,6 +11,7 @@
 import { filterSupportedLeagues } from '../constants/sports';
 import { parse, PredictFeeCollectionSchema } from '../schemas';
 import {
+  PredictExtendedSportsMarketsFlag,
   PredictFeatureFlags,
   PredictLiveSportsFlag,
   PredictMarketHighlightsFlag,
@@ -80,9 +82,23 @@
       unwrapRemoteFeatureFlag<VersionGatedFeatureFlag>(flags.predictUpDown),
     ) ?? false;
 
+  const rawExtendedSportsFlag =
+    unwrapRemoteFeatureFlag<PredictExtendedSportsMarketsFlag>(
+      flags.predictExtendedSportsMarkets,
+    ) ?? DEFAULT_EXTENDED_SPORTS_MARKETS_FLAG;
+
+  const isExtendedSportsEnabled = validatedVersionGatedFeatureFlag(
+    rawExtendedSportsFlag,
+  );
+
+  const extendedSportsMarketsLeagues = isExtendedSportsEnabled
+    ? filterSupportedLeagues(rawExtendedSportsFlag.leagues ?? [])
+    : [];
+
   return {
     feeCollection,
     liveSportsLeagues,
+    extendedSportsMarketsLeagues,
     marketHighlightsFlag,
     fakOrdersEnabled,
     predictWithAnyTokenEnabled,

diff --git a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.test.tsx b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.test.tsx
--- a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.test.tsx
+++ b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.test.tsx
@@ -4,6 +4,7 @@
 import PredictionsSection from './PredictionsSection';
 import Routes from '../../../../../constants/navigation/Routes';
 import { PREDICT_CLAIM_BUTTON_TEST_IDS } from '../../../../UI/Predict/components/PredictActionButtons/PredictClaimButton.testIds';
+import { PredictEventValues } from '../../../../UI/Predict/constants/eventNames';
 
 const mockNavigate = jest.fn();
 const mockClaim = jest.fn();
@@ -234,6 +235,33 @@
     });
   });
 
+  it('navigates with homepage_positions entry_point when positions section title is pressed', () => {
+    mockUsePredictPositionsForHomepage.mockImplementation(
+      ({
+        claimable = false,
+      }: { maxPositions?: number; claimable?: boolean } = {}) => ({
+        positions: claimable ? [] : mockActivePositions,
+        isLoading: false,
+        error: null,
+        totalClaimableValue: 0,
+        refetch: jest.fn(),
+      }),
+    );
+
+    renderWithProvider(
+      <PredictionsSection sectionIndex={0} totalSectionsLoaded={1} />,
+    );
+
+    fireEvent.press(screen.getByText('Predictions'));
+
+    expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, {
+      screen: Routes.PREDICT.MARKET_LIST,
+      params: {
+        entryPoint: PredictEventValues.ENTRY_POINT.HOMEPAGE_POSITIONS,
+      },
+    });
+  });
+
   it('returns null when predict is disabled', () => {
     jest
       .requireMock('../../../../UI/Predict/selectors/featureFlags')
@@ -639,6 +667,32 @@
       expect(screen.getByText('Test Position 1')).toBeOnTheScreen();
     });
 
+    it('navigates with homepage_positions entry_point on title press', () => {
+      mockUsePredictPositionsForHomepage.mockReturnValue({
+        positions: mockActivePositions,
+        isLoading: false,
+        error: null,
+        refetch: jest.fn(),
+      });
+
+      renderWithProvider(
+        <PredictionsSection
+          sectionIndex={0}
+          totalSectionsLoaded={5}
+          mode="positions-only"
+        />,
+      );
+
+      fireEvent.press(screen.getByText('Predictions'));
+
+      expect(mockNavigate).toHaveBeenCalledWith(Routes.PREDICT.ROOT, {
+        screen: Routes.PREDICT.MARKET_LIST,
+        params: {
+          entryPoint: PredictEventValues.ENTRY_POINT.HOMEPAGE_POSITIONS,
+        },
+      });
+    });
+
     it('returns null when no positions after loading', () => {
       mockUsePredictPositionsForHomepage.mockReturnValue({
         positions: [],

diff --git a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx
--- a/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx
+++ b/app/components/Views/Homepage/Sections/Predictions/PredictionsSection.tsx
@@ -242,6 +242,15 @@
     });
   }, [navigation]);
 
+  const handleViewAllFromPositions = useCallback(() => {
+    navigation.navigate(Routes.PREDICT.ROOT, {
+      screen: Routes.PREDICT.MARKET_LIST,
+      params: {
+        entryPoint: PredictEventValues.ENTRY_POINT.HOMEPAGE_POSITIONS,
+      },
+    });
+  }, [navigation]);
+
   const handlePositionPress = useCallback(
     (position: PredictPosition) => {
       navigation.navigate(Routes.PREDICT.ROOT, {
@@ -256,7 +265,11 @@
     [navigation],
   );
 
-  return { handleViewAllPredictions, handlePositionPress };
+  return {
+    handleViewAllPredictions,
+    handleViewAllFromPositions,
+    handlePositionPress,
+  };
 };
 
 const usePredictPositionsSectionData = () => {
@@ -334,9 +347,12 @@
     const queryClient = useQueryClient();
     const title = titleOverride ?? strings('homepage.sections.predictions');
     const analyticsName = sectionNameOverride ?? HomeSectionNames.PREDICT;
-    const { handleViewAllPredictions, handlePositionPress } =
-      usePredictNavigationHandlers();
     const {
+      handleViewAllPredictions,
+      handleViewAllFromPositions,
+      handlePositionPress,
+    } = usePredictNavigationHandlers();
+    const {
       privacyMode,
       positions,
       isLoadingPositions,
@@ -406,7 +422,7 @@
         <View ref={sectionViewRef} onLayout={onLayout}>
           <HomepagePredictPositions
             title={title}
-            onViewAll={handleViewAllPredictions}
+            onViewAll={handleViewAllFromPositions}
             privacyMode={privacyMode}
             isLoadingPositions={isLoadingPositions}
             positions={positions}
@@ -456,7 +472,7 @@
     const queryClient = useQueryClient();
     const title = titleOverride ?? strings('homepage.sections.predictions');
     const analyticsName = sectionNameOverride ?? HomeSectionNames.PREDICT;
-    const { handleViewAllPredictions, handlePositionPress } =
+    const { handleViewAllFromPositions, handlePositionPress } =
       usePredictNavigationHandlers();
     const {
       privacyMode,
@@ -503,7 +519,7 @@
       <View ref={sectionViewRef} onLayout={onLayout}>
         <HomepagePredictPositions
           title={title}
-          onViewAll={handleViewAllPredictions}
+          onViewAll={handleViewAllFromPositions}
           privacyMode={privacyMode}
           isLoadingPositions={isLoadingPositions}
           positions={positions}

diff --git a/app/components/Views/confirmations/hooks/useAutomaticGasFeeTokenSelect.test.ts b/app/components/Views/confirmations/hooks/useAutomaticGasFeeTokenSelect.test.ts
--- a/app/components/Views/confirmations/hooks/useAutomaticGasFeeTokenSelect.test.ts
+++ b/app/components/Views/confirmations/hooks/useAutomaticGasFeeTokenSelect.test.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/naming-convention */
 import { GasFeeToken, TransactionMeta } from '@metamask/transaction-controller';
 import { Hex } from '@metamask/utils';
 import { act } from '@testing-library/react';
@@ -19,6 +20,13 @@
 jest.mock('../../../../util/transaction-controller');
 jest.mock('./gas/useIsGaslessSupported');
 
+const mockSetConfirmationMetric = jest.fn();
+jest.mock('./metrics/useConfirmationMetricEvents', () => ({
+  useConfirmationMetricEvents: () => ({
+    setConfirmationMetric: mockSetConfirmationMetric,
+  }),
+}));
+
 const FROM_MOCK = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc';
 export const GAS_FEE_TOKEN_MOCK: GasFeeToken = {
   amount: toHex(1000),
@@ -128,11 +136,19 @@
       expect.any(String),
       GAS_FEE_TOKEN_MOCK.tokenAddress,
     );
+    expect(mockSetConfirmationMetric).toHaveBeenCalledTimes(1);
+    expect(mockSetConfirmationMetric).toHaveBeenCalledWith({
+      properties: {
+        gas_payment_token_default: true,
+        gas_payment_token_default_symbol: GAS_FEE_TOKEN_MOCK.symbol,
+      },
+    });
   });
 
   it('does not select first gas fee token if gas fee token already selected', () => {
     runHook({ selectedGasFeeToken: GAS_FEE_TOKEN_MOCK.tokenAddress });
     expect(updateSelectedGasFeeTokenMock).toHaveBeenCalledTimes(0);
+    expect(mockSetConfirmationMetric).not.toHaveBeenCalled();
   });
 
   it('selects first gas fee token if gas fee token already selected but doesnt correspond to any gasFeeTokens (only if `excludeNativeTokenForFee` is set', () => {
@@ -155,11 +171,19 @@
       expect.any(String),
       '0x9876543210000000000000000000000000000000',
     );
+    expect(mockSetConfirmationMetric).toHaveBeenCalledTimes(1);
+    expect(mockSetConfirmationMetric).toHaveBeenCalledWith({
+      properties: {
+        gas_payment_token_default: true,
+        gas_payment_token_default_symbol: undefined,
+      },
+    });
   });
 
   it('does not select first gas fee token if no gas fee tokens', () => {
     runHook({ gasFeeTokens: [] });
     expect(updateSelectedGasFeeTokenMock).toHaveBeenCalledTimes(0);
+    expect(mockSetConfirmationMetric).not.toHaveBeenCalled();
   });
 
   it('does not select first gas fee token if not first load', () => {
@@ -177,6 +201,7 @@
     rerender({});
 
     expect(updateSelectedGasFeeTokenMock).toHaveBeenCalledTimes(0);
+    expect(mockSetConfirmationMetric).not.toHaveBeenCalled();
   });
 
   it('does not select first gas fee token if gasless not supported', () => {
@@ -189,11 +214,13 @@
     runHook();
 
     expect(updateSelectedGasFeeTokenMock).toHaveBeenCalledTimes(0);
+    expect(mockSetConfirmationMetric).not.toHaveBeenCalled();
   });
 
   it('does not select first gas fee token if sufficient balance', () => {
     runHook();
     expect(updateSelectedGasFeeTokenMock).toHaveBeenCalledTimes(0);
+    expect(mockSetConfirmationMetric).not.toHaveBeenCalled();
   });
 
   it('does not select first gas fee token after firstCheck is set to false', () => {
@@ -210,11 +237,13 @@
     });
     rerender({});
     expect(updateSelectedGasFeeTokenMock).toHaveBeenCalledTimes(1); // Only first run
+    expect(mockSetConfirmationMetric).toHaveBeenCalledTimes(1); // Only first run
   });
 
   it('does not select if gasFeeTokens is falsy', () => {
     runHook({ gasFeeTokens: [] });
     expect(updateSelectedGasFeeTokenMock).toHaveBeenCalledTimes(0);
+    expect(mockSetConfirmationMetric).not.toHaveBeenCalled();
... diff truncated: showing 800 of 986 lines

You can send follow-ups to the cloud agent here.

@georgewrmarshall georgewrmarshall force-pushed the chore/upgrade-design-system-v31 branch from eb83a86 to dda4e20 Compare April 9, 2026 18:54
"@metamask/delegation-controller": "^2.0.2",
"@metamask/delegation-deployments": "^1.0.0",
"@metamask/design-system-react-native": "^0.13.0",
"@metamask/design-system-react-native": "^0.16.0",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why ^0.16.0?

This bumps the design system monorepo from v0.13.0 to v0.16.0 (monorepo release v31.0.0). The codebase was audited for breaking changes before upgrading: BoxHorizontalBoxRow and BoxVerticalBoxColumn renames have no usages here, and the KeyValueRow API overhaul doesn't affect this repo since it uses a local implementation in components-temp/.

"@metamask/transaction-pay-controller": "^19.1.0",
"@metamask/tron-wallet-snap": "^1.25.1",
"@metamask/utils": "^11.8.1",
"@metamask/utils": "^11.11.0",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why bump @metamask/utils here?

@metamask/design-system-react-native v0.16.0 declares a peer dependency on @metamask/utils: ^11.11.0, but the project was pinned to ^11.8.1 (resolving to 11.10.0). Without this bump, npm/yarn resolves two separate copies of @metamask/utils in the tree — one for the host (11.10.0) and one for @metamask/design-system-shared (11.11.0) — which can cause instanceof checks to silently fail across module boundaries.

import KeyValueRowRoot from './KeyValueRoot/KeyValueRoot';

/**
* @deprecated Please update your code to use `KeyValueRow` from `@metamask/design-system-react-native`.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Breaking API change — not a drop-in replacement.

The DSRN KeyValueRow v0.16.0 completely replaced the nested field/value object API with flat props: keyLabel, value, variant, keyEndButtonIconProps, etc. The local implementation here (and its KeyValueRowStubs sub-components) serves ~15 call sites across the codebase that rely on the old shape. Migration requires updating each call site to the new flat-prop API — see the linked migration docs.

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Broad axios preapproval bypasses age gate permanently
    • Replaced broad 'axios' preapproval with 'axios@1.15.0' to limit the bypass to the intended SSRF-fix release and restore the age gate for all other versions.

Create PR

Or push these changes by commenting:

@cursor push 1972fe3f39
Preview (1972fe3f39)
diff --git a/.yarnrc.yml b/.yarnrc.yml
--- a/.yarnrc.yml
+++ b/.yarnrc.yml
@@ -30,4 +30,4 @@
   - '@metamask-previews/*'
   - '@lavamoat/*'
   - '@consensys/*'
-  - 'axios' # Preapproved to allow 1.15.0 (critical SSRF fix) before 3-day age gate expires
+  - 'axios@1.15.0' # Preapproved to allow 1.15.0 (critical SSRF fix) before 3-day age gate expires

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 6f9d24e. Configure here.

@georgewrmarshall georgewrmarshall force-pushed the chore/upgrade-design-system-v31 branch from 6f9d24e to 588124d Compare April 9, 2026 19:31
* @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/AvatarIcon/README.md AvatarIcon}
* @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/AvatarNetwork/README.md AvatarNetwork}
* @see {@link https://github.com/MetaMask/metamask-design-system/blob/main/packages/design-system-react-native/src/components/AvatarToken/README.md AvatarToken}
*/
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avatar should be replaced with one of the variants we no longer have this unified component

@georgewrmarshall georgewrmarshall self-assigned this Apr 9, 2026
@georgewrmarshall georgewrmarshall marked this pull request as ready for review April 9, 2026 19:33
@georgewrmarshall georgewrmarshall requested a review from a team as a code owner April 9, 2026 19:33
brianacnguyen
brianacnguyen previously approved these changes Apr 9, 2026
@github-actions github-actions bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 9, 2026
brianacnguyen
brianacnguyen previously approved these changes Apr 9, 2026
@georgewrmarshall georgewrmarshall force-pushed the chore/upgrade-design-system-v31 branch from fc934d7 to 664a12a Compare April 9, 2026 22:35
@georgewrmarshall georgewrmarshall requested a review from a team as a code owner April 9, 2026 22:35
@georgewrmarshall georgewrmarshall force-pushed the chore/upgrade-design-system-v31 branch from 664a12a to 76603c0 Compare April 9, 2026 22:35
@georgewrmarshall georgewrmarshall requested review from a team as code owners April 10, 2026 21:11
@georgewrmarshall georgewrmarshall force-pushed the chore/upgrade-design-system-v31 branch from 74d0994 to 5740e79 Compare April 10, 2026 21:13
import { Box } from '@metamask/design-system-react-native';
import Icon, {
IconName,
IconSize,
Copy link
Copy Markdown
Contributor Author

@georgewrmarshall georgewrmarshall Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixing wrong icon size import

@georgewrmarshall georgewrmarshall removed request for a team April 10, 2026 21:14
@github-actions github-actions bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 10, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeConfirmations, SmokeTrade, SmokePredictions, SmokeWalletPlatform, SmokeAccounts, SmokeNetworkAbstractions
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 72%
click to see 🤖 AI reasoning details

E2E Test Selection:

The PR bumps @metamask/design-system-react-native from ^0.14.0 to ^0.16.0 (2 minor versions) and @metamask/utils from ^11.8.1 to ^11.11.0. The snapshot changes confirm real rendering differences from the design system update:

  1. Icon size API changed: numeric string "24" → semantic token "lg" (visible in Bridge slippage stepper snapshots)
  2. CSS class generation changed: "w-[32px] h-[32px]" → "w-8 h-8" (visible in ManualBackupStep1 snapshot)
  3. usePredictShare.utils.tsx: IconSize import moved from design-system-react-native to local Icon component — functional change affecting Predict toast notifications

The deprecated JSDoc additions (Avatar, Badge, BadgeStatus, TextFieldSearch, HeaderSearch, KeyValueRow, SensitiveText, KeyValueRow) are documentation-only with zero functional impact.

However, the design system package update affects widely-used components across the app. The snapshot changes indicate visual rendering differences that could affect:

  • SmokeConfirmations: Avatar, Badge, KeyValueRow components are used in confirmation screens; icon size changes could affect UI
  • SmokeTrade: Bridge slippage stepper directly affected (snapshot changed); bridge/swap flows use these components
  • SmokePredictions: usePredictShare.utils.tsx directly changed (IconSize import refactor); Predict toast notifications affected
  • SmokeWalletPlatform: SensitiveText used for balance display (privacy mode); ManualBackupStep1 snapshot changed (backup flow)
  • SmokeAccounts: ManualBackupStep1 directly affected by snapshot change; Avatar components used in account list
  • SmokeNetworkAbstractions: Avatar/Badge components used in network selector UI

Not selecting all tags because: the changes are primarily design system rendering updates (not logic changes), the deprecated annotations are documentation-only, and the core functionality of most features is unaffected. The @metamask/utils bump is a utility library update unlikely to break E2E flows.

Dependent tag requirements checked:

  • SmokeTrade selected → SmokeConfirmations also selected (required)
  • SmokePredictions selected → SmokeWalletPlatform and SmokeConfirmations also selected (required)

Performance Test Selection:
The changes are design system component updates (deprecated JSDoc annotations, package version bumps) and snapshot updates. While the design system package bump changes how icon sizes and CSS classes are rendered, these are cosmetic/visual changes that don't affect the performance characteristics measured by performance tests (render times, data loading, state management). The @metamask/utils bump is a utility library update unlikely to impact performance metrics. No performance test tags are warranted.

View GitHub Actions results

@github-actions
Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
17 value mismatches detected (expected — fixture represents an existing user).
View details

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk-medium Moderate testing recommended · Possible bug introduction risk size-S skip-sonar-cloud Only used for bypassing sonar cloud when failures are not relevant to the changes. team-design-system All issues relating to design system in Mobile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants