Skip to content

Commit 2e4ef85

Browse files
authored
chore: migrate off of useFeatureFlag (MetaMask#24125)
## **Description** This PR migrates feature flag checks from the custom `useFeatureFlag` hook to Redux selectors, aligning with the codebase's feature flag architecture guidelines. ### Reason for change: - The existing `useFeatureFlag` hook was creating an inconsistent pattern for accessing feature flags - Redux selectors provide better memoization, testability, and integration with the existing state management architecture - Centralizes feature flag access patterns for better maintainability ### Solution: - Added new Redux selectors for OTA Updates, Full Page Account List, and Rewards feature flags - Each selector includes both a "raw" version (direct flag value) and a combined version that respects the `basicFunctionalityEnabled` setting - Updated all consuming components to use `useSelector` with the new selectors - Added comprehensive test coverage for all new selectors - Removed the deprecated `useFeatureFlag` hook ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** Feature: Feature Flag Selectors Scenario: OTA Updates flag respects basic functionality setting Given the app has basic functionality enabled And the remote feature flag for OTA updates is enabled When the app checks if OTA updates are enabled Then the selector returns true Scenario: Full Page Account List flag is disabled when basic functionality is off Given the app has basic functionality disabled And the remote feature flag for full page account list is enabled When the AccountSelector component renders Then it uses the non-full-page account list UI Scenario: Rewards flags work correctly Given the app has basic functionality enabled And the rewards feature flags are enabled remotely When the WaysToEarn component checks mUSD holding flag Then the appropriate rewards options are displayed## **Screenshots/Recordings** ### **Before** N/A - Internal refactoring with no visual changes ### **After** N/A - Internal refactoring with no visual changes ## **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. ## **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.
1 parent 2ca6335 commit 2e4ef85

18 files changed

Lines changed: 713 additions & 252 deletions

File tree

app/components/UI/Rewards/components/Tabs/OverviewTab/WaysToEarn/WaysToEarn.test.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
selectRewardsCardSpendFeatureFlags,
1212
selectRewardsMusdDepositEnabledFlag,
1313
} from '../../../../../../../selectors/featureFlagController/rewards';
14+
import { selectMusdHoldingEnabledFlag } from '../../../../../../../selectors/featureFlagController/rewards/rewardsEnabled';
1415
import { selectPredictEnabledFlag } from '../../../../../Predict/selectors/featureFlags';
1516
import { MetaMetricsEvents } from '../../../../../../hooks/useMetrics';
1617
import { RewardsMetricsButtons } from '../../../../utils';
@@ -80,20 +81,13 @@ jest.mock('react-redux', () => ({
8081
useSelector: jest.fn(),
8182
}));
8283

83-
// Mock useFeatureFlag hook
84-
jest.mock('../../../../../../../components/hooks/useFeatureFlag', () => ({
85-
useFeatureFlag: jest.fn((key: string) => {
86-
if (key === 'rewardsEnableMusdHolding') {
87-
return mockIsMusdHoldingEnabled;
88-
}
89-
return false;
84+
// Mock selectMusdHoldingEnabledFlag selector
85+
jest.mock(
86+
'../../../../../../../selectors/featureFlagController/rewards/rewardsEnabled',
87+
() => ({
88+
selectMusdHoldingEnabledFlag: jest.fn(),
9089
}),
91-
FeatureFlagNames: {
92-
rewardsEnabled: 'rewardsEnabled',
93-
otaUpdatesEnabled: 'otaUpdatesEnabled',
94-
rewardsEnableMusdHolding: 'rewardsEnableMusdHolding',
95-
},
96-
}));
90+
);
9791

9892
// Mock useMetrics hook
9993
jest.mock('../../../../../../hooks/useMetrics', () => ({
@@ -283,6 +277,9 @@ describe('WaysToEarn', () => {
283277
if (selector === selectRewardsMusdDepositEnabledFlag) {
284278
return mockIsMusdDepositEnabled;
285279
}
280+
if (selector === selectMusdHoldingEnabledFlag) {
281+
return mockIsMusdHoldingEnabled;
282+
}
286283
return undefined;
287284
});
288285

app/components/UI/Rewards/components/Tabs/OverviewTab/WaysToEarn/WaysToEarn.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,8 @@ import {
3333
selectRewardsCardSpendFeatureFlags,
3434
selectRewardsMusdDepositEnabledFlag,
3535
} from '../../../../../../../selectors/featureFlagController/rewards';
36+
import { selectMusdHoldingEnabledFlag } from '../../../../../../../selectors/featureFlagController/rewards/rewardsEnabled';
3637
import { selectPredictEnabledFlag } from '../../../../../Predict/selectors/featureFlags';
37-
import {
38-
useFeatureFlag,
39-
FeatureFlagNames,
40-
} from '../../../../../../hooks/useFeatureFlag';
4138
import { PredictEventValues } from '../../../../../Predict/constants/eventNames';
4239
import {
4340
MetaMetricsEvents,
@@ -263,9 +260,7 @@ export const WaysToEarn = () => {
263260
const isCardSpendEnabled = useSelector(selectRewardsCardSpendFeatureFlags);
264261
const isPredictEnabled = useSelector(selectPredictEnabledFlag);
265262
const isMusdDepositEnabled = useSelector(selectRewardsMusdDepositEnabledFlag);
266-
const isMusdHoldingEnabled = useFeatureFlag(
267-
FeatureFlagNames.rewardsEnableMusdHolding,
268-
);
263+
const isMusdHoldingEnabled = useSelector(selectMusdHoldingEnabledFlag);
269264
const { trackEvent, createEventBuilder } = useMetrics();
270265

271266
// Use the swap/bridge navigation hook

app/components/Views/AccountSelector/AccountSelector.test.tsx

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import {
2020
internalSolanaAccount1,
2121
} from '../../../util/test/accountsControllerTestUtils';
2222

23-
jest.mock('../../hooks/useFeatureFlag', () => ({
24-
useFeatureFlag: jest.fn(() => false), // Default to BottomSheet version for tests
25-
FeatureFlagNames: {
26-
rewardsEnabled: 'rewardsEnabled',
27-
otaUpdatesEnabled: 'otaUpdatesEnabled',
28-
fullPageAccountList: 'fullPageAccountList',
29-
},
30-
}));
23+
const mockSelectFullPageAccountListEnabledFlag = jest.fn(() => false);
24+
jest.mock(
25+
'../../../selectors/featureFlagController/fullPageAccountList',
26+
() => ({
27+
selectFullPageAccountListEnabledFlag: () =>
28+
mockSelectFullPageAccountListEnabledFlag(),
29+
}),
30+
);
3131

3232
const mockAvatarAccountType = 'Maskicon' as const;
3333

@@ -670,17 +670,13 @@ describe('AccountSelector', () => {
670670
});
671671

672672
describe('Feature Flag: Full-Page Account List', () => {
673-
let mockUseFeatureFlag: jest.Mock;
674-
675673
beforeEach(() => {
676674
jest.clearAllMocks();
677-
mockUseFeatureFlag = jest.requireMock(
678-
'../../hooks/useFeatureFlag',
679-
).useFeatureFlag;
675+
mockSelectFullPageAccountListEnabledFlag.mockReturnValue(false);
680676
});
681677

682678
it('renders BottomSheet when feature flag is disabled', () => {
683-
mockUseFeatureFlag.mockReturnValue(false);
679+
mockSelectFullPageAccountListEnabledFlag.mockReturnValue(false);
684680

685681
renderScreen(
686682
AccountSelectorWrapper,
@@ -702,7 +698,7 @@ describe('AccountSelector', () => {
702698
});
703699

704700
it('renders full-page modal when feature flag is enabled', () => {
705-
mockUseFeatureFlag.mockReturnValue(true);
701+
mockSelectFullPageAccountListEnabledFlag.mockReturnValue(true);
706702

707703
renderScreen(
708704
AccountSelectorWrapper,
@@ -725,7 +721,7 @@ describe('AccountSelector', () => {
725721

726722
it('renders add button in both modes', () => {
727723
// Arrange: BottomSheet mode
728-
mockUseFeatureFlag.mockReturnValue(false);
724+
mockSelectFullPageAccountListEnabledFlag.mockReturnValue(false);
729725

730726
// Act: Render in BottomSheet mode
731727
const { unmount } = renderScreen(
@@ -750,7 +746,7 @@ describe('AccountSelector', () => {
750746

751747
// Arrange: Full-page mode
752748
jest.useRealTimers();
753-
mockUseFeatureFlag.mockReturnValue(true);
749+
mockSelectFullPageAccountListEnabledFlag.mockReturnValue(true);
754750

755751
// Act: Render in full-page mode
756752
renderScreen(
@@ -777,7 +773,7 @@ describe('AccountSelector', () => {
777773
it('switches between multichain screens in full-page mode', () => {
778774
// Arrange
779775
jest.useRealTimers();
780-
mockUseFeatureFlag.mockReturnValue(true);
776+
mockSelectFullPageAccountListEnabledFlag.mockReturnValue(true);
781777
mockSelectMultichainAccountsState2Enabled.mockReturnValue(true);
782778

783779
renderScreen(
@@ -806,7 +802,7 @@ describe('AccountSelector', () => {
806802

807803
it('closes BottomSheet when account is selected with feature flag disabled', async () => {
808804
// Arrange
809-
mockUseFeatureFlag.mockReturnValue(false);
805+
mockSelectFullPageAccountListEnabledFlag.mockReturnValue(false);
810806

811807
const { getAllByTestId } = renderScreen(
812808
AccountSelectorWrapper,
@@ -840,7 +836,7 @@ describe('AccountSelector', () => {
840836

841837
it('renders SheetHeader with title in full-page mode', () => {
842838
// Arrange
843-
mockUseFeatureFlag.mockReturnValue(true);
839+
mockSelectFullPageAccountListEnabledFlag.mockReturnValue(true);
844840

845841
renderScreen(
846842
AccountSelectorWrapper,
@@ -864,7 +860,7 @@ describe('AccountSelector', () => {
864860
it('closes full-page modal when account is selected with feature flag enabled', async () => {
865861
// Arrange
866862
jest.useRealTimers();
867-
mockUseFeatureFlag.mockReturnValue(true);
863+
mockSelectFullPageAccountListEnabledFlag.mockReturnValue(true);
868864

869865
// Mock the useNavigation hook to prevent navigation warnings
870866
const mockGoBack = jest.fn();

app/components/Views/AccountSelector/AccountSelector.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import BottomSheet, {
3636
import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader';
3737
import SheetHeader from '../../../component-library/components/Sheet/SheetHeader';
3838
import Engine from '../../../core/Engine';
39-
import { useFeatureFlag, FeatureFlagNames } from '../../hooks/useFeatureFlag';
39+
import { selectFullPageAccountListEnabledFlag } from '../../../selectors/featureFlagController/fullPageAccountList';
4040
import { store } from '../../../store';
4141
import { MetaMetricsEvents } from '../../../core/Analytics';
4242
import { strings } from '../../../../locales/i18n';
@@ -95,8 +95,8 @@ const AccountSelector = ({ route }: AccountSelectorProps) => {
9595
const routeParams = useMemo(() => route?.params, [route?.params]);
9696

9797
// Feature flag for full-page account list
98-
const isFullPageAccountList = useFeatureFlag(
99-
FeatureFlagNames.fullPageAccountList,
98+
const isFullPageAccountList = useSelector(
99+
selectFullPageAccountListEnabledFlag,
100100
);
101101
const sheetRef = useRef<BottomSheetRef>(null);
102102

app/components/Views/FeatureFlagOverride/FeatureFlagOverride.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
FeatureFlagInfo,
1111
isMinimumRequiredVersionSupported,
1212
} from '../../../util/feature-flags';
13-
import { FeatureFlagNames } from '../../hooks/useFeatureFlag';
13+
import { FeatureFlagNames } from '../../../constants/featureFlags';
1414

1515
// Mock all dependencies
1616
jest.mock('@react-navigation/native', () => ({

app/components/Views/FeatureFlagOverride/FeatureFlagOverride.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from '../../../util/feature-flags';
2424
import { useFeatureFlagOverride } from '../../../contexts/FeatureFlagOverrideContext';
2525
import { useFeatureFlagStats } from '../../../hooks/useFeatureFlagStats';
26-
import { FeatureFlagNames } from '../../hooks/useFeatureFlag';
26+
import { FeatureFlagNames } from '../../../constants/featureFlags';
2727

2828
interface FeatureFlagRowProps {
2929
flag: FeatureFlagInfo;

app/components/hooks/useFeatureFlag.test.ts

Lines changed: 0 additions & 158 deletions
This file was deleted.

app/components/hooks/useFeatureFlag.ts

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)