-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
base: main
Are you sure you want to change the base?
Changes from 29 commits
00a3bd1
b4f9cc8
cdd4a58
1787680
5299891
6204914
6f8d208
608f05b
a8ec092
01df041
c8c2af0
ca33006
27b32a3
d7aed0d
b80145d
9a67929
934a13a
faa6cb0
20c3b42
d22d1cb
ba1802e
5e97075
c838b21
426655e
ebc940e
8014b45
9a35568
8c305c1
c8495e5
d7d384c
b784e89
38292f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, the tests for this hook need to be updated There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
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, | ||||||
|
@@ -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, | ||||||
|
@@ -266,6 +268,10 @@ | |||||
) { | ||||||
this.controllerMessenger = new ExtendedControllerMessenger(); | ||||||
|
||||||
// Basic Functionality toggle defaults to true | ||||||
const getBasicFunctionalityToggleState = () => | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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', | ||||||
|
@@ -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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q: why testing the exact boolean false equality here?
Suggested change
and same on line 949 |
||||||
}); | ||||||
|
||||||
const phishingController = new PhishingController({ | ||||||
messenger: this.controllerMessenger.getRestricted({ | ||||||
name: 'PhishingController', | ||||||
|
@@ -933,8 +946,7 @@ | |||||
encryptor, | ||||||
getMnemonic: getPrimaryKeyringMnemonic.bind(this), | ||||||
getFeatureFlags: () => ({ | ||||||
disableSnaps: | ||||||
store.getState().settings.basicFunctionalityEnabled === false, | ||||||
disableSnaps: getBasicFunctionalityToggleState() === false, | ||||||
}), | ||||||
}); | ||||||
|
||||||
|
@@ -1135,7 +1147,7 @@ | |||||
|
||||||
return Boolean( | ||||||
hasProperty(showIncomingTransactions, currentChainId) && | ||||||
showIncomingTransactions?.[currentHexChainId], | ||||||
showIncomingTransactions?.[currentHexChainId], | ||||||
); | ||||||
}, | ||||||
updateTransactions: true, | ||||||
|
@@ -1387,6 +1399,7 @@ | |||||
GasFeeController: gasFeeController, | ||||||
ApprovalController: approvalController, | ||||||
PermissionController: permissionController, | ||||||
RemoteFeatureFlagController: remoteFeatureFlagController, | ||||||
SelectedNetworkController: selectedNetworkController, | ||||||
SignatureController: new SignatureController({ | ||||||
messenger: this.controllerMessenger.getRestricted({ | ||||||
|
@@ -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 GitHub Actions / scripts (lint:tsc)
|
||||||
this.controllerMessenger, | ||||||
); | ||||||
|
||||||
const { NftController: nfts } = this.context; | ||||||
|
||||||
|
@@ -1488,7 +1501,7 @@ | |||||
(state: NetworkState) => { | ||||||
if ( | ||||||
state.networksMetadata[state.selectedNetworkClientId].status === | ||||||
NetworkStatus.Available && | ||||||
NetworkStatus.Available && | ||||||
networkController.getNetworkClientById( | ||||||
networkController?.state.selectedNetworkClientId, | ||||||
).configuration.chainId !== currentChainId | ||||||
|
@@ -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 | ||||||
}`, | ||||||
); | ||||||
} | ||||||
|
@@ -1718,7 +1730,7 @@ | |||||
const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined; | ||||||
if ( | ||||||
accountsByChainId?.[toHexadecimal(chainId)]?.[ | ||||||
selectSelectedInternalAccountChecksummedAddress | ||||||
selectSelectedInternalAccountChecksummedAddress | ||||||
] | ||||||
) { | ||||||
const balanceBN = hexToBN( | ||||||
|
@@ -1755,7 +1767,7 @@ | |||||
|
||||||
const tokenBalances = | ||||||
allTokenBalances?.[selectedInternalAccount.address as Hex]?.[ | ||||||
chainId | ||||||
chainId | ||||||
] ?? {}; | ||||||
tokens.forEach( | ||||||
(item: { address: string; balance?: string; decimals: number }) => { | ||||||
|
@@ -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 | ||||||
|
@@ -2061,6 +2073,7 @@ | |||||
NetworkController, | ||||||
PreferencesController, | ||||||
PhishingController, | ||||||
RemoteFeatureFlagController, | ||||||
PPOMController, | ||||||
TokenBalancesController, | ||||||
TokenRatesController, | ||||||
|
@@ -2106,6 +2119,7 @@ | |||||
KeyringController, | ||||||
NetworkController, | ||||||
PhishingController, | ||||||
RemoteFeatureFlagController, | ||||||
PPOMController, | ||||||
PreferencesController, | ||||||
TokenBalancesController, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'; | ||||||
import Logger from '../../../../util/Logger'; | ||||||
import { RemoteFeatureFlagInitParamTypes } from './types'; | ||||||
import { | ||||||
getFeatureFlagAppEnvironment, | ||||||
getFeatureFlagAppDistribution | ||||||
} from './utils'; | ||||||
|
||||||
|
||||||
const init = ({ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add remote feature flag controller There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this a TODO? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. Yes unit tests please! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename to
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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({ | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
}, | ||||||
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) => { | ||||||
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>; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
controllerMessenger: ControllerMessenger, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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'; | ||
|
||
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; | ||
} | ||
}; | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 teamsI'll update the
CODEOWNERS
syntax to match files likefeatureFlagController/index.ts
but no further nested filesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
addressed on b784e89