diff --git a/src/components/AppLayout/Sidebar/useSidebarItems.tsx b/src/components/AppLayout/Sidebar/useSidebarItems.tsx index 58439e79a4..4cf8e76bbb 100644 --- a/src/components/AppLayout/Sidebar/useSidebarItems.tsx +++ b/src/components/AppLayout/Sidebar/useSidebarItems.tsx @@ -4,8 +4,13 @@ import { useRouteMatch } from 'react-router-dom' import { ListItemType } from 'src/components/List' import ListIcon from 'src/components/List/ListIcon' import { SAFELIST_ADDRESS } from 'src/routes/routes' +import { FEATURES } from 'src/config/networks/network.d' +import { useSelector } from 'react-redux' +import { safeFeaturesEnabledSelector } from 'src/logic/safe/store/selectors' const useSidebarItems = (): ListItemType[] => { + const featuresEnabled = useSelector(safeFeaturesEnabledSelector) + const safeAppsEnabled = Boolean(featuresEnabled?.includes(FEATURES.SAFE_APPS)) const matchSafe = useRouteMatch({ path: `${SAFELIST_ADDRESS}`, strict: false }) const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) const matchSafeWithAction = useRouteMatch({ path: `${SAFELIST_ADDRESS}/:safeAddress/:safeAction` }) as { @@ -13,11 +18,30 @@ const useSidebarItems = (): ListItemType[] => { params: Record } - const sidebarItems = useMemo((): ListItemType[] => { + return useMemo((): ListItemType[] => { if (!matchSafe || !matchSafeWithAddress) { return [] } + const settingsItem = { + label: 'Settings', + icon: , + selected: matchSafeWithAction?.params.safeAction === 'settings', + href: `${matchSafeWithAddress?.url}/settings`, + } + + const safeSidebar = safeAppsEnabled + ? [ + { + label: 'Apps', + icon: , + selected: matchSafeWithAction?.params.safeAction === 'apps', + href: `${matchSafeWithAddress?.url}/apps`, + }, + settingsItem, + ] + : [settingsItem] + return [ { label: 'ASSETS', @@ -37,22 +61,9 @@ const useSidebarItems = (): ListItemType[] => { selected: matchSafeWithAction?.params.safeAction === 'address-book', href: `${matchSafeWithAddress?.url}/address-book`, }, - { - label: 'Apps', - icon: , - selected: matchSafeWithAction?.params.safeAction === 'apps', - href: `${matchSafeWithAddress?.url}/apps`, - }, - { - label: 'Settings', - icon: , - selected: matchSafeWithAction?.params.safeAction === 'settings', - href: `${matchSafeWithAddress?.url}/settings`, - }, + ...safeSidebar, ] - }, [matchSafe, matchSafeWithAction, matchSafeWithAddress]) - - return sidebarItems + }, [matchSafe, matchSafeWithAction, matchSafeWithAddress, safeAppsEnabled]) } export { useSidebarItems } diff --git a/src/config/index.ts b/src/config/index.ts index 9e3e94b49c..45faa9d080 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -24,7 +24,7 @@ const getCurrentEnvironment = (): string => { type NetworkSpecificConfiguration = EnvironmentSettings & { network: NetworkSettings, - features?: SafeFeatures, + disabledFeatures?: SafeFeatures, } const configuration = (): NetworkSpecificConfiguration => { @@ -37,7 +37,7 @@ const configuration = (): NetworkSpecificConfiguration => { return { ...configFile.environment.production, network: configFile.network, - features: configFile.features, + disabledFeatures: configFile.disabledFeatures, } } @@ -49,7 +49,7 @@ const configuration = (): NetworkSpecificConfiguration => { return { ...networkBaseConfig, network: configFile.network, - features: configFile.features, + disabledFeatures: configFile.disabledFeatures, } } @@ -77,7 +77,7 @@ export const getNetworkExplorerInfo = (): { name: string; url: string; apiUrl: s apiUrl: getConfig()?.networkExplorerApiUrl, }) -export const getNetworkConfigFeatures = (): SafeFeatures | undefined => getConfig()?.features +export const getNetworkConfigDisabledFeatures = (): SafeFeatures => getConfig()?.disabledFeatures || [] export const getNetworkInfo = (): NetworkSettings => getConfig()?.network diff --git a/src/config/networks/network.d.ts b/src/config/networks/network.d.ts index fbdebfca83..c42b6ef889 100644 --- a/src/config/networks/network.d.ts +++ b/src/config/networks/network.d.ts @@ -1,4 +1,12 @@ // matches src/logic/tokens/store/model/token.ts `TokenProps` type + +export enum FEATURES { + ERC721 = 'ERC721', + ERC1155 = 'ERC1155', + SAFE_APPS = 'SAFE_APPS', + CONTRACT_INTERACTION = 'CONTRACT_INTERACTION' +} + type Token = { address: string name: string @@ -34,11 +42,7 @@ export type NetworkSettings = { // something around this to display or not some critical sections in the app, depending on the network support // I listed the ones that may conflict with the network. // If non is present, all the sections are available. -export type SafeFeatures = { - safeApps?: boolean, - collectibles?: boolean, - contractInteraction?: boolean -} +export type SafeFeatures = FEATURES[] type GasPrice = { gasPrice: number @@ -68,6 +72,6 @@ type SafeEnvironments = { export interface NetworkConfig { network: NetworkSettings - features?: SafeFeatures + disabledFeatures?: SafeFeatures environment: SafeEnvironments } diff --git a/src/logic/safe/store/actions/fetchSafe.ts b/src/logic/safe/store/actions/fetchSafe.ts index a398ffb733..e688ce222b 100644 --- a/src/logic/safe/store/actions/fetchSafe.ts +++ b/src/logic/safe/store/actions/fetchSafe.ts @@ -123,6 +123,9 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: Dispatch modules: buildModulesLinkedList(modules?.array, modules?.next), nonce: Number(remoteNonce), threshold: Number(remoteThreshold), + featuresEnabled: localSafe?.currentVersion + ? enabledFeatures(localSafe?.currentVersion) + : localSafe?.featuresEnabled, }), ) diff --git a/src/logic/safe/store/models/safe.ts b/src/logic/safe/store/models/safe.ts index 0311d3be2c..bd38c747fe 100644 --- a/src/logic/safe/store/models/safe.ts +++ b/src/logic/safe/store/models/safe.ts @@ -1,4 +1,5 @@ import { List, Map, Record, RecordOf, Set } from 'immutable' +import { FEATURES } from 'src/config/networks/network.d' export type SafeOwner = { name: string @@ -24,7 +25,7 @@ export type SafeRecordProps = { recurringUser?: boolean currentVersion: string needsUpdate: boolean - featuresEnabled: Array + featuresEnabled: Array } const makeSafe = Record({ diff --git a/src/logic/safe/utils/safeVersion.ts b/src/logic/safe/utils/safeVersion.ts index de6fc60b03..2c4b91d36a 100644 --- a/src/logic/safe/utils/safeVersion.ts +++ b/src/logic/safe/utils/safeVersion.ts @@ -5,13 +5,22 @@ import { GnosisSafe } from 'src/types/contracts/GnosisSafe.d' import { getGnosisSafeInstanceAt, getSafeMasterContract } from 'src/logic/contracts/safeContracts' import { LATEST_SAFE_VERSION } from 'src/utils/constants' +import { getNetworkConfigDisabledFeatures } from 'src/config' +import { FEATURES } from 'src/config/networks/network.d' -export const FEATURES = [ - { name: 'ERC721', validVersion: '>=1.1.1' }, - { name: 'ERC1155', validVersion: '>=1.1.1' }, +type FeatureConfigByVersion = { + name: FEATURES + validVersion?: string +} + +const FEATURES_BY_VERSION: FeatureConfigByVersion[] = [ + { name: FEATURES.ERC721, validVersion: '>=1.1.1' }, + { name: FEATURES.ERC1155, validVersion: '>=1.1.1' }, + { name: FEATURES.SAFE_APPS }, + { name: FEATURES.CONTRACT_INTERACTION }, ] -type Feature = typeof FEATURES[number] +type Feature = typeof FEATURES_BY_VERSION[number] export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string): boolean => { if (!currentVersion || !latestVersion) { @@ -27,13 +36,19 @@ export const safeNeedsUpdate = (currentVersion?: string, latestVersion?: string) export const getCurrentSafeVersion = (gnosisSafeInstance: GnosisSafe): Promise => gnosisSafeInstance.methods.VERSION().call() -export const enabledFeatures = (version: string): string[] => - FEATURES.reduce((acc: string[], feature: Feature) => { - if (semverSatisfies(version, feature.validVersion)) { +const checkFeatureEnabledByVersion = (featureConfig: FeatureConfigByVersion, version: string) => { + return featureConfig.validVersion ? semverSatisfies(version, featureConfig.validVersion) : true +} + +export const enabledFeatures = (version: string): FEATURES[] => { + const disabledFeatures = getNetworkConfigDisabledFeatures() + return FEATURES_BY_VERSION.reduce((acc: FEATURES[], feature: Feature) => { + if (!disabledFeatures.includes(feature.name) && checkFeatureEnabledByVersion(feature, version)) { acc.push(feature.name) } return acc }, []) +} interface SafeVersionInfo { current: string diff --git a/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx b/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx index 371dfb7a0f..ebbdc9fe19 100644 --- a/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx +++ b/src/routes/safe/components/Balances/SendModal/screens/ChooseTxType/index.tsx @@ -17,6 +17,7 @@ import ContractInteractionIcon from 'src/routes/safe/components/Transactions/Txs import Collectible from '../assets/collectibles.svg' import Token from '../assets/token.svg' +import { FEATURES } from 'src/config/networks/network.d' type ActiveScreen = 'sendFunds' | 'sendCollectible' | 'contractInteraction' @@ -29,7 +30,8 @@ interface ChooseTxTypeProps { const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }: ChooseTxTypeProps): React.ReactElement => { const classes = useStyles() const featuresEnabled = useSelector(safeFeaturesEnabledSelector) - const erc721Enabled = featuresEnabled?.includes('ERC721') + const erc721Enabled = featuresEnabled?.includes(FEATURES.ERC721) + const contractInteractionEnabled = featuresEnabled?.includes(FEATURES.CONTRACT_INTERACTION) const [disableContractInteraction, setDisableContractInteraction] = React.useState(!!recipientAddress) React.useEffect(() => { @@ -99,22 +101,24 @@ const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }: ChooseTxTy Send collectible )} - + {contractInteractionEnabled && ( + + )} diff --git a/src/routes/safe/components/Balances/index.tsx b/src/routes/safe/components/Balances/index.tsx index c72c001919..49bd4d8b59 100644 --- a/src/routes/safe/components/Balances/index.tsx +++ b/src/routes/safe/components/Balances/index.tsx @@ -17,13 +17,14 @@ import SendModal from 'src/routes/safe/components/Balances/SendModal' import CurrencyDropdown from 'src/routes/safe/components/CurrencyDropdown' import { safeFeaturesEnabledSelector, - safeParamAddressFromStateSelector, safeNameSelector, + safeParamAddressFromStateSelector, } from 'src/logic/safe/store/selectors' import { wrapInSuspense } from 'src/utils/wrapInSuspense' import { useFetchTokens } from 'src/logic/safe/hooks/useFetchTokens' -import { Route, Switch, NavLink, Redirect } from 'react-router-dom' +import { NavLink, Redirect, Route, Switch } from 'react-router-dom' +import { FEATURES } from 'src/config/networks/network.d' const Collectibles = React.lazy(() => import('src/routes/safe/components/Balances/Collectibles')) const Coins = React.lazy(() => import('src/routes/safe/components/Balances/Coins')) @@ -58,7 +59,7 @@ const Balances = (): React.ReactElement => { useFetchTokens(address as string) useEffect(() => { - const erc721Enabled = Boolean(featuresEnabled?.includes('ERC721')) + const erc721Enabled = Boolean(featuresEnabled?.includes(FEATURES.ERC721)) setState((prevState) => ({ ...prevState, diff --git a/src/routes/safe/container/index.tsx b/src/routes/safe/container/index.tsx index 2ca98cbf4b..2b3cbe6797 100644 --- a/src/routes/safe/container/index.tsx +++ b/src/routes/safe/container/index.tsx @@ -5,9 +5,10 @@ import { GenericModal } from '@gnosis.pm/safe-react-components' import NoSafe from 'src/components/NoSafe' import { providerNameSelector } from 'src/logic/wallets/store/selectors' -import { safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' +import { safeFeaturesEnabledSelector, safeParamAddressFromStateSelector } from 'src/logic/safe/store/selectors' import { wrapInSuspense } from 'src/utils/wrapInSuspense' import { SAFELIST_ADDRESS } from 'src/routes/routes' +import { FEATURES } from 'src/config/networks/network.d' export const BALANCES_TAB_BTN_TEST_ID = 'balances-tab-btn' export const SETTINGS_TAB_BTN_TEST_ID = 'settings-tab-btn' @@ -34,7 +35,9 @@ const Container = (): React.ReactElement => { const safeAddress = useSelector(safeParamAddressFromStateSelector) const provider = useSelector(providerNameSelector) + const featuresEnabled = useSelector(safeFeaturesEnabledSelector) const matchSafeWithAddress = useRouteMatch<{ safeAddress: string }>({ path: `${SAFELIST_ADDRESS}/:safeAddress` }) + const safeAppsEnabled = Boolean(featuresEnabled?.includes(FEATURES.SAFE_APPS)) if (!safeAddress) { return @@ -67,7 +70,17 @@ const Container = (): React.ReactElement => { path={`${matchSafeWithAddress?.path}/transactions`} render={() => wrapInSuspense(, null)} /> - wrapInSuspense(, null)} /> + { + if (!safeAppsEnabled) { + history.push(`${matchSafeWithAddress?.url}/balances`) + } + return wrapInSuspense(, null) + }} + /> +