diff --git a/suite-common/message-system/src/messageSystemTypes.ts b/suite-common/message-system/src/messageSystemTypes.ts index 05c3d336c07..fc9d72c607b 100644 --- a/suite-common/message-system/src/messageSystemTypes.ts +++ b/suite-common/message-system/src/messageSystemTypes.ts @@ -24,6 +24,7 @@ export const Feature = { ethClaim: 'eth.staking.claim', firmwareRevisionCheck: 'security.firmware.check', firmwareHashCheck: 'security.firmware.hashCheck', + solanaMobile: 'mobile.solana', } as const; export type FeatureDomain = (typeof Feature)[keyof typeof Feature]; diff --git a/suite-native/discovery/src/discoveryConfigSlice.ts b/suite-native/discovery/src/discoveryConfigSlice.ts index 69ed0ae22e2..9542c874c7f 100644 --- a/suite-native/discovery/src/discoveryConfigSlice.ts +++ b/suite-native/discovery/src/discoveryConfigSlice.ts @@ -1,6 +1,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { A, F, pipe } from '@mobily/ts-belt'; +import { MessageSystemRootState } from '@suite-common/message-system'; +import { createWeakMapSelector, returnStableArrayIfEmpty } from '@suite-common/redux-utils'; import { AccountsRootState, DeviceRootState, @@ -17,16 +19,15 @@ import { } from '@suite-native/config'; import { type NetworkSymbol } from '@suite-common/wallet-config'; import { - createSelectIsFeatureFlagEnabled, FeatureFlag, FeatureFlagsRootState, selectIsFeatureFlagEnabled, + selectIsSolanaEnabled, } from '@suite-native/feature-flags'; import { selectNetworkSymbolsOfAccountsWithTokensAllowed, TokensRootState, } from '@suite-native/tokens'; -import { createWeakMapSelector, returnStableArrayIfEmpty } from '@suite-common/redux-utils'; type DiscoveryInfo = { startTimestamp: number; @@ -97,11 +98,9 @@ export const selectDiscoveryInfo = (state: DiscoveryConfigSliceRootState) => state.discoveryConfig.discoveryInfo; const createMemoizedSelector = createWeakMapSelector.withTypes< - DeviceRootState & DiscoveryConfigSliceRootState & FeatureFlagsRootState + DeviceRootState & DiscoveryConfigSliceRootState & FeatureFlagsRootState & MessageSystemRootState >(); -const selectIsSolanaEnabled = createSelectIsFeatureFlagEnabled(FeatureFlag.IsSolanaEnabled); - export const selectFeatureFlagEnabledNetworkSymbols = createMemoizedSelector( [selectIsSolanaEnabled, selectAreTestnetsEnabled], (isSolanaEnabled, areTestnetsEnabled) => { diff --git a/suite-native/discovery/src/discoverySelectors.ts b/suite-native/discovery/src/discoverySelectors.ts index ec7c69dc75a..a19dbac9c70 100644 --- a/suite-native/discovery/src/discoverySelectors.ts +++ b/suite-native/discovery/src/discoverySelectors.ts @@ -27,6 +27,7 @@ import { TokenAddress, TokenSymbol } from '@suite-common/wallet-types'; import { isFirmwareVersionSupported } from '@suite-native/device'; import { FeatureFlagsRootState } from '@suite-native/feature-flags'; import { StaticSessionId } from '@trezor/connect'; +import { MessageSystemRootState } from '@suite-common/message-system'; import { DiscoveryConfigSliceRootState, @@ -106,7 +107,8 @@ export const selectNetworksWithUnfinishedDiscovery = ( state: DeviceRootState & AccountsRootState & FeatureFlagsRootState & - DiscoveryConfigSliceRootState, + DiscoveryConfigSliceRootState & + MessageSystemRootState, forcedAreTestnetsEnabled?: boolean, availableCardanoDerivations?: AccountType[], ) => { @@ -132,7 +134,8 @@ export const selectShouldRunDiscoveryForDevice = ( state: DeviceRootState & AccountsRootState & FeatureFlagsRootState & - DiscoveryConfigSliceRootState, + DiscoveryConfigSliceRootState & + MessageSystemRootState, ) => { // no discovery for PortfolioTracker ever const isPortfolioTrackerDevice = selectIsPortfolioTrackerDevice(state); diff --git a/suite-native/feature-flags/src/featureFlagsSlice.ts b/suite-native/feature-flags/src/featureFlagsSlice.ts index 1c2196b0c96..62c7530f694 100644 --- a/suite-native/feature-flags/src/featureFlagsSlice.ts +++ b/suite-native/feature-flags/src/featureFlagsSlice.ts @@ -7,9 +7,9 @@ export const FeatureFlag = { IsDeviceConnectEnabled: 'isDeviceConnectEnabled', IsRippleSendEnabled: 'isRippleSendEnabled', IsCardanoSendEnabled: 'isCardanoSendEnabled', - IsSolanaSendEnabled: 'isSolanaSendEnabled', IsRegtestEnabled: 'isRegtestEnabled', IsSolanaEnabled: 'IsSolanaEnabled', + IsSolanaEnabledByRemote: 'IsSolanaEnabledByRemote', // should be updated only via message system IsConnectPopupEnabled: 'IsConnectPopupEnabled', } as const; export type FeatureFlag = (typeof FeatureFlag)[keyof typeof FeatureFlag]; @@ -24,9 +24,9 @@ export const featureFlagsInitialState: FeatureFlagsState = { [FeatureFlag.IsDeviceConnectEnabled]: isAndroid() || isDebugEnv(), [FeatureFlag.IsRippleSendEnabled]: isAndroid() && isDevelopOrDebugEnv(), [FeatureFlag.IsCardanoSendEnabled]: isAndroid() && isDevelopOrDebugEnv(), - [FeatureFlag.IsSolanaSendEnabled]: isAndroid() && isDevelopOrDebugEnv(), [FeatureFlag.IsRegtestEnabled]: isDebugEnv() || isDetoxTestBuild(), [FeatureFlag.IsSolanaEnabled]: false, + [FeatureFlag.IsSolanaEnabledByRemote]: false, [FeatureFlag.IsConnectPopupEnabled]: isDevelopOrDebugEnv(), }; @@ -34,9 +34,9 @@ export const featureFlagsPersistedKeys: Array = [ FeatureFlag.IsDeviceConnectEnabled, FeatureFlag.IsRippleSendEnabled, FeatureFlag.IsCardanoSendEnabled, - FeatureFlag.IsSolanaSendEnabled, FeatureFlag.IsRegtestEnabled, FeatureFlag.IsSolanaEnabled, + FeatureFlag.IsSolanaEnabledByRemote, FeatureFlag.IsConnectPopupEnabled, ]; @@ -47,6 +47,12 @@ export const featureFlagsSlice = createSlice({ toggleFeatureFlag: (state, { payload }: PayloadAction<{ featureFlag: FeatureFlag }>) => { state[payload.featureFlag] = !state[payload.featureFlag]; }, + setFeatureFlag: ( + state, + { payload }: PayloadAction<{ featureFlag: FeatureFlag; value: boolean }>, + ) => { + state[payload.featureFlag] = payload.value; + }, }, }); @@ -57,5 +63,9 @@ export const createSelectIsFeatureFlagEnabled = export const selectIsFeatureFlagEnabled = (state: FeatureFlagsRootState, key: FeatureFlag) => state.featureFlags[key]; -export const { toggleFeatureFlag } = featureFlagsSlice.actions; +export const selectIsSolanaEnabled = (state: FeatureFlagsRootState) => + state.featureFlags[FeatureFlag.IsSolanaEnabledByRemote] || + state.featureFlags[FeatureFlag.IsSolanaEnabled]; + +export const { toggleFeatureFlag, setFeatureFlag } = featureFlagsSlice.actions; export const featureFlagsReducer = featureFlagsSlice.reducer; diff --git a/suite-native/message-system/src/components/KillswitchMessageScreen.tsx b/suite-native/message-system/src/components/KillswitchMessageScreen.tsx index 627a16f67df..d1f99b4af4d 100644 --- a/suite-native/message-system/src/components/KillswitchMessageScreen.tsx +++ b/suite-native/message-system/src/components/KillswitchMessageScreen.tsx @@ -4,8 +4,7 @@ import { A, G } from '@mobily/ts-belt'; import { Box, Button, PictogramTitleHeader, VStack } from '@suite-native/atoms'; import { prepareNativeStyle, useNativeStyles } from '@trezor/styles'; -import { messageSystemActions, selectActiveFeatureMessages } from '@suite-common/message-system'; -import { Variant } from '@suite-common/suite-types'; +import { messageSystemActions } from '@suite-common/message-system'; import { Translation } from '@suite-native/intl'; import { useOpenLink } from '@suite-native/link'; @@ -35,14 +34,13 @@ const buttonsWrapperStyle = prepareNativeStyle(_ => ({ width: '100%', })); -export const FeatureMessageScreen = () => { +export const KillswitchMessageScreen = () => { const dispatch = useDispatch(); const openLink = useOpenLink(); + const { applyStyle } = useNativeStyles(); const killswitch = A.head(useSelector(selectActiveKillswitchMessages)); - const { applyStyle } = useNativeStyles(); - if (!killswitch) return null; const { diff --git a/suite-native/message-system/src/index.ts b/suite-native/message-system/src/index.ts index 0f9d1a2c732..42e2857b83e 100644 --- a/suite-native/message-system/src/index.ts +++ b/suite-native/message-system/src/index.ts @@ -1,3 +1,4 @@ export * from './messageSystemMiddleware'; export * from './components/MessageSystemBannerRenderer'; export * from './components/KillswitchMessageScreen'; +export * from './selectors'; diff --git a/suite-native/message-system/src/messageSystemMiddleware.ts b/suite-native/message-system/src/messageSystemMiddleware.ts index e103146f36d..84a7a882760 100644 --- a/suite-native/message-system/src/messageSystemMiddleware.ts +++ b/suite-native/message-system/src/messageSystemMiddleware.ts @@ -12,6 +12,9 @@ import { selectDeviceEnabledDiscoveryNetworkSymbols, toggleEnabledDiscoveryNetworkSymbol, } from '@suite-native/discovery'; +import { FeatureFlag, setFeatureFlag } from '@suite-native/feature-flags'; + +import { selectIsSolanaFeatureEnabled } from './selectors'; const isAnyOfMessageSystemAffectingActions = isAnyOf( messageSystemActions.fetchSuccessUpdate, @@ -41,6 +44,15 @@ export const messageSystemMiddleware = createMiddleware((action, { next, dispatc const categorizedValidMessages = categorizeMessages(validMessages); dispatch(messageSystemActions.updateValidMessages(categorizedValidMessages)); + + const isSolanaRemoteFeatureEnabled = selectIsSolanaFeatureEnabled(getState()); + + dispatch( + setFeatureFlag({ + featureFlag: FeatureFlag.IsSolanaEnabledByRemote, + value: isSolanaRemoteFeatureEnabled, + }), + ); } return action; diff --git a/suite-native/message-system/src/selectors.ts b/suite-native/message-system/src/selectors.ts index 708494d3de8..c11a349a2a4 100644 --- a/suite-native/message-system/src/selectors.ts +++ b/suite-native/message-system/src/selectors.ts @@ -1,9 +1,22 @@ -import { createMemoizedSelector, selectActiveFeatureMessages } from '@suite-common/message-system'; +import { + createMemoizedSelector, + Feature, + MessageSystemRootState, + selectActiveFeatureMessages, + selectIsFeatureEnabled, +} from '@suite-common/message-system'; export const selectActiveKillswitchMessages = createMemoizedSelector( [selectActiveFeatureMessages], messages => - messages.filter( - m => m.feature?.filter(item => item.domain === 'killswitch' && item.flag) ?? false, - ), + messages.filter(m => { + const killswitchFeatures = m.feature?.filter( + item => item.domain === 'killswitch' && item?.flag, + ); + + return (killswitchFeatures?.length ?? 0) > 0; + }), ); + +export const selectIsSolanaFeatureEnabled = (state: MessageSystemRootState) => + selectIsFeatureEnabled(state, Feature.solanaMobile); diff --git a/suite-native/module-accounts-management/src/components/TransactionListHeader.tsx b/suite-native/module-accounts-management/src/components/TransactionListHeader.tsx index 2632267fc6e..10c34f578f4 100644 --- a/suite-native/module-accounts-management/src/components/TransactionListHeader.tsx +++ b/suite-native/module-accounts-management/src/components/TransactionListHeader.tsx @@ -21,6 +21,7 @@ import { } from '@suite-native/navigation'; import { Translation } from '@suite-native/intl'; import { FeatureFlag, FeatureFlagsRootState, useFeatureFlag } from '@suite-native/feature-flags'; +import { MessageSystemRootState } from '@suite-common/message-system'; import { AccountDetailGraph } from './AccountDetailGraph'; import { AccountDetailCryptoValue } from './AccountDetailCryptoValue'; @@ -100,8 +101,9 @@ export const TransactionListHeader = memo( const isTestnetAccount = useSelector((state: AccountsRootState) => selectIsTestnetAccount(state, accountKey), ); - const isNetworkSendFlowEnabled = useSelector((state: FeatureFlagsRootState) => - selectIsNetworkSendFlowEnabled(state, account?.symbol), + const isNetworkSendFlowEnabled = useSelector( + (state: FeatureFlagsRootState & MessageSystemRootState) => + selectIsNetworkSendFlowEnabled(state, account?.symbol), ); const isPortfolioTrackerDevice = useSelector(selectIsPortfolioTrackerDevice); diff --git a/suite-native/module-accounts-management/src/selectors.ts b/suite-native/module-accounts-management/src/selectors.ts index ddfb2ba8523..e0d3b84fc66 100644 --- a/suite-native/module-accounts-management/src/selectors.ts +++ b/suite-native/module-accounts-management/src/selectors.ts @@ -1,10 +1,12 @@ import { D, pipe } from '@mobily/ts-belt'; +import { MessageSystemRootState } from '@suite-common/message-system'; import { type NetworkSymbol, getNetworkType, networks } from '@suite-common/wallet-config'; import { FeatureFlagsRootState, selectIsFeatureFlagEnabled, FeatureFlag, + selectIsSolanaEnabled, } from '@suite-native/feature-flags'; const PRODUCTION_SEND_COINS_WHITELIST = pipe( @@ -14,13 +16,13 @@ const PRODUCTION_SEND_COINS_WHITELIST = pipe( ); export const selectIsNetworkSendFlowEnabled = ( - state: FeatureFlagsRootState, - symbol?: NetworkSymbol, + state: FeatureFlagsRootState & MessageSystemRootState, + networkSymbol?: NetworkSymbol, ) => { - if (!symbol) return false; - const networkType = getNetworkType(symbol); + if (!networkSymbol) return false; + const networkType = getNetworkType(networkSymbol); - if (PRODUCTION_SEND_COINS_WHITELIST.includes(symbol)) return true; + if (PRODUCTION_SEND_COINS_WHITELIST.includes(networkSymbol)) return true; const isRippleSendEnabled = selectIsFeatureFlagEnabled(state, FeatureFlag.IsRippleSendEnabled); @@ -33,9 +35,9 @@ export const selectIsNetworkSendFlowEnabled = ( if (isCardanoSendEnabled && networkType === 'cardano') return true; - const isSolanaSendEnabled = selectIsFeatureFlagEnabled(state, FeatureFlag.IsSolanaSendEnabled); + const isSolanaEnabled = selectIsSolanaEnabled(state); - if (isSolanaSendEnabled && networkType === 'solana') return true; + if (isSolanaEnabled && networkType === 'solana') return true; return false; }; diff --git a/suite-native/module-dev-utils/src/components/FeatureFlags.tsx b/suite-native/module-dev-utils/src/components/FeatureFlags.tsx index c8040d4982d..7a84c6b4db8 100644 --- a/suite-native/module-dev-utils/src/components/FeatureFlags.tsx +++ b/suite-native/module-dev-utils/src/components/FeatureFlags.tsx @@ -5,9 +5,9 @@ const featureFlagsTitleMap = { [FeatureFlagEnum.IsDeviceConnectEnabled]: 'Connect device', [FeatureFlagEnum.IsRippleSendEnabled]: 'Ripple send', [FeatureFlagEnum.IsCardanoSendEnabled]: 'Cardano send', - [FeatureFlagEnum.IsSolanaSendEnabled]: 'Solana send', [FeatureFlagEnum.IsRegtestEnabled]: 'Regtest', [FeatureFlagEnum.IsSolanaEnabled]: 'Solana', + [FeatureFlagEnum.IsSolanaEnabledByRemote]: 'Hidden in UI', [FeatureFlagEnum.IsConnectPopupEnabled]: 'Connect Popup', } as const satisfies Record; @@ -22,15 +22,21 @@ const FeatureFlag = ({ featureFlag }: { featureFlag: FeatureFlagEnum }) => { ); }; -export const FeatureFlags = () => ( - - - Feature Flags - - {Object.values(FeatureFlagEnum).map(featureFlag => ( - - ))} +export const FeatureFlags = () => { + const adjustableFeatureFlags = Object.values(FeatureFlagEnum).filter( + f => f !== FeatureFlagEnum.IsSolanaEnabledByRemote, + ); + + return ( + + + Feature Flags + + {adjustableFeatureFlags.map(featureFlag => ( + + ))} + - - -); + + ); +};