Skip to content

Commit 18f8b61

Browse files
feat: added BFT consolidation feature for new users- #43935 (MetaMask#35510)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> <!-- mms-check directive vocabulary — read by .github/scripts/shared/pr-template-checks.ts at module load to build the validation plan. Directives are invisible in rendered markdown and must NOT be removed or edited without updating the validator registry. type=text Section must contain non-placeholder prose. type=changelog Section must have a valid CHANGELOG entry: line. type=issue-link Section must have a Fixes:/Closes:/Refs: line with a value. type=manual-testing Section must have real testing steps or an explicit N/A. type=screenshot Section must have evidence (image/URL) or an explicit N/A. type=checklist Section must have all checkboxes consciously checked. required=true|false Whether a missing/invalid section runs the validator at all. blocking=true|false Whether a failure of this check fails the CI workflow. Default: false — failures are shown as warnings in the sticky comment but do not block the PR. Sections without a directive are checked for structural presence only. --> ## **Description** <!-- mms-check: type=text required=true --> This PR adds Phase 1 of the consolidated Basic Functionality Toggle experience for new users. When enabled by ld flag, newly onboarded users are marked as part of the consolidated Basic Functionality cohort. For those users, the Basic Functionality toggle controls the related external-service-backed settings together, while granular settings are hidden from Settings ## **Changelog** <!-- mms-check: type=changelog required=true blocking=true --> <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: Consolidated Privacy toggles in BFT for new users ## **Related issues** <!-- mms-check: type=issue-link required=true --> Fixes: [issue](https://consensyssoftware.atlassian.net/browse/CEUX-1328) ## **Manual testing steps** <!-- mms-check: type=manual-testing required=true --> ```gherkin Complete onboarding on a new wallet (sets isBasicFunctionalityConsolidatedEnabled). Settings → Security & Privacy — only Basic Functionality + IPFS (+ analytics sections); no granular toggles. Settings → Advanced — no “Autodetect tokens” row. Turn Basic Functionality OFF — all 8 synced prefs should disable together. turn LD flag OFF → granular toggles reappear even for cohort users. ``` ## **Screenshots/Recordings** <!-- mms-check: type=screenshot required=true --> <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- mms-check: type=checklist required=true --> <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [ ] 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 - 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** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Bulk-updates multiple privacy and security preferences and changes which settings are visible, gated by rollout flag and onboarding cohort—but existing users are not enrolled on backup flows. > > **Overview** > Phase 1 rolls out **consolidated Basic Functionality (BFT)** for eligible new wallets when the version-gated remote flag `mobileUxBftcConsolidation` is on. First-time onboarding completion (not backup flows) can persist **`isBasicFunctionalityConsolidatedEnabled`** and run **`syncConsolidatedBasicFunctionalityPreferences`** so eight `PreferencesController` privacy/network prefs move together with the user’s Basic Functionality choice. > > **`toggleBasicFunctionality`** now reads that cohort via **`selectIsBasicFunctionalityConsolidationEnabled`** (remote flag **and** persisted marker) and calls the same sync helper when consolidation applies, in addition to existing Redux and `MultichainAccountService` behavior. > > **Security** and **Advanced** settings hide granular rows (e.g. Blockaid, network details, batch balances, transaction simulations, NFT/token autodetect) when consolidation is active; **Autodetect tokens** is hidden on Advanced. Kill-switch: turning the remote flag off restores granular UI even for cohort users. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 2e60fa5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9effc88 commit 18f8b61

14 files changed

Lines changed: 444 additions & 21 deletions

File tree

app/actions/settings/index.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,31 @@ export function setBasicFunctionality(basicFunctionalityEnabled) {
5555
};
5656
}
5757

58+
export function setBasicFunctionalityConsolidatedEnabled(
59+
isBasicFunctionalityConsolidatedEnabled,
60+
) {
61+
return {
62+
type: 'SET_BASIC_FUNCTIONALITY_CONSOLIDATED_ENABLED',
63+
isBasicFunctionalityConsolidatedEnabled,
64+
};
65+
}
66+
5867
// Thunk action creator for user-initiated toggles (includes MultichainAccountService integration)
5968
export function toggleBasicFunctionality(basicFunctionalityEnabled) {
60-
return async (dispatch) => {
61-
// First dispatch the Redux state update
69+
return async (dispatch, getState) => {
6270
dispatch(setBasicFunctionality(basicFunctionalityEnabled));
6371

72+
const {
73+
selectIsBasicFunctionalityConsolidationEnabled,
74+
} = require('../../selectors/featureFlagController/basicFunctionalityConsolidation');
75+
const {
76+
syncConsolidatedBasicFunctionalityPreferences,
77+
} = require('../../util/basicFunctionality/syncConsolidatedBasicFunctionalityPreferences');
78+
79+
if (selectIsBasicFunctionalityConsolidationEnabled(getState())) {
80+
syncConsolidatedBasicFunctionalityPreferences(basicFunctionalityEnabled);
81+
}
82+
6483
const Engine = require('../../core/Engine').default;
6584
Engine.context.MultichainAccountService.setBasicFunctionality(
6685
basicFunctionalityEnabled,

app/actions/settings/toggleBasicFunctionality.test.js

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
import { toggleBasicFunctionality, setBasicFunctionality } from './index';
2+
import { selectIsBasicFunctionalityConsolidationEnabled } from '../../selectors/featureFlagController/basicFunctionalityConsolidation';
3+
import { syncConsolidatedBasicFunctionalityPreferences } from '../../util/basicFunctionality/syncConsolidatedBasicFunctionalityPreferences';
4+
5+
const mockSyncConsolidatedBasicFunctionalityPreferences = jest.mocked(
6+
syncConsolidatedBasicFunctionalityPreferences,
7+
);
8+
const mockSelectIsBasicFunctionalityConsolidationEnabled = jest.mocked(
9+
selectIsBasicFunctionalityConsolidationEnabled,
10+
);
211

312
// Mock Engine
413
const mockSetBasicFunctionality = jest.fn().mockResolvedValue(undefined);
@@ -8,21 +17,39 @@ jest.mock('../../core/Engine', () => ({
817
MultichainAccountService: {
918
setBasicFunctionality: mockSetBasicFunctionality,
1019
},
20+
PreferencesController: {},
1121
},
1222
},
1323
}));
1424

25+
jest.mock(
26+
'../../selectors/featureFlagController/basicFunctionalityConsolidation',
27+
() => ({
28+
selectIsBasicFunctionalityConsolidationEnabled: jest.fn(() => false),
29+
}),
30+
);
31+
32+
jest.mock(
33+
'../../util/basicFunctionality/syncConsolidatedBasicFunctionalityPreferences',
34+
() => ({
35+
syncConsolidatedBasicFunctionalityPreferences: jest.fn(),
36+
}),
37+
);
38+
1539
describe('toggleBasicFunctionality action', () => {
1640
let mockDispatch;
41+
let mockGetState;
1742

1843
beforeEach(() => {
1944
mockDispatch = jest.fn();
45+
mockGetState = jest.fn(() => ({}));
2046
jest.clearAllMocks();
47+
mockSelectIsBasicFunctionalityConsolidationEnabled.mockReturnValue(false);
2148
});
2249

2350
it('dispatches Redux state update and calls MultichainAccountService', async () => {
2451
const action = toggleBasicFunctionality(true);
25-
await action(mockDispatch);
52+
await action(mockDispatch, mockGetState);
2653

2754
// Verify Redux state is updated
2855
expect(mockDispatch).toHaveBeenCalledWith(setBasicFunctionality(true));
@@ -33,7 +60,7 @@ describe('toggleBasicFunctionality action', () => {
3360

3461
it('dispatches Redux state update with false value', async () => {
3562
const action = toggleBasicFunctionality(false);
36-
await action(mockDispatch);
63+
await action(mockDispatch, mockGetState);
3764

3865
// Verify Redux state is updated
3966
expect(mockDispatch).toHaveBeenCalledWith(setBasicFunctionality(false));
@@ -51,7 +78,7 @@ describe('toggleBasicFunctionality action', () => {
5178
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
5279

5380
const action = toggleBasicFunctionality(false);
54-
await action(mockDispatch);
81+
await action(mockDispatch, mockGetState);
5582

5683
// Verify Redux state is still updated despite service error
5784
expect(mockDispatch).toHaveBeenCalledWith(setBasicFunctionality(false));
@@ -70,4 +97,23 @@ describe('toggleBasicFunctionality action', () => {
7097

7198
consoleSpy.mockRestore();
7299
});
100+
101+
it('syncs consolidated preferences when consolidation is enabled', async () => {
102+
mockSelectIsBasicFunctionalityConsolidationEnabled.mockReturnValue(true);
103+
const action = toggleBasicFunctionality(false);
104+
await action(mockDispatch, mockGetState);
105+
106+
expect(
107+
mockSyncConsolidatedBasicFunctionalityPreferences,
108+
).toHaveBeenCalledWith(false);
109+
});
110+
111+
it('does not sync consolidated preferences when consolidation is disabled', async () => {
112+
const action = toggleBasicFunctionality(true);
113+
await action(mockDispatch, mockGetState);
114+
115+
expect(
116+
mockSyncConsolidatedBasicFunctionalityPreferences,
117+
).not.toHaveBeenCalled();
118+
});
73119
});

app/components/Views/Settings/AdvancedSettings/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import type { RootState } from '../../../../reducers';
4343
import type { AppNavigationProp } from '../../../../core/NavigationService/types';
4444
import { SettingsToggleRow } from '../components/SettingsToggleRow';
4545
import { createStyles } from './AdvancedSettings.styles';
46+
import { selectIsBasicFunctionalityConsolidationEnabled } from '../../../../selectors/featureFlagController/basicFunctionalityConsolidation';
4647

4748
interface SettingsState {
4849
showHexData: boolean;
@@ -63,6 +64,7 @@ interface StateProps {
6364
dismissSmartAccountSuggestionEnabled: ReturnType<
6465
typeof selectDismissSmartAccountSuggestionEnabled
6566
>;
67+
isBasicFunctionalityConsolidationEnabled: boolean;
6668
}
6769

6870
interface DispatchProps {
@@ -94,6 +96,7 @@ const AdvancedSettings = ({
9496
route,
9597
smartTransactionsOptInStatus,
9698
dismissSmartAccountSuggestionEnabled,
99+
isBasicFunctionalityConsolidationEnabled,
97100
}: Props) => {
98101
const { colors } = useTheme();
99102
const styles = createStyles(colors);
@@ -242,7 +245,9 @@ const AdvancedSettings = ({
242245
onValueChange={setShowHexData}
243246
/>
244247

245-
<AutoDetectTokensSettings />
248+
{!isBasicFunctionalityConsolidationEnabled && (
249+
<AutoDetectTokensSettings />
250+
)}
246251

247252
<SettingsToggleRow
248253
title={strings('app_settings.show_fiat_on_testnets')}
@@ -295,6 +300,8 @@ const mapStateToProps = (state: SettingsRootState): StateProps => ({
295300
smartTransactionsOptInStatus: selectSmartTransactionsOptInStatus(state),
296301
dismissSmartAccountSuggestionEnabled:
297302
selectDismissSmartAccountSuggestionEnabled(state),
303+
isBasicFunctionalityConsolidationEnabled:
304+
selectIsBasicFunctionalityConsolidationEnabled(state),
298305
});
299306

300307
const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({

app/components/Views/Settings/SecuritySettings/SecuritySettings.tsx

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import BatchAccountBalanceSettings from '../../Settings/BatchAccountBalanceSetti
7272
import useCheckNftAutoDetectionModal from '../../../hooks/useCheckNftAutoDetectionModal';
7373
import useCheckMultiRpcModal from '../../../hooks/useCheckMultiRpcModal';
7474
import { useStyles } from '../../../../component-library/hooks/useStyles';
75+
import { selectIsBasicFunctionalityConsolidationEnabled } from '../../../../selectors/featureFlagController/basicFunctionalityConsolidation';
7576

7677
const Settings: React.FC = () => {
7778
const { trackEvent, isEnabled, createEventBuilder } = useAnalytics();
@@ -90,6 +91,9 @@ const Settings: React.FC = () => {
9091
const isBasicFunctionalityEnabled = useSelector(
9192
(state: RootState) => state?.settings?.basicFunctionalityEnabled,
9293
);
94+
const isBasicFunctionalityConsolidationEnabled = useSelector(
95+
selectIsBasicFunctionalityConsolidationEnabled,
96+
);
9397
const scrollViewRef = useRef<ScrollView>(null);
9498
const metaMetricsSectionRef = useRef<View>(null);
9599
const dataCollectionSectionRef = useRef<View>(null);
@@ -379,7 +383,7 @@ const Settings: React.FC = () => {
379383
<ChangePassword />
380384
<AutoLock />
381385
<DeviceSecurityToggle />
382-
<BlockaidSettings />
386+
{!isBasicFunctionalityConsolidationEnabled && <BlockaidSettings />}
383387
<Text variant={TextVariant.HeadingMd} style={styles.subHeading}>
384388
{strings('app_settings.privacy_heading')}
385389
</Text>
@@ -392,21 +396,33 @@ const Settings: React.FC = () => {
392396
<ClearPrivacy />
393397
{renderClearBrowserHistorySection()}
394398
<ClearCookiesSection />
395-
<Text variant={TextVariant.HeadingMd} style={styles.subHeading}>
396-
{strings('app_settings.network_provider')}
397-
</Text>
398-
<NetworkDetailsCheckSettings />
399-
<Text variant={TextVariant.HeadingMd} style={styles.subHeading}>
400-
{strings('app_settings.transactions_subheading')}
401-
</Text>
402-
<BatchAccountBalanceSettings />
399+
{!isBasicFunctionalityConsolidationEnabled && (
400+
<>
401+
<Text variant={TextVariant.HeadingMd} style={styles.subHeading}>
402+
{strings('app_settings.network_provider')}
403+
</Text>
404+
<NetworkDetailsCheckSettings />
405+
</>
406+
)}
407+
{!isBasicFunctionalityConsolidationEnabled && (
408+
<>
409+
<Text variant={TextVariant.HeadingMd} style={styles.subHeading}>
410+
{strings('app_settings.transactions_subheading')}
411+
</Text>
412+
<BatchAccountBalanceSettings />
413+
{renderUseTransactionSimulations()}
414+
</>
415+
)}
403416
{renderHistoryModal()}
404-
{renderUseTransactionSimulations()}
405-
<Text variant={TextVariant.HeadingMd} style={styles.subHeading}>
406-
{strings('app_settings.token_nft_ens_subheading')}
407-
</Text>
408-
<DisplayNFTMediaSettings />
409-
{isMainnet && <AutoDetectNFTSettings />}
417+
{!isBasicFunctionalityConsolidationEnabled && (
418+
<>
419+
<Text variant={TextVariant.HeadingMd} style={styles.subHeading}>
420+
{strings('app_settings.token_nft_ens_subheading')}
421+
</Text>
422+
<DisplayNFTMediaSettings />
423+
{isMainnet && <AutoDetectNFTSettings />}
424+
</>
425+
)}
410426
<IPFSGatewaySettings />
411427
<Text variant={TextVariant.HeadingMd} style={styles.subHeading}>
412428
{strings('app_settings.analytics_subheading')}

app/reducers/settings/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const initialState = {
88
avatarAccountType: AvatarAccountType.Maskicon,
99
hideZeroBalanceTokens: true,
1010
basicFunctionalityEnabled: true,
11+
isBasicFunctionalityConsolidatedEnabled: false,
1112
deepLinkModalDisabled: false,
1213
hapticsEnabled: true,
1314
// Whether this account is shown on the Top Traders leaderboard. Local mirror
@@ -61,6 +62,12 @@ const settingsReducer = (state = initialState, action) => {
6162
...state,
6263
basicFunctionalityEnabled: action.basicFunctionalityEnabled,
6364
};
65+
case 'SET_BASIC_FUNCTIONALITY_CONSOLIDATED_ENABLED':
66+
return {
67+
...state,
68+
isBasicFunctionalityConsolidatedEnabled:
69+
action.isBasicFunctionalityConsolidatedEnabled,
70+
};
6471
case 'TOGGLE_DEVICE_NOTIFICATIONS':
6572
return {
6673
...state,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {
2+
MOBILE_UX_BFTC_CONSOLIDATION_FLAG_NAME,
3+
selectIsBasicFunctionalityConsolidationEnabled,
4+
selectMobileUxBftcConsolidationFlagEnabled,
5+
} from './index';
6+
// eslint-disable-next-line import-x/no-namespace
7+
import * as remoteFeatureFlagModule from '../../../util/remoteFeatureFlag';
8+
9+
jest.mock('react-native-device-info', () => ({
10+
getVersion: jest.fn(() => '7.60.0'),
11+
}));
12+
13+
describe('basicFunctionalityConsolidation selectors', () => {
14+
let mockHasMinimumRequiredVersion: jest.SpyInstance;
15+
16+
beforeEach(() => {
17+
jest.clearAllMocks();
18+
mockHasMinimumRequiredVersion = jest.spyOn(
19+
remoteFeatureFlagModule,
20+
'hasMinimumRequiredVersion',
21+
);
22+
mockHasMinimumRequiredVersion.mockReturnValue(true);
23+
});
24+
25+
afterEach(() => {
26+
mockHasMinimumRequiredVersion?.mockRestore();
27+
});
28+
29+
describe('selectMobileUxBftcConsolidationFlagEnabled', () => {
30+
it('returns true when remote flag is valid and enabled', () => {
31+
const result = selectMobileUxBftcConsolidationFlagEnabled.resultFunc({
32+
[MOBILE_UX_BFTC_CONSOLIDATION_FLAG_NAME]: {
33+
enabled: true,
34+
minimumVersion: '1.0.0',
35+
},
36+
});
37+
38+
expect(result).toBe(true);
39+
});
40+
41+
it('returns false when remote flag is disabled', () => {
42+
const result = selectMobileUxBftcConsolidationFlagEnabled.resultFunc({
43+
[MOBILE_UX_BFTC_CONSOLIDATION_FLAG_NAME]: {
44+
enabled: false,
45+
minimumVersion: '1.0.0',
46+
},
47+
});
48+
49+
expect(result).toBe(false);
50+
});
51+
52+
it('returns false when remote flag is missing', () => {
53+
const result = selectMobileUxBftcConsolidationFlagEnabled.resultFunc({});
54+
55+
expect(result).toBe(false);
56+
});
57+
});
58+
59+
describe('selectIsBasicFunctionalityConsolidationEnabled', () => {
60+
it('returns true when remote flag and cohort marker are enabled', () => {
61+
const result = selectIsBasicFunctionalityConsolidationEnabled.resultFunc(
62+
true,
63+
true,
64+
);
65+
66+
expect(result).toBe(true);
67+
});
68+
69+
it('returns false when remote flag is disabled (kill-switch)', () => {
70+
const result = selectIsBasicFunctionalityConsolidationEnabled.resultFunc(
71+
false,
72+
true,
73+
);
74+
75+
expect(result).toBe(false);
76+
});
77+
78+
it('returns false when cohort marker is missing for existing users', () => {
79+
const result = selectIsBasicFunctionalityConsolidationEnabled.resultFunc(
80+
true,
81+
false,
82+
);
83+
84+
expect(result).toBe(false);
85+
});
86+
});
87+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { createSelector } from 'reselect';
2+
import { selectRemoteFeatureFlags } from '..';
3+
import {
4+
validatedVersionGatedFeatureFlag,
5+
type VersionGatedFeatureFlag,
6+
} from '../../../util/remoteFeatureFlag';
7+
import { selectIsBasicFunctionalityConsolidatedEnabled } from '../../settings';
8+
9+
export const MOBILE_UX_BFTC_CONSOLIDATION_FLAG_NAME =
10+
'mobileUxBftcConsolidation';
11+
12+
/**
13+
* Remote rollout flag for consolidated Basic Functionality (version-gated).
14+
* Default OFF in production; acts as kill-switch when disabled.
15+
*/
16+
export const selectMobileUxBftcConsolidationFlagEnabled = createSelector(
17+
selectRemoteFeatureFlags,
18+
(remoteFeatureFlags) => {
19+
const remoteFlag = remoteFeatureFlags?.[
20+
MOBILE_UX_BFTC_CONSOLIDATION_FLAG_NAME
21+
] as unknown as VersionGatedFeatureFlag;
22+
23+
return validatedVersionGatedFeatureFlag(remoteFlag) ?? false;
24+
},
25+
);
26+
27+
/**
28+
* True when the user should see consolidated Basic Functionality settings.
29+
* Requires both the remote flag and the persisted onboarding cohort marker.
30+
*/
31+
export const selectIsBasicFunctionalityConsolidationEnabled = createSelector(
32+
selectMobileUxBftcConsolidationFlagEnabled,
33+
selectIsBasicFunctionalityConsolidatedEnabled,
34+
(isRemoteFlagEnabled, isConsolidatedUser) =>
35+
isRemoteFlagEnabled && isConsolidatedUser,
36+
);

0 commit comments

Comments
 (0)