From 73eaaf0ed505b04456d92839a7147cd6209615fa Mon Sep 17 00:00:00 2001 From: Wei Sun Date: Wed, 6 May 2026 18:02:28 -0700 Subject: [PATCH 1/3] chore: add shared native-stack modal options (#29694) ## **Description** This PR introduces two shared `NativeStackNavigationOptions` presets in `clearStackNavigatorOptions.ts`: Perps already uses `createNativeStackNavigator`, but several screens repeated the same option blobs (`presentation: 'transparentModal'`, transparent `contentStyle`, `animation: 'none'`, `headerShown: false`) inline. That duplicated the JS-stack `clearStackNavigatorOptions` idea without a typed, reusable native-stack equivalent. ## **Changelog** CHANGELOG entry:null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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** > Moderate risk because it changes React Navigation option presets for native stacks, which can affect modal presentation, animations, and header behavior across Perps flows. > > **Overview** > Standardizes Perps native-stack overlay behavior by replacing inline `presentation: 'transparentModal'`/transparent styling with shared presets (`transparentModalScreenOptions`, `clearNativeStackNavigatorOptions`). > > Adds native-stack equivalents of the existing clear JS-stack options in `clearStackNavigatorOptions.ts`, and updates Perps screens (TP/SL, close-position/bottom-sheet stacks, and pay-with modal) to use these presets for consistent transparent modals without unwanted animations. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 3a0cec5fe529e086dbddbe392eac1418de9804f7. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- app/components/UI/Perps/routes/index.tsx | 28 ++++++++----------- .../navigation/clearStackNavigatorOptions.ts | 28 +++++++++++++++++++ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/app/components/UI/Perps/routes/index.tsx b/app/components/UI/Perps/routes/index.tsx index c7a322f2ab1d..fe3c4f4082fe 100644 --- a/app/components/UI/Perps/routes/index.tsx +++ b/app/components/UI/Perps/routes/index.tsx @@ -47,7 +47,11 @@ import { RouteProp, useNavigation, useRoute } from '@react-navigation/native'; /* eslint-disable-next-line */ import { NavigationContext } from '@react-navigation/core'; import { CONFIRMATION_HEADER_CONFIG } from '../constants/perpsConfig'; -import { clearStackNavigatorOptions } from '../../../../constants/navigation/clearStackNavigatorOptions'; +import { + clearNativeStackNavigatorOptions, + clearStackNavigatorOptions, + transparentModalScreenOptions, +} from '../../../../constants/navigation/clearStackNavigatorOptions'; const Stack = createNativeStackNavigator(); const ModalStack = createStackNavigator(); @@ -73,7 +77,7 @@ function getRedesignedConfirmationsHeaderOptions({ title: '', headerBackVisible: false, contentStyle: { backgroundColor: 'transparent' }, - presentation: 'transparentModal', + ...transparentModalScreenOptions, }; } @@ -353,9 +357,9 @@ const PerpsScreenStack = () => { name={Routes.PERPS.TPSL} component={PerpsTPSLView} options={{ + ...transparentModalScreenOptions, title: strings('perps.tpsl.title'), headerShown: false, - presentation: 'transparentModal', }} /> @@ -411,12 +415,8 @@ const PerpsScreenStack = () => { name={Routes.PERPS.MODALS.CLOSE_POSITION_MODALS} component={PerpsClosePositionBottomSheetStack} options={{ - headerShown: false, - contentStyle: { - backgroundColor: 'transparent', - }, - animation: 'none', - presentation: 'transparentModal', + ...clearNativeStackNavigatorOptions, + ...transparentModalScreenOptions, }} /> @@ -425,12 +425,8 @@ const PerpsScreenStack = () => { name={Routes.PERPS.MODALS.ROOT} component={PerpsModalStack} options={{ - headerShown: false, - contentStyle: { - backgroundColor: 'transparent', - }, - animation: 'none', - presentation: 'transparentModal', + ...clearNativeStackNavigatorOptions, + ...transparentModalScreenOptions, }} /> @@ -442,7 +438,7 @@ const PerpsScreenStack = () => { component={PayWithModal} options={{ headerShown: false, - presentation: 'transparentModal', + ...transparentModalScreenOptions, }} /> diff --git a/app/constants/navigation/clearStackNavigatorOptions.ts b/app/constants/navigation/clearStackNavigatorOptions.ts index ae81976ad804..4e088e3b3554 100644 --- a/app/constants/navigation/clearStackNavigatorOptions.ts +++ b/app/constants/navigation/clearStackNavigatorOptions.ts @@ -1,3 +1,4 @@ +import type { NativeStackNavigationOptions } from '@react-navigation/native-stack'; import type { StackNavigationOptions } from '@react-navigation/stack'; /** Transparent stack with no transition animation; used for modal-style flows. */ @@ -22,3 +23,30 @@ export const clearStackNavigatorOptionsWithTransitionAnimation: StackNavigationO }), animationEnabled: false, }; + +/** + * Native-stack counterpart to {@link clearStackNavigatorOptions}. + * Use with `createNativeStackNavigator` only (`contentStyle` / `animation`, not `cardStyle` / `animationEnabled`). + * + * Includes `animation: 'none'` — omit this preset on screens where you want the default push/modal animation. + */ +export const clearNativeStackNavigatorOptions: NativeStackNavigationOptions = { + headerShown: false, + contentStyle: { + backgroundColor: 'transparent', + }, + animation: 'none', +}; + +/** + * Per-screen options for overlay-style screens on native stack. + * Replaces the JS-stack `cardStyleInterpolator` trick (overlay opacity 0) — native stack keeps the + * presenting screen mounted and does not dim it when `presentation: 'transparentModal'` is used. + * + * Often spread **after** {@link clearNativeStackNavigatorOptions} for fully static overlays. + * Skip `clearNativeStackNavigatorOptions` when this screen should keep the default stack animation + * (it sets `animation: 'none'`). + */ +export const transparentModalScreenOptions: NativeStackNavigationOptions = { + presentation: 'transparentModal', +}; From 8208502740d27a04b54e338fb640edfd36f3c585 Mon Sep 17 00:00:00 2001 From: sophieqgu <37032128+sophieqgu@users.noreply.github.com> Date: Wed, 6 May 2026 21:27:27 -0400 Subject: [PATCH 2/3] fix(Rewards): error when visiting rewards tab (#29823) ## **Description** https://consensyssoftware.atlassian.net/browse/RWDS-1267 The Rewards crash likely came from an older persisted Rewards Redux state that did not have newer array fields, especially campaigns. On affected devices, selectCampaigns could return undefined. It was account/device dependent because persisted state differs per user/install. Fix Applied: hardened both the restore path and selector path to fallback to initial state (empty array or object instead of undefined) ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/RWDS-1267 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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] > **Low Risk** > Low risk: changes are defensive fallbacks for missing/undefined persisted state and selector inputs, with added tests; behavior should only differ for legacy/partial states. > > **Overview** > Prevents Rewards tab crashes caused by older/partial persisted Redux/controller state by **defaulting missing fields to safe empty values**. > > Selectors and rehydrate logic now coalesce `undefined` arrays/objects (e.g., `campaigns`, season arrays, `benefits`, dismissed-toasts map, controller `accounts`/`subscriptions`) to empty defaults, and `RewardsController` default state was extracted into `defaultState.ts` and reused. Test coverage was expanded to lock in these upgrade/undefined-state behaviors. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit d7b351d0a795e0016dc2f4a686382955d3b4336b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .../hooks/useCampaignOutcomeToast.test.ts | 12 +- .../rewards-controller/RewardsController.ts | 35 +----- .../rewards-controller/defaultState.ts | 32 ++++++ app/reducers/rewards/index.test.ts | 47 ++++++++ app/reducers/rewards/index.ts | 11 +- app/reducers/rewards/selectors.test.ts | 104 ++++++++++++++++++ app/reducers/rewards/selectors.ts | 44 +++++--- app/selectors/rewards/index.test.ts | 86 ++++++++++++++- app/selectors/rewards/index.ts | 17 ++- 9 files changed, 331 insertions(+), 57 deletions(-) create mode 100644 app/core/Engine/controllers/rewards-controller/defaultState.ts diff --git a/app/components/UI/Rewards/hooks/useCampaignOutcomeToast.test.ts b/app/components/UI/Rewards/hooks/useCampaignOutcomeToast.test.ts index 0f13d88bcafa..3c7982808f03 100644 --- a/app/components/UI/Rewards/hooks/useCampaignOutcomeToast.test.ts +++ b/app/components/UI/Rewards/hooks/useCampaignOutcomeToast.test.ts @@ -146,13 +146,13 @@ function setupDefaults({ subscriptionId = SUBSCRIPTION_ID, outcome = null, }: { - campaigns?: ReturnType[]; + campaigns?: ReturnType[] | undefined; dismissed?: Record; subscriptionId?: string | null; outcome?: BaseCampaignParticipantOutcomeDto | null; } = {}) { mockUseSelector.mockImplementation((selector) => { - if (selector === selectCampaigns) return campaigns; + if (selector === selectCampaigns) return campaigns ?? []; if (selector === selectDismissedCampaignOutcomeToasts) return dismissed; if (selector === selectRewardsSubscriptionId) return subscriptionId; return undefined; @@ -193,6 +193,14 @@ describe('useCampaignOutcomeToast', () => { expect(mockShowToast).not.toHaveBeenCalled(); }); + it('campaigns are missing from persisted state', () => { + setupDefaults({ campaigns: undefined }); + expect(() => + renderHook(() => useCampaignOutcomeToast(mockConfig)), + ).not.toThrow(); + expect(mockShowToast).not.toHaveBeenCalled(); + }); + it('subscriptionId is missing', () => { setupDefaults({ subscriptionId: null, diff --git a/app/core/Engine/controllers/rewards-controller/RewardsController.ts b/app/core/Engine/controllers/rewards-controller/RewardsController.ts index 65056cd9adbe..2509f28f37a8 100644 --- a/app/core/Engine/controllers/rewards-controller/RewardsController.ts +++ b/app/core/Engine/controllers/rewards-controller/RewardsController.ts @@ -52,6 +52,10 @@ import { BASE32_REGEX, CampaignType, } from './types'; +import { + defaultRewardsControllerState, + getRewardsControllerDefaultState, +} from './defaultState'; import type { RewardsControllerMessenger } from '../../messengers/rewards-controller-messenger'; import { storeSubscriptionToken, @@ -308,36 +312,7 @@ const metadata: StateMetadata = { }, }; -/** - * Get the default state for the RewardsController - */ -export const getRewardsControllerDefaultState = (): RewardsControllerState => ({ - activeAccount: null, - accounts: {}, - subscriptions: {}, - subscriptionBenefits: {}, - seasons: {}, - subscriptionReferralDetails: {}, - seasonStatuses: {}, - activeBoosts: {}, - unlockedRewards: {}, - pointsEvents: {}, - offDeviceSubscriptionAccounts: {}, - campaigns: {}, - campaignParticipantStatus: {}, - ondoCampaignLeaderboard: {}, - ondoCampaignLeaderboardPositions: {}, - ondoCampaignPortfolio: {}, - ondoCampaignActivity: {}, - ondoCampaignDeposits: {}, - perpsTradingCampaignLeaderboard: {}, - perpsTradingCampaignLeaderboardPositions: {}, - perpsTradingCampaignVolume: {}, - pointsEstimateHistory: [], - rewardsEnvUrl: null, -}); - -export const defaultRewardsControllerState = getRewardsControllerDefaultState(); +export { defaultRewardsControllerState, getRewardsControllerDefaultState }; type CacheReader = ( key: string, diff --git a/app/core/Engine/controllers/rewards-controller/defaultState.ts b/app/core/Engine/controllers/rewards-controller/defaultState.ts new file mode 100644 index 000000000000..16d1033d0ab1 --- /dev/null +++ b/app/core/Engine/controllers/rewards-controller/defaultState.ts @@ -0,0 +1,32 @@ +import type { RewardsControllerState } from './types'; + +/** + * Get the default state for the RewardsController. + */ +export const getRewardsControllerDefaultState = (): RewardsControllerState => ({ + activeAccount: null, + accounts: {}, + subscriptions: {}, + subscriptionBenefits: {}, + seasons: {}, + subscriptionReferralDetails: {}, + seasonStatuses: {}, + activeBoosts: {}, + unlockedRewards: {}, + pointsEvents: {}, + offDeviceSubscriptionAccounts: {}, + campaigns: {}, + campaignParticipantStatus: {}, + ondoCampaignLeaderboard: {}, + ondoCampaignLeaderboardPositions: {}, + ondoCampaignPortfolio: {}, + ondoCampaignActivity: {}, + ondoCampaignDeposits: {}, + perpsTradingCampaignLeaderboard: {}, + perpsTradingCampaignLeaderboardPositions: {}, + perpsTradingCampaignVolume: {}, + pointsEstimateHistory: [], + rewardsEnvUrl: null, +}); + +export const defaultRewardsControllerState = getRewardsControllerDefaultState(); diff --git a/app/reducers/rewards/index.test.ts b/app/reducers/rewards/index.test.ts index 59079e538a40..fc5b0514d260 100644 --- a/app/reducers/rewards/index.test.ts +++ b/app/reducers/rewards/index.test.ts @@ -2328,6 +2328,27 @@ describe('rewardsReducer', () => { ); }); + it('should default persisted season arrays to empty arrays when absent', () => { + const persistedRewardsStateWithoutFields = { + ...initialState, + seasonTiers: undefined, + seasonActivityTypes: undefined, + seasonWaysToEarn: undefined, + } as unknown as RewardsState; + const rehydrateAction = { + type: 'persist/REHYDRATE', + payload: { + rewards: persistedRewardsStateWithoutFields, + }, + }; + + const state = rewardsReducer(initialState, rehydrateAction); + + expect(state.seasonTiers).toEqual([]); + expect(state.seasonActivityTypes).toEqual([]); + expect(state.seasonWaysToEarn).toEqual([]); + }); + it('should preserve all persisted UI state fields', () => { // Arrange const persistedRewardsState: RewardsState = { @@ -2681,6 +2702,21 @@ describe('rewardsReducer', () => { expect(state.ondoCampaignActivity).toEqual({}); }); + + it('should default campaigns to [] when absent from persisted state (upgrade path)', () => { + const persistedRewardsStateWithoutField = { + ...initialState, + campaigns: undefined, + } as unknown as RewardsState; + const rehydrateAction = { + type: 'persist/REHYDRATE', + payload: { rewards: persistedRewardsStateWithoutField }, + }; + + const state = rewardsReducer(initialState, rehydrateAction); + + expect(state.campaigns).toEqual([]); + }); }); describe('unknown actions', () => { @@ -4623,6 +4659,17 @@ describe('setBenefits', () => { expect(state.benefits).toEqual(mockBenefitsPayload.benefits); }); + it('sets benefits to empty array when payload benefits are missing', () => { + const action = setBenefits({ + ...mockBenefitsPayload, + benefits: undefined, + } as unknown as typeof mockBenefitsPayload); + + const state = rewardsReducer(initialState, action); + + expect(state.benefits).toEqual([]); + }); + it('replaces existing benefits with new payload benefits', () => { const stateWithBenefits: RewardsState = { ...initialState, diff --git a/app/reducers/rewards/index.ts b/app/reducers/rewards/index.ts index e35e437aaa32..af29b06ca572 100644 --- a/app/reducers/rewards/index.ts +++ b/app/reducers/rewards/index.ts @@ -687,7 +687,7 @@ const rewardsSlice = createSlice({ }, setBenefits: (state, action: PayloadAction) => { - state.benefits = action.payload.benefits; + state.benefits = action.payload.benefits ?? []; }, setBenefitsLoading: (state, action: PayloadAction) => { @@ -897,9 +897,10 @@ const rewardsSlice = createSlice({ seasonName: action.payload.rewards.seasonName, seasonStartDate: action.payload.rewards.seasonStartDate, seasonEndDate: action.payload.rewards.seasonEndDate, - seasonTiers: action.payload.rewards.seasonTiers, - seasonActivityTypes: action.payload.rewards.seasonActivityTypes, - seasonWaysToEarn: action.payload.rewards.seasonWaysToEarn, + seasonTiers: action.payload.rewards.seasonTiers ?? [], + seasonActivityTypes: + action.payload.rewards.seasonActivityTypes ?? [], + seasonWaysToEarn: action.payload.rewards.seasonWaysToEarn ?? [], referralCode: action.payload.rewards.referralCode, refereeCount: action.payload.rewards.refereeCount, currentTier: action.payload.rewards.currentTier, @@ -912,7 +913,7 @@ const rewardsSlice = createSlice({ activeBoosts: action.payload.rewards.activeBoosts, pointsEvents: action.payload.rewards.pointsEvents, unlockedRewards: action.payload.rewards.unlockedRewards, - campaigns: action.payload.rewards.campaigns, + campaigns: action.payload.rewards.campaigns ?? [], campaignParticipantStatuses: action.payload.rewards.campaignParticipantStatuses ?? {}, ondoCampaignLeaderboardPositions: diff --git a/app/reducers/rewards/selectors.test.ts b/app/reducers/rewards/selectors.test.ts index 9519b4cb7788..3cb37ace0b3e 100644 --- a/app/reducers/rewards/selectors.test.ts +++ b/app/reducers/rewards/selectors.test.ts @@ -46,6 +46,7 @@ import { selectBulkLinkFailedAccounts, selectBulkLinkWasInterrupted, selectBulkLinkAccountProgress, + selectBenefits, selectCampaigns, selectCampaignsLoading, selectCampaignsError, @@ -85,6 +86,7 @@ import { SeasonWayToEarnDto, PointsEventDto, OndoGmActivityEntryDto, + SubscriptionBenefitDto, } from '../../core/Engine/controllers/rewards-controller/types'; import { RootState } from '..'; import { RewardsState, AccountOptInBannerInfoStatus } from '.'; @@ -550,6 +552,16 @@ describe('Rewards selectors', () => { expect(result.current).toEqual([]); }); + it('returns empty array when season tiers are undefined', () => { + const mockState = { + rewards: { seasonTiers: undefined }, + } as unknown as RootState; + mockedUseSelector.mockImplementation((selector) => selector(mockState)); + + const { result } = renderHook(() => useSelector(selectSeasonTiers)); + expect(result.current).toEqual([]); + }); + it('returns season tiers when set', () => { const mockTiers: SeasonTierDto[] = [ { @@ -605,6 +617,18 @@ describe('Rewards selectors', () => { expect(result.current).toEqual([]); }); + it('returns empty array when season activity types are undefined', () => { + const mockState = { + rewards: { seasonActivityTypes: undefined }, + } as unknown as RootState; + mockedUseSelector.mockImplementation((selector) => selector(mockState)); + + const { result } = renderHook(() => + useSelector(selectSeasonActivityTypes), + ); + expect(result.current).toEqual([]); + }); + it('returns season activity types when set', () => { const mockActivityTypes: SeasonActivityTypeDto[] = [ { @@ -639,6 +663,16 @@ describe('Rewards selectors', () => { expect(result.current).toEqual([]); }); + it('returns empty array when season ways to earn are undefined', () => { + const mockState = { + rewards: { seasonWaysToEarn: undefined }, + } as unknown as RootState; + mockedUseSelector.mockImplementation((selector) => selector(mockState)); + + const { result } = renderHook(() => useSelector(selectSeasonWaysToEarn)); + expect(result.current).toEqual([]); + }); + it('returns season ways to earn when set', () => { const mockWaysToEarn: SeasonWayToEarnDto[] = [ { @@ -1033,6 +1067,18 @@ describe('Rewards selectors', () => { expect(result.current).toHaveLength(0); }); + it('returns empty array when account banner config is undefined', () => { + const mockState = { + rewards: { hideCurrentAccountNotOptedInBanner: undefined }, + } as unknown as RootState; + mockedUseSelector.mockImplementation((selector) => selector(mockState)); + + const { result } = renderHook(() => + useSelector(selectHideCurrentAccountNotOptedInBannerArray), + ); + expect(result.current).toEqual([]); + }); + it('returns single account configuration when set', () => { const mockAccountConfig: AccountOptInBannerInfoStatus = { accountGroupId: 'keyring:wallet1/1', @@ -3120,6 +3166,37 @@ describe('Rewards selectors', () => { showUpcomingDate: false, }; + describe('selectBenefits', () => { + const mockBenefit: SubscriptionBenefitDto = { + id: 101, + longTitle: 'Premium Access', + shortDescription: 'Get premium perks', + longDescription: 'Unlock premium partner benefits.', + thumbnail: 'https://example.com/benefits/premium.png', + validFrom: '2026-01-01T00:00:00.000Z', + validTo: '2026-12-31T00:00:00.000Z', + actionDate: '2026-06-01T00:00:00.000Z', + url: 'https://example.com/claim', + chain: 'ethereum', + type: { + id: 1, + name: 'Partner', + }, + }; + + it('returns empty array when benefits are undefined', () => { + const state = createMockRootState({ + benefits: undefined as unknown as SubscriptionBenefitDto[], + }); + expect(selectBenefits(state)).toEqual([]); + }); + + it('returns benefits when they exist', () => { + const state = createMockRootState({ benefits: [mockBenefit] }); + expect(selectBenefits(state)).toEqual([mockBenefit]); + }); + }); + describe('selectCampaigns', () => { it('returns empty array when campaigns is empty', () => { const mockState = { rewards: { campaigns: [] } }; @@ -3129,6 +3206,16 @@ describe('Rewards selectors', () => { expect(result.current).toEqual([]); }); + it('returns empty array when campaigns is undefined', () => { + const mockState = { + rewards: { campaigns: undefined }, + } as unknown as RootState; + mockedUseSelector.mockImplementation((selector) => selector(mockState)); + + const { result } = renderHook(() => useSelector(selectCampaigns)); + expect(result.current).toEqual([]); + }); + it('returns campaigns array when campaigns exist', () => { const mockState = { rewards: { campaigns: [mockCampaign] } }; mockedUseSelector.mockImplementation((selector) => selector(mockState)); @@ -3143,6 +3230,13 @@ describe('Rewards selectors', () => { expect(selectCampaigns(state)).toEqual([]); }); + it('returns empty array when campaigns is undefined', () => { + const state = createMockRootState({ + campaigns: undefined as unknown as CampaignDto[], + }); + expect(selectCampaigns(state)).toEqual([]); + }); + it('returns campaigns when they exist', () => { const state = createMockRootState({ campaigns: [mockCampaign] }); expect(selectCampaigns(state)).toEqual([mockCampaign]); @@ -3861,6 +3955,16 @@ describe('Rewards selectors', () => { expect(selectDismissedCampaignOutcomeToasts(state)).toEqual({}); }); + it('returns empty object when dismissed toasts are undefined', () => { + const state = createMockRootState({ + dismissedCampaignOutcomeToasts: undefined as unknown as Record< + string, + boolean + >, + }); + expect(selectDismissedCampaignOutcomeToasts(state)).toEqual({}); + }); + it('returns the dismissed toasts map', () => { const dismissed = { 'campaign-1:sub-1:winner': true, diff --git a/app/reducers/rewards/selectors.ts b/app/reducers/rewards/selectors.ts index 83c12f18cc98..fce1475bf100 100644 --- a/app/reducers/rewards/selectors.ts +++ b/app/reducers/rewards/selectors.ts @@ -1,8 +1,8 @@ import { createSelector } from 'reselect'; -import { RootState } from '..'; +import type { RootState } from '..'; +import { initialState } from '.'; import { RewardsTab, OnboardingStep } from './types'; import { hasMinimumRequiredVersion } from '../../util/remoteFeatureFlag'; -import { SubscriptionBenefitDto } from '../../core/Engine/controllers/rewards-controller/types.ts'; export const selectActiveTab = (state: RootState): RewardsTab => state.rewards.activeTab; @@ -49,14 +49,20 @@ export const selectSeasonStartDate = (state: RootState) => export const selectSeasonEndDate = (state: RootState) => state.rewards.seasonEndDate; -export const selectSeasonTiers = (state: RootState) => - state.rewards.seasonTiers; +export const selectSeasonTiers = ( + state: RootState, +): RootState['rewards']['seasonTiers'] => + state.rewards.seasonTiers ?? initialState.seasonTiers; -export const selectSeasonActivityTypes = (state: RootState) => - state.rewards.seasonActivityTypes; +export const selectSeasonActivityTypes = ( + state: RootState, +): RootState['rewards']['seasonActivityTypes'] => + state.rewards.seasonActivityTypes ?? initialState.seasonActivityTypes; -export const selectSeasonWaysToEarn = (state: RootState) => - state.rewards.seasonWaysToEarn; +export const selectSeasonWaysToEarn = ( + state: RootState, +): RootState['rewards']['seasonWaysToEarn'] => + state.rewards.seasonWaysToEarn ?? initialState.seasonWaysToEarn; export const selectOnboardingActiveStep = (state: RootState): OnboardingStep => state.rewards.onboardingActiveStep; @@ -90,7 +96,9 @@ export const selectHideUnlinkedAccountsBanner = (state: RootState) => export const selectHideCurrentAccountNotOptedInBannerArray = ( state: RootState, -) => state.rewards.hideCurrentAccountNotOptedInBanner; +): RootState['rewards']['hideCurrentAccountNotOptedInBanner'] => + state.rewards.hideCurrentAccountNotOptedInBanner ?? + initialState.hideCurrentAccountNotOptedInBanner; export const selectActiveBoosts = (state: RootState) => state.rewards.activeBoosts; @@ -152,14 +160,19 @@ export const selectBulkLinkAccountProgress = (state: RootState) => { }; // Benefits selectors -export const selectBenefits = (state: RootState): SubscriptionBenefitDto[] => - state.rewards.benefits; +export const selectBenefits = ( + state: RootState, +): RootState['rewards']['benefits'] => + state.rewards.benefits ?? initialState.benefits; export const selectBenefitsLoading = (state: RootState): boolean => state.rewards.benefitsLoading; // Campaigns selectors -export const selectCampaigns = (state: RootState) => state.rewards.campaigns; +export const selectCampaigns = ( + state: RootState, +): RootState['rewards']['campaigns'] => + state.rewards.campaigns ?? initialState.campaigns; export const selectCampaignById = (campaignId: string) => (state: RootState) => state.rewards.campaigns?.find((c) => c.id === campaignId) ?? null; @@ -317,8 +330,11 @@ export const selectOndoCampaignDepositsError = (state: RootState) => export const selectPendingDeeplink = (state: RootState) => state.rewards.pendingDeeplink; -export const selectDismissedCampaignOutcomeToasts = (state: RootState) => - state.rewards.dismissedCampaignOutcomeToasts; +export const selectDismissedCampaignOutcomeToasts = ( + state: RootState, +): RootState['rewards']['dismissedCampaignOutcomeToasts'] => + state.rewards.dismissedCampaignOutcomeToasts ?? + initialState.dismissedCampaignOutcomeToasts; // Perps Trading Campaign leaderboard selectors export const selectPerpsTradingCampaignLeaderboard = (state: RootState) => diff --git a/app/selectors/rewards/index.test.ts b/app/selectors/rewards/index.test.ts index 40c19bc21974..6d2db2a7f16e 100644 --- a/app/selectors/rewards/index.test.ts +++ b/app/selectors/rewards/index.test.ts @@ -65,7 +65,13 @@ describe('Rewards Selectors', () => { const result = selectRewardsControllerState(state); // Assert - expect(result).toBeUndefined(); + expect(result).toEqual( + expect.objectContaining({ + activeAccount: null, + accounts: {}, + subscriptions: {}, + }), + ); }); }); @@ -177,6 +183,24 @@ describe('Rewards Selectors', () => { // Assert expect(result).toBeNull(); }); + + it('returns null when RewardsController is undefined', () => { + // Arrange + const state = { + engine: { + backgroundState: { + RewardsController: undefined, + }, + }, + rewards: { candidateSubscriptionId: null }, + } as unknown as RootState; + + // Act + const result = selectRewardsSubscriptionId(state); + + // Assert + expect(result).toBeNull(); + }); }); describe('selectRewardsActiveAccountAddress', () => { @@ -249,6 +273,23 @@ describe('Rewards Selectors', () => { expect(result).toBeNull(); }); + it('returns null when RewardsController is undefined', () => { + // Arrange + const state = { + engine: { + backgroundState: { + RewardsController: undefined, + }, + }, + } as unknown as RootState; + + // Act + const result = selectRewardsActiveAccountAddress(state); + + // Assert + expect(result).toBeNull(); + }); + it('returns null when active account has no account property', () => { // Arrange const state = createMockRootState({ @@ -395,6 +436,49 @@ describe('Rewards Selectors', () => { expect(result).toHaveLength(0); }); + it('returns empty array when controller accounts are missing', () => { + const state = { + engine: { + backgroundState: { + RewardsController: { + activeAccount: { subscriptionId: 'sub-1' }, + accounts: undefined, + subscriptions: { + 'sub-1': { id: 'sub-1' }, + }, + }, + }, + }, + rewards: { candidateSubscriptionId: null }, + } as unknown as RootState; + + const result = selectCurrentSubscriptionAccounts(state); + expect(result).toEqual([]); + }); + + it('returns empty array when controller subscriptions are missing', () => { + const state = { + engine: { + backgroundState: { + RewardsController: { + activeAccount: { subscriptionId: 'sub-1' }, + accounts: { + 'acct-1': { + subscriptionId: 'sub-1', + account: 'eip155:1:0x123', + }, + }, + subscriptions: undefined, + }, + }, + }, + rewards: { candidateSubscriptionId: null }, + } as unknown as RootState; + + const result = selectCurrentSubscriptionAccounts(state); + expect(result).toEqual([]); + }); + it('returns empty array when no accounts match subscription', () => { const state = { engine: { diff --git a/app/selectors/rewards/index.ts b/app/selectors/rewards/index.ts index ca99456407f2..c91b88dc3382 100644 --- a/app/selectors/rewards/index.ts +++ b/app/selectors/rewards/index.ts @@ -1,14 +1,21 @@ import { createSelector } from '@reduxjs/toolkit'; import { RootState } from '../../reducers'; -import { RewardsAccountState } from '../../core/Engine/controllers/rewards-controller/types'; +import type { + RewardsAccountState, + RewardsControllerState, +} from '../../core/Engine/controllers/rewards-controller/types'; +import { defaultRewardsControllerState } from '../../core/Engine/controllers/rewards-controller/defaultState'; /** * * @param state - Root redux state * @returns - AccountsController state */ -export const selectRewardsControllerState = (state: RootState) => - state.engine.backgroundState.RewardsController; +export const selectRewardsControllerState = ( + state: RootState, +): RewardsControllerState => + state.engine.backgroundState.RewardsController ?? + defaultRewardsControllerState; export const selectRewardsActiveAccountSubscriptionId = createSelector( selectRewardsControllerState, @@ -57,14 +64,14 @@ export const selectCurrentSubscription = createSelector( [selectRewardsSubscriptionId, selectRewardsControllerState], (subscriptionId, rewardsState) => subscriptionId - ? (rewardsState.subscriptions[subscriptionId] ?? null) + ? (rewardsState.subscriptions?.[subscriptionId] ?? null) : null, ); export const selectCurrentSubscriptionAccounts = createSelector( [selectRewardsControllerState, selectCurrentSubscription], (rewardsState, subscription) => - Object.values(rewardsState.accounts).filter( + Object.values(rewardsState.accounts ?? {}).filter( (account: RewardsAccountState) => account.subscriptionId === subscription?.id, ), From 8136f4fd685a1cf7cf2bedaf7a0cf23e2401ae71 Mon Sep 17 00:00:00 2001 From: Alexey Kureev Date: Thu, 7 May 2026 06:11:19 +0200 Subject: [PATCH 3/3] feat(money): swap bottom-bar Money icon for Dollar glyph (MUSD-773) (#29813) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Replaces the legacy `Bank` icon used by the Money tab in the bottom navigation bar with the new Dollar glyph from the Figma spec (MUSD-773), in both selected (filled) and unselected (outline) states. - Adds two new SVG assets: `dollar.svg` (outlined circle + dollar sign) and `dollar-filled.svg` (solid circle with dollar-sign cutout). - Both SVGs use `currentColor` for stroke and fill so the Icon component can tint them per theme — they pick up the correct active/inactive color in both light and dark mode, matching the convention used by the other tab bar icons (`Home`/`HomeFilled`, `MetamaskFoxOutline`/`MetamaskFoxFilled`). - The filled variant uses `fill-rule="evenodd"` so the dollar sign is a transparent cutout through the colored circle, showing the tab-bar background through it (avoids hardcoded light/dark color pairs). - Wires the new icons in `TabBar.constants.ts` (inactive → `IconName.Dollar`) and `TabBar.tsx` (`FILLED_ICONS` map → `IconName.DollarFilled`). - Adds `Dollar` and `DollarFilled` to the local `IconName` enum and asset map by hand to avoid the side-effect rewrite that `yarn generate-icons` would have done to the rest of the enum. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [MUSD-773](https://consensyssoftware.atlassian.net/browse/MUSD-773) ## **Manual testing steps** ```gherkin Feature: Money tab uses the new Dollar icon in both states and themes Scenario: Money tab is unselected in light mode Given the app is in light mode And the user is not on the Money tab Then the Money tab icon is the outlined Dollar glyph And its stroke and dollar sign use the inactive icon tint Scenario: Money tab is unselected in dark mode Given the app is in dark mode And the user is not on the Money tab Then the Money tab icon is the outlined Dollar glyph And its stroke and dollar sign use the inactive icon tint Scenario: Money tab is selected in light mode Given the app is in light mode When the user taps the Money tab Then the Money tab icon switches to the filled Dollar glyph And the circle paints in the active icon tint And the dollar sign is transparent, showing the tab-bar background through it Scenario: Money tab is selected in dark mode Given the app is in dark mode When the user taps the Money tab Then the Money tab icon switches to the filled Dollar glyph And the circle paints in the active icon tint And the dollar sign is transparent, showing the tab-bar background through it ``` ## **Screenshots/Recordings** ### **Before** ### **After** image image ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I've included tests if applicable - [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - [ ] I've tested with a power user scenario - [ ] I've instrumented key operations with Sentry traces for production performance metrics ## **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. [MUSD-773]: https://consensyssoftware.atlassian.net/browse/MUSD-773?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- > [!NOTE] > **Low Risk** > Low risk UI-only change that swaps icon assets and enum entries; main risk is any missed references to the removed/renamed `IconName` values causing build-time errors. > > **Overview** > Updates the bottom navigation **Money** tab to use new `Musd`/`MusdFilled` icon assets instead of the legacy bank glyph, wiring them through the tab bar icon maps. > > Extends the icon system with new `IconName` entries and SVG assets, and updates Portfolio-related tab UI/tests to use `IconName.PieChart` in place of the removed `IconName.Portfolio` (including `HomepageDiscoveryTabs`). > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ee491bb2140cf4f57d2e860f84c5c5216c79780e. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --------- Co-authored-by: Cursor Agent Co-authored-by: Brian August Nguyen --- .../Tabs/TabsIconBar/TabsIconBar.test.tsx | 6 +-- .../Tabs/TabsIconList/TabsIconList.test.tsx | 38 +++++++++---------- .../Tabs/TabsIconTab/TabsIconTab.test.tsx | 4 +- .../components/Icons/Icon/Icon.assets.ts | 12 ++++-- .../components/Icons/Icon/Icon.types.ts | 6 ++- .../Icons/Icon/assets/musd-filled.svg | 1 + .../components/Icons/Icon/assets/musd.svg | 1 + .../Navigation/TabBar/TabBar.constants.ts | 2 +- .../components/Navigation/TabBar/TabBar.tsx | 2 +- .../HomepageDiscoveryTabs.tsx | 2 +- 10 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 app/component-library/components/Icons/Icon/assets/musd-filled.svg create mode 100644 app/component-library/components/Icons/Icon/assets/musd.svg diff --git a/app/component-library/components-temp/Tabs/TabsIconBar/TabsIconBar.test.tsx b/app/component-library/components-temp/Tabs/TabsIconBar/TabsIconBar.test.tsx index e9f10fbd9479..b27f4fee5ab6 100644 --- a/app/component-library/components-temp/Tabs/TabsIconBar/TabsIconBar.test.tsx +++ b/app/component-library/components-temp/Tabs/TabsIconBar/TabsIconBar.test.tsx @@ -21,7 +21,7 @@ describe('TabsIconBar', () => { { key: 'tab1', label: 'Portfolio', - iconName: IconName.Portfolio, + iconName: IconName.PieChart, content: null, }, { @@ -118,7 +118,7 @@ describe('TabsIconBar', () => { { key: 'tab1', label: 'Portfolio', - iconName: IconName.Portfolio, + iconName: IconName.PieChart, content: null, }, { @@ -393,7 +393,7 @@ describe('TabsIconBar', () => { { key: 'new1', label: 'New A', - iconName: IconName.Portfolio, + iconName: IconName.PieChart, content: null, }, { diff --git a/app/component-library/components-temp/Tabs/TabsIconList/TabsIconList.test.tsx b/app/component-library/components-temp/Tabs/TabsIconList/TabsIconList.test.tsx index 4b2977811447..64b781dfcadc 100644 --- a/app/component-library/components-temp/Tabs/TabsIconList/TabsIconList.test.tsx +++ b/app/component-library/components-temp/Tabs/TabsIconList/TabsIconList.test.tsx @@ -47,7 +47,7 @@ describe('TabsIconList', () => { Portfolio Content @@ -68,7 +68,7 @@ describe('TabsIconList', () => { Content 1 @@ -90,7 +90,7 @@ describe('TabsIconList', () => { @@ -115,7 +115,7 @@ describe('TabsIconList', () => { @@ -140,7 +140,7 @@ describe('TabsIconList', () => { Content 1 @@ -156,7 +156,7 @@ describe('TabsIconList', () => { Portfolio Content @@ -180,7 +180,7 @@ describe('TabsIconList', () => { Content 1 @@ -207,7 +207,7 @@ describe('TabsIconList', () => { Content 1 @@ -237,7 +237,7 @@ describe('TabsIconList', () => { Content 1 @@ -265,7 +265,7 @@ describe('TabsIconList', () => { Content 1 @@ -296,7 +296,7 @@ describe('TabsIconList', () => { Content 1 @@ -326,7 +326,7 @@ describe('TabsIconList', () => { Content 1 @@ -359,7 +359,7 @@ describe('TabsIconList', () => { Content 1 @@ -395,7 +395,7 @@ describe('TabsIconList', () => { Content 1 @@ -420,7 +420,7 @@ describe('TabsIconList', () => { Content 1 @@ -444,7 +444,7 @@ describe('TabsIconList', () => { Content 1 @@ -474,7 +474,7 @@ describe('TabsIconList', () => { { key: 'p-tab', label: 'Portfolio', - icon: IconName.Portfolio, + icon: IconName.PieChart, content: 'Portfolio Content', }, { @@ -526,7 +526,7 @@ describe('TabsIconList', () => { { key: 'p-tab', label: 'Portfolio', - icon: IconName.Portfolio, + icon: IconName.PieChart, content: 'Portfolio Content', }, { @@ -555,7 +555,7 @@ describe('TabsIconList', () => { Portfolio Content diff --git a/app/component-library/components-temp/Tabs/TabsIconTab/TabsIconTab.test.tsx b/app/component-library/components-temp/Tabs/TabsIconTab/TabsIconTab.test.tsx index 6789e8f0e0b8..52bafa0e5ca9 100644 --- a/app/component-library/components-temp/Tabs/TabsIconTab/TabsIconTab.test.tsx +++ b/app/component-library/components-temp/Tabs/TabsIconTab/TabsIconTab.test.tsx @@ -9,7 +9,7 @@ import { IconName } from '../../../components/Icons/Icon/Icon.types'; describe('TabsIconTab', () => { const defaultProps = { label: 'Portfolio', - iconName: IconName.Portfolio, + iconName: IconName.PieChart, isActive: false, onPress: jest.fn(), }; @@ -108,7 +108,7 @@ describe('TabsIconTab', () => { describe('Icon', () => { it('renders all supported icon names without throwing', () => { const icons: IconName[] = [ - IconName.Portfolio, + IconName.PieChart, IconName.Candlestick, IconName.Predictions, ]; diff --git a/app/component-library/components/Icons/Icon/Icon.assets.ts b/app/component-library/components/Icons/Icon/Icon.assets.ts index 362637579fdd..97625ce00701 100644 --- a/app/component-library/components/Icons/Icon/Icon.assets.ts +++ b/app/component-library/components/Icons/Icon/Icon.assets.ts @@ -167,6 +167,8 @@ import monitorSVG from './assets/monitor.svg'; import morehorizontalSVG from './assets/more-horizontal.svg'; import moreverticalSVG from './assets/more-vertical.svg'; import mountainflagSVG from './assets/mountain-flag.svg'; +import musdfilledSVG from './assets/musd-filled.svg'; +import musdSVG from './assets/musd.svg'; import musicnoteSVG from './assets/music-note.svg'; import nophotographySVG from './assets/no-photography.svg'; import notificationSVG from './assets/notification.svg'; @@ -176,13 +178,13 @@ import passwordcheckSVG from './assets/password-check.svg'; import pendingSVG from './assets/pending.svg'; import peopleSVG from './assets/people.svg'; import personcancelSVG from './assets/person-cancel.svg'; +import piechartSVG from './assets/pie-chart.svg'; import pinSVG from './assets/pin.svg'; import plantSVG from './assets/plant.svg'; -import pieChartSVG from './assets/pie-chart.svg'; -import predictionsSVG from './assets/predictions.svg'; import plugSVG from './assets/plug.svg'; import plusandminusSVG from './assets/plus-and-minus.svg'; import policyalertSVG from './assets/policy-alert.svg'; +import predictionsSVG from './assets/predictions.svg'; import printSVG from './assets/print.svg'; import priorityhighSVG from './assets/priority-high.svg'; import privacytipSVG from './assets/privacy-tip.svg'; @@ -454,6 +456,8 @@ export const assetByIconName: AssetByIconName = { [IconName.MoreHorizontal]: morehorizontalSVG, [IconName.MoreVertical]: moreverticalSVG, [IconName.MountainFlag]: mountainflagSVG, + [IconName.MusdFilled]: musdfilledSVG, + [IconName.Musd]: musdSVG, [IconName.MusicNote]: musicnoteSVG, [IconName.NoPhotography]: nophotographySVG, [IconName.Notification]: notificationSVG, @@ -463,13 +467,13 @@ export const assetByIconName: AssetByIconName = { [IconName.Pending]: pendingSVG, [IconName.People]: peopleSVG, [IconName.PersonCancel]: personcancelSVG, + [IconName.PieChart]: piechartSVG, [IconName.Pin]: pinSVG, [IconName.Plant]: plantSVG, - [IconName.Portfolio]: pieChartSVG, - [IconName.Predictions]: predictionsSVG, [IconName.Plug]: plugSVG, [IconName.PlusAndMinus]: plusandminusSVG, [IconName.PolicyAlert]: policyalertSVG, + [IconName.Predictions]: predictionsSVG, [IconName.Print]: printSVG, [IconName.PriorityHigh]: priorityhighSVG, [IconName.PrivacyTip]: privacytipSVG, diff --git a/app/component-library/components/Icons/Icon/Icon.types.ts b/app/component-library/components/Icons/Icon/Icon.types.ts index 63a92415a656..649011ddab5d 100644 --- a/app/component-library/components/Icons/Icon/Icon.types.ts +++ b/app/component-library/components/Icons/Icon/Icon.types.ts @@ -237,6 +237,8 @@ export enum IconName { MoreHorizontal = 'MoreHorizontal', MoreVertical = 'MoreVertical', MountainFlag = 'MountainFlag', + MusdFilled = 'MusdFilled', + Musd = 'Musd', MusicNote = 'MusicNote', NoPhotography = 'NoPhotography', Notification = 'Notification', @@ -246,13 +248,13 @@ export enum IconName { Pending = 'Pending', People = 'People', PersonCancel = 'PersonCancel', + PieChart = 'PieChart', Pin = 'Pin', Plant = 'Plant', - Portfolio = 'Portfolio', - Predictions = 'Predictions', Plug = 'Plug', PlusAndMinus = 'PlusAndMinus', PolicyAlert = 'PolicyAlert', + Predictions = 'Predictions', Print = 'Print', PriorityHigh = 'PriorityHigh', PrivacyTip = 'PrivacyTip', diff --git a/app/component-library/components/Icons/Icon/assets/musd-filled.svg b/app/component-library/components/Icons/Icon/assets/musd-filled.svg new file mode 100644 index 000000000000..d960542501cc --- /dev/null +++ b/app/component-library/components/Icons/Icon/assets/musd-filled.svg @@ -0,0 +1 @@ + diff --git a/app/component-library/components/Icons/Icon/assets/musd.svg b/app/component-library/components/Icons/Icon/assets/musd.svg new file mode 100644 index 000000000000..f65d9cc79cdd --- /dev/null +++ b/app/component-library/components/Icons/Icon/assets/musd.svg @@ -0,0 +1 @@ + diff --git a/app/component-library/components/Navigation/TabBar/TabBar.constants.ts b/app/component-library/components/Navigation/TabBar/TabBar.constants.ts index 619bcb374569..6faa1e92b11d 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.constants.ts +++ b/app/component-library/components/Navigation/TabBar/TabBar.constants.ts @@ -14,7 +14,7 @@ export const ICON_BY_TAB_BAR_ICON_KEY: IconByTabBarIconKey = { [TabBarIconKey.Setting]: IconName.Setting, [TabBarIconKey.Rewards]: IconName.MetamaskFoxOutline, [TabBarIconKey.Trending]: IconName.Search, - [TabBarIconKey.Money]: IconName.Bank, + [TabBarIconKey.Money]: IconName.Musd, }; export const LABEL_BY_TAB_BAR_ICON_KEY = { diff --git a/app/component-library/components/Navigation/TabBar/TabBar.tsx b/app/component-library/components/Navigation/TabBar/TabBar.tsx index f3d8b1a8f70c..10ccc18c8c2b 100644 --- a/app/component-library/components/Navigation/TabBar/TabBar.tsx +++ b/app/component-library/components/Navigation/TabBar/TabBar.tsx @@ -34,7 +34,7 @@ const FILLED_ICONS: Partial> = { [TabBarIconKey.Activity]: IconName.ClockFilled, [TabBarIconKey.Trending]: IconName.Search, [TabBarIconKey.Rewards]: IconName.MetamaskFoxFilled, - [TabBarIconKey.Money]: IconName.Bank, + [TabBarIconKey.Money]: IconName.MusdFilled, }; const TabBar = ({ state, descriptors, navigation }: TabBarProps) => { diff --git a/app/components/Views/Homepage/components/HomepageDiscoveryTabs/HomepageDiscoveryTabs.tsx b/app/components/Views/Homepage/components/HomepageDiscoveryTabs/HomepageDiscoveryTabs.tsx index 51c2f1ff9bce..90f1fe151986 100644 --- a/app/components/Views/Homepage/components/HomepageDiscoveryTabs/HomepageDiscoveryTabs.tsx +++ b/app/components/Views/Homepage/components/HomepageDiscoveryTabs/HomepageDiscoveryTabs.tsx @@ -284,7 +284,7 @@ const HomepageDiscoveryTabs = forwardRef< style: { zIndex: 2 }, }} > - +