Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement remote feature flag controller #12427

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
00a3bd1
setup metro bundler symlink resolution
joaoloureirop Nov 22, 2024
b4f9cc8
yarn setup fixup
joaoloureirop Nov 22, 2024
cdd4a58
metro bundler: bypass lint errors
joaoloureirop Nov 22, 2024
1787680
init RemoteFeatureFlagController
joaoloureirop Nov 22, 2024
5299891
controller init state destruct
joaoloureirop Nov 26, 2024
6204914
add feature flag selectors
joaoloureirop Nov 26, 2024
6f8d208
ff(mobileMinimumVersion): set fallback values
joaoloureirop Nov 26, 2024
608f05b
remove feature flag saga / slice
joaoloureirop Nov 26, 2024
a8ec092
remote feature flags behing basic func toggle
joaoloureirop Nov 26, 2024
01df041
fix controller state change event
joaoloureirop Nov 26, 2024
c8c2af0
get env vars to init feature flag controllers
joaoloureirop Nov 26, 2024
ca33006
update min version fallback values
joaoloureirop Nov 26, 2024
27b32a3
remove controller messenger allowances
joaoloureirop Nov 26, 2024
d7aed0d
Revert "metro bundler: bypass lint errors"
joaoloureirop Nov 26, 2024
b80145d
remove unused import
joaoloureirop Nov 26, 2024
9a67929
fix lint errors
joaoloureirop Nov 26, 2024
934a13a
remove unneded semicolon
joaoloureirop Nov 26, 2024
faa6cb0
remove featureFlag initial root state
joaoloureirop Nov 26, 2024
20c3b42
Merge branch 'main' into feat/feature-flags
joaoloureirop Nov 27, 2024
d22d1cb
Engine: split Feature flag controller
joaoloureirop Nov 27, 2024
ba1802e
set RemoteFeatureFlagController & minappversion flag codeowners
joaoloureirop Nov 27, 2024
5e97075
minimumAppVersion featureflag: improve types
joaoloureirop Nov 27, 2024
c838b21
remove dead code
joaoloureirop Nov 27, 2024
426655e
add Feature flag controller logger
joaoloureirop Nov 27, 2024
ebc940e
Merge branch 'main' into feat/feature-flags
joaoloureirop Nov 27, 2024
8014b45
fix basic functionality toggle
joaoloureirop Nov 27, 2024
9a35568
Revert "yarn setup fixup"
joaoloureirop Nov 27, 2024
8c305c1
improve minimumAppVersion TS types
joaoloureirop Nov 27, 2024
c8495e5
fix unit tests
joaoloureirop Nov 27, 2024
d7d384c
migration: remove featureFlags property
joaoloureirop Nov 27, 2024
b784e89
codeowners: featureFlagController selector base files pattern
joaoloureirop Nov 27, 2024
38292f7
update useMinimumVersions.tsx import paths
joaoloureirop Nov 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 22 additions & 17 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@
app/component-library/ @MetaMask/design-system-engineers

# Platform Team
.github/CODEOWNERS @MetaMask/mobile-platform
patches/ @MetaMask/mobile-platform
app/core/Engine/Engine.ts @MetaMask/mobile-platform
app/core/Engine/Engine.test.ts @MetaMask/mobile-platform
app/core/Engine/index.ts @MetaMask/mobile-platform
app/core/Engine/types.ts @MetaMask/mobile-platform
app/core/Analytics/ @MetaMask/mobile-platform
app/util/metrics/ @MetaMask/mobile-platform
app/components/hooks/useMetrics/ @MetaMask/mobile-platform
app/store/migrations/ @MetaMask/mobile-platform
bitrise.yml @MetaMask/mobile-platform
yarn.lock @MetaMask/mobile-platform
ios/Podfile.lock @MetaMask/mobile-platform
.github/CODEOWNERS @MetaMask/mobile-platform
patches/ @MetaMask/mobile-platform
app/core/Engine/Engine.ts @MetaMask/mobile-platform
app/core/Engine/Engine.test.ts @MetaMask/mobile-platform
app/core/Engine/index.ts @MetaMask/mobile-platform
app/core/Engine/types.ts @MetaMask/mobile-platform
app/core/Engine/controllers/RemoteFeatureFlagController/ @MetaMask/mobile-platform
app/core/Analytics/ @MetaMask/mobile-platform
app/util/metrics/ @MetaMask/mobile-platform
app/components/hooks/useMetrics/ @MetaMask/mobile-platform
app/selectors/featureFlagController/index.ts @MetaMask/mobile-platform
Copy link
Contributor

Choose a reason for hiding this comment

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

S: maybe you can make the team own the whole app/selectors/featureFlagController directory instead of individual files?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We want app/selectors/featureFlagController/<featureFlagName> to be owned by other teams

I'll update the CODEOWNERS syntax to match files like featureFlagController/index.ts but no further nested files

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed on b784e89

app/selectors/featureFlagController/index.test.ts @MetaMask/mobile-platform
app/selectors/featureFlagController/mocks.ts @MetaMask/mobile-platform
app/selectors/featureFlagController/minimumAppVersion/ @MetaMask/mobile-platform
app/store/migrations/ @MetaMask/mobile-platform
bitrise.yml @MetaMask/mobile-platform
yarn.lock @MetaMask/mobile-platform
ios/Podfile.lock @MetaMask/mobile-platform

# Ramps Team
app/components/UI/Ramp/ @MetaMask/ramp
Expand Down Expand Up @@ -53,10 +58,10 @@ app/components/UI/Swaps @MetaMask/swaps-engineers
app/components/Views/Notifications @MetaMask/notifications
app/components/Views/Settings/NotificationsSettings @MetaMask/notifications
app/components/UI/Notifications @MetaMask/notifications
app/reducers/notification @MetaMask/notifications
app/actions/notification @MetaMask/notifications
app/selectors/notification @MetaMask/notifications
app/util/notifications @MetaMask/notifications
app/reducers/notification @MetaMask/notifications
app/actions/notification @MetaMask/notifications
app/selectors/notification @MetaMask/notifications
app/util/notifications @MetaMask/notifications
app/store/util/notifications @MetaMask/notifications

# LavaMoat Team
Expand Down
10 changes: 4 additions & 6 deletions app/components/hooks/MinimumVersions/useMinimumVersions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@ import { createUpdateNeededNavDetails } from '../../UI/UpdateNeeded/UpdateNeeded
import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
import { InteractionManager } from 'react-native';
import { FeatureFlagsState } from '../../../core/redux/slices/featureFlags';
import { SecurityState } from '../../../../app/reducers/security';
import { RootState } from '../../../../app/reducers';
import { selectAppMinimumBuild } from '../../../../app/selectors/featureFlagController/minimumAppVersion';
joaoloureirop marked this conversation as resolved.
Show resolved Hide resolved

const useMinimumVersions = () => {
const { automaticSecurityChecksEnabled }: SecurityState = useSelector(
(state: RootState) => state.security,
);
const { featureFlags }: FeatureFlagsState = useSelector(
(state: RootState) => state.featureFlags,
);

const appMinimumBuild = useSelector((state: RootState) => selectAppMinimumBuild(state));
Copy link
Contributor

Choose a reason for hiding this comment

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

Q: not sure if this change makes the mock in the test obsolete and how this test doesn't need any changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, the tests for this hook need to be updated

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm surprised the test is passing given the changes in the hook... so maybe rewrite the test in a more robust way. A change like this should make it fail otherwise it probably means it's not testing anything but the mocks...

const currentBuildNumber = Number(getBuildNumber());
const navigation = useNavigation();
const shouldTriggerUpdateFlow =
automaticSecurityChecksEnabled &&
featureFlags?.mobileMinimumVersions?.appMinimumBuild > currentBuildNumber;
automaticSecurityChecksEnabled && appMinimumBuild > currentBuildNumber;

useEffect(() => {
if (shouldTriggerUpdateFlow) {
Expand Down
40 changes: 27 additions & 13 deletions app/core/Engine/Engine.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-shadow */
import Crypto from 'react-native-quick-crypto';
import { scrypt } from 'react-native-fast-crypto';

import {
AccountTrackerController,
AssetsContractController,
Expand Down Expand Up @@ -79,6 +80,7 @@
LedgerMobileBridge,
LedgerTransportMiddleware,
} from '@metamask/eth-ledger-bridge-keyring';
import RemoteFeatureFlagController from './controllers/RemoteFeatureFlagController';
import { Encryptor, LEGACY_DERIVATION_OPTIONS } from '../Encryptor';
import {
isMainnetByChainId,
Expand Down Expand Up @@ -266,6 +268,10 @@
) {
this.controllerMessenger = new ExtendedControllerMessenger();

// Basic Functionality toggle defaults to true
const getBasicFunctionalityToggleState = () =>
Copy link
Contributor

Choose a reason for hiding this comment

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

S: should we use a selector here?

store.getState().settings?.basicFunctionalityEnabled ?? true;

const approvalController = new ApprovalController({
messenger: this.controllerMessenger.getRestricted({
name: 'ApprovalController',
Expand Down Expand Up @@ -478,6 +484,13 @@
'https://gas.api.cx.metamask.io/networks/<chain_id>/suggestedGasFees',
});

const remoteFeatureFlagController = RemoteFeatureFlagController.init({
initialState,
controllerMessenger: this.controllerMessenger,
fetchFunction,
disabled: getBasicFunctionalityToggleState() === false
Copy link
Contributor

Choose a reason for hiding this comment

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

Q: why testing the exact boolean false equality here?
If the value is true, it's true.
If the value is false, it's true.
If the value eis anything like undefined or null, it will be true because of the getBasicFunctionalityToggleState default.
So if we have a false here, it's necessarily a real false as a value in the state. So the following would be enough:

Suggested change
disabled: getBasicFunctionalityToggleState() === false
disabled: !getBasicFunctionalityToggleState()

and same on line 949

});

const phishingController = new PhishingController({
messenger: this.controllerMessenger.getRestricted({
name: 'PhishingController',
Expand Down Expand Up @@ -933,8 +946,7 @@
encryptor,
getMnemonic: getPrimaryKeyringMnemonic.bind(this),
getFeatureFlags: () => ({
disableSnaps:
store.getState().settings.basicFunctionalityEnabled === false,
disableSnaps: getBasicFunctionalityToggleState() === false,
}),
});

Expand Down Expand Up @@ -1135,7 +1147,7 @@

return Boolean(
hasProperty(showIncomingTransactions, currentChainId) &&
showIncomingTransactions?.[currentHexChainId],
showIncomingTransactions?.[currentHexChainId],
);
},
updateTransactions: true,
Expand Down Expand Up @@ -1387,6 +1399,7 @@
GasFeeController: gasFeeController,
ApprovalController: approvalController,
PermissionController: permissionController,
RemoteFeatureFlagController: remoteFeatureFlagController,
SelectedNetworkController: selectedNetworkController,
SignatureController: new SignatureController({
messenger: this.controllerMessenger.getRestricted({
Expand Down Expand Up @@ -1466,9 +1479,9 @@

this.datamodel = new ComposableController(
// @ts-expect-error TODO: Filter out non-controller instances
controllers,

Check failure on line 1482 in app/core/Engine/Engine.ts

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Unused '@ts-expect-error' directive.
this.controllerMessenger,
);

Check failure on line 1484 in app/core/Engine/Engine.ts

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Argument of type 'ControllerMessenger' is not assignable to parameter of type 'RestrictedControllerMessenger<"ComposableController", never, any, never, any>'.

const { NftController: nfts } = this.context;

Expand All @@ -1488,7 +1501,7 @@
(state: NetworkState) => {
if (
state.networksMetadata[state.selectedNetworkClientId].status ===
NetworkStatus.Available &&
NetworkStatus.Available &&
networkController.getNetworkClientById(
networkController?.state.selectedNetworkClientId,
).configuration.chainId !== currentChainId
Expand All @@ -1513,10 +1526,9 @@
} catch (error) {
console.error(
error,
`Network ID not changed, current chainId: ${
networkController.getNetworkClientById(
networkController?.state.selectedNetworkClientId,
).configuration.chainId
`Network ID not changed, current chainId: ${networkController.getNetworkClientById(
networkController?.state.selectedNetworkClientId,
).configuration.chainId
}`,
);
}
Expand Down Expand Up @@ -1718,7 +1730,7 @@
const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined;
if (
accountsByChainId?.[toHexadecimal(chainId)]?.[
selectSelectedInternalAccountChecksummedAddress
selectSelectedInternalAccountChecksummedAddress
]
) {
const balanceBN = hexToBN(
Expand Down Expand Up @@ -1755,7 +1767,7 @@

const tokenBalances =
allTokenBalances?.[selectedInternalAccount.address as Hex]?.[
chainId
chainId
] ?? {};
tokens.forEach(
(item: { address: string; balance?: string; decimals: number }) => {
Expand All @@ -1766,9 +1778,9 @@
item.balance ||
(item.address in tokenBalances
? renderFromTokenMinimalUnit(
tokenBalances[item.address as Hex],
item.decimals,
)
tokenBalances[item.address as Hex],
item.decimals,
)
: undefined);
const tokenBalanceFiat = balanceToFiatNumber(
// TODO: Fix this by handling or eliminating the undefined case
Expand Down Expand Up @@ -2061,6 +2073,7 @@
NetworkController,
PreferencesController,
PhishingController,
RemoteFeatureFlagController,
PPOMController,
TokenBalancesController,
TokenRatesController,
Expand Down Expand Up @@ -2106,6 +2119,7 @@
KeyringController,
NetworkController,
PhishingController,
RemoteFeatureFlagController,
PPOMController,
PreferencesController,
TokenBalancesController,
Expand Down
Copy link
Contributor

Choose a reason for hiding this comment

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

To keep things consistent, let's follow the pattern that resembles that of what is currently in Engine/controllers/accounts The directory name RemoteFeatureFlagController is fine. We can keep the index file that you have and use it to re-export utils and anything else in the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

In this case, it's debatable whether or not we should just put all of the functions in index or utils file. IMO, creating the utils file and re-exporting via index allows us to be more flexible in the future, where index can export other things such as constants, etc.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
RemoteFeatureFlagController,
ClientConfigApiService,
ClientType,
} from '@metamask/remote-feature-flag-controller';

Check failure on line 5 in app/core/Engine/controllers/RemoteFeatureFlagController/index.ts

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Cannot find module '@metamask/remote-feature-flag-controller' or its corresponding type declarations.
import Logger from '../../../../util/Logger';
import { RemoteFeatureFlagInitParamTypes } from './types';
import {
getFeatureFlagAppEnvironment,
getFeatureFlagAppDistribution
} from './utils';


const init = ({
Copy link
Contributor Author

@joaoloureirop joaoloureirop Nov 27, 2024

Choose a reason for hiding this comment

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

add remote feature flag controller init unit tests

Copy link
Contributor

Choose a reason for hiding this comment

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

is this a TODO?

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks. Yes unit tests please!

Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to createRemoteFeatureFlagsController

Suggested change
const init = ({
const createRemoteFeatureFlagsController = ({

Copy link
Contributor

@Cal-L Cal-L Nov 27, 2024

Choose a reason for hiding this comment

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

Move into utils file

initialState,
controllerMessenger,
fetchFunction,
disabled,
}: RemoteFeatureFlagInitParamTypes) => {

const remoteFeatureFlagController = new RemoteFeatureFlagController({
messenger: controllerMessenger.getRestricted({
Copy link
Contributor

Choose a reason for hiding this comment

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

Move messenger creation back into Engine file since we as a platform team (at least) need to be aware of messenger related changes. You can reference how the accounts controller is set up.

Copy link
Contributor

Choose a reason for hiding this comment

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

Discussed with Mark a few days ago - Here's the PR that updated the accounts controller side - #12416

name: 'RemoteFeatureFlagController',
allowedActions: [],
allowedEvents: [],
}),
state: {
...initialState.RemoteFeatureFlagController
Copy link
Contributor

Choose a reason for hiding this comment

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

No need to spread in an object. Just assign the value. Also pass initialState.RemoteFeatureFlagController into initialState instead of deconstructing from here

},
disabled,
clientConfigApiService: new ClientConfigApiService({
fetch: fetchFunction,
config: {
client: ClientType.Mobile,
environment: getFeatureFlagAppEnvironment(),
distribution: getFeatureFlagAppDistribution(),
},
}),
});

if (disabled) {
Logger.log('Feature Flags Controller disabled');
} else {
remoteFeatureFlagController.getRemoteFeatureFlags().then((featFlags) => {

Check failure on line 44 in app/core/Engine/controllers/RemoteFeatureFlagController/index.ts

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Parameter 'featFlags' implicitly has an 'any' type.
Logger.log(`Received feature flags ${JSON.stringify(featFlags)}`);
});
}

return remoteFeatureFlagController;
}

export default {
init
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ControllerMessenger, EngineState } from '../../types';

export interface RemoteFeatureFlagInitParamTypes {
initialState: Partial<EngineState>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
initialState: Partial<EngineState>;
initialState: RemoteFeatureFlagControllerState;

controllerMessenger: ControllerMessenger,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
controllerMessenger: ControllerMessenger,
controllerMessenger: RemoteFeatureFlagControllerMessenger,

fetchFunction: typeof fetch,
disabled: boolean
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
DistributionType,
EnvironmentType,
} from '@metamask/remote-feature-flag-controller';

Check failure on line 4 in app/core/Engine/controllers/RemoteFeatureFlagController/utils.ts

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Cannot find module '@metamask/remote-feature-flag-controller' or its corresponding type declarations.

export const getFeatureFlagAppEnvironment = () => {
const env = process.env.METAMASK_ENVIRONMENT;
switch (env) {
case 'local': return EnvironmentType.Development;
case 'pre-release': return EnvironmentType.ReleaseCandidate;
case 'production': return EnvironmentType.Production;
default: return EnvironmentType.Development;
}
};

export const getFeatureFlagAppDistribution = () => {
const dist = process.env.METAMASK_BUILD_TYPE;
switch (dist) {
case 'main': return DistributionType.Main;
case 'flask': return DistributionType.Flask;
default: return DistributionType.Main;
}
};

6 changes: 6 additions & 0 deletions app/core/Engine/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@
} from '@metamask/accounts-controller';
import { BaseState } from '@metamask/base-controller';
import { getPermissionSpecifications } from '../Permissions/specifications.js';
import {
RemoteFeatureFlagController,
RemoteFeatureFlagControllerState
} from '@metamask/remote-feature-flag-controller';

Check failure on line 159 in app/core/Engine/types.ts

View workflow job for this annotation

GitHub Actions / scripts (lint:tsc)

Cannot find module '@metamask/remote-feature-flag-controller' or its corresponding type declarations.

/**
* Controllers that area always instantiated
Expand Down Expand Up @@ -282,6 +286,7 @@
SelectedNetworkController: SelectedNetworkController;
PhishingController: PhishingController;
PreferencesController: PreferencesController;
RemoteFeatureFlagController: RemoteFeatureFlagController;
PPOMController: PPOMController;
TokenBalancesController: TokenBalancesController;
TokenListController: TokenListController;
Expand Down Expand Up @@ -320,6 +325,7 @@
KeyringController: KeyringControllerState;
NetworkController: NetworkState;
PreferencesController: PreferencesState;
RemoteFeatureFlagController: RemoteFeatureFlagControllerState;
PhishingController: PhishingControllerState;
TokenBalancesController: TokenBalancesControllerState;
TokenRatesController: TokenRatesControllerState;
Expand Down
4 changes: 4 additions & 0 deletions app/core/EngineService/EngineService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ class EngineService {
name: 'PreferencesController',
key: `${engine.context.PreferencesController.name}:stateChange`,
},
{
name: 'RemoteFeatureFlagController',
key: `${engine.context.RemoteFeatureFlagController.name}:stateChange`,
},
{
name: 'SelectedNetworkController',
key: `${engine.context.SelectedNetworkController.name}:stateChange`,
Expand Down
51 changes: 0 additions & 51 deletions app/core/redux/slices/featureFlags/index.test.ts

This file was deleted.

Loading
Loading