diff --git a/app/components/UI/DrawerView/index.js b/app/components/UI/DrawerView/index.js index 98f8962961a..39f8c0ba6f5 100644 --- a/app/components/UI/DrawerView/index.js +++ b/app/components/UI/DrawerView/index.js @@ -7,6 +7,7 @@ import { StyleSheet, Text, InteractionManager, + Platform, } from 'react-native'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; @@ -27,6 +28,7 @@ import { strings } from '../../../../locales/i18n'; import Modal from 'react-native-modal'; import { toggleInfoNetworkModal, + toggleNetworkModal, toggleReceiveModal, } from '../../../actions/modals'; import { showAlert } from '../../../actions/alert'; @@ -47,6 +49,7 @@ import { getEther } from '../../../util/transactions'; import { newAssetTransaction } from '../../../actions/transaction'; import { protectWalletModalVisible } from '../../../actions/user'; import DeeplinkManager from '../../../core/DeeplinkManager/SharedDeeplinkManager'; +import SettingsNotification from '../SettingsNotification'; import { RPC } from '../../../constants/network'; import { findRouteNameFromNavigatorState } from '../../../util/general'; import AnalyticsV2 from '../../../util/analyticsV2'; @@ -56,6 +59,7 @@ import { } from '../../../util/ENSUtils'; import ClipboardManager from '../../../core/ClipboardManager'; import { collectiblesSelector } from '../../../reducers/collectibles'; +import { getCurrentRoute } from '../../../reducers/navigation'; import { ScrollView } from 'react-native-gesture-handler'; import { isZero } from '../../../util/lodash'; import { Authentication } from '../../../core/'; @@ -67,6 +71,7 @@ import { } from '../../../actions/onboardNetwork'; import Routes from '../../../constants/navigation/Routes'; import { scale } from 'react-native-size-matters'; +import generateTestId from '../../../../wdio/utils/generateTestId'; import { DRAWER_VIEW_LOCK_TEXT_ID } from '../../../../wdio/screen-objects/testIDs/Screens/DrawerView.testIds'; import { selectNetworkConfigurations, @@ -357,6 +362,10 @@ class DrawerView extends PureComponent { * List of keyrings */ keyrings: PropTypes.array, + /** + * Action that toggles the network modal + */ + toggleNetworkModal: PropTypes.func, /** * Action that toggles the receive modal */ @@ -365,6 +374,10 @@ class DrawerView extends PureComponent { * Action that shows the global alert */ showAlert: PropTypes.func.isRequired, + /** + * Boolean that determines the status of the networks modal + */ + networkModalVisible: PropTypes.bool.isRequired, /** * Boolean that determines the status of the receive modal */ @@ -377,6 +390,10 @@ class DrawerView extends PureComponent { * Boolean that determines if the user has set a password before */ passwordSet: PropTypes.bool, + /** + * Wizard onboarding state + */ + wizard: PropTypes.object, /** * Current provider ticker */ @@ -410,11 +427,18 @@ class DrawerView extends PureComponent { * Callback to close drawer */ onCloseDrawer: PropTypes.func, - + /** + * Latest navigation route + */ + currentRoute: PropTypes.string, /** * handles action for onboarding to a network */ onboardNetworkAction: PropTypes.func, + /** + * returns switched network state + */ + switchedNetwork: PropTypes.object, /** * updates when network is switched */ @@ -976,6 +1000,8 @@ class DrawerView extends PureComponent { identities, selectedAddress, currentCurrency, + seedphraseBackedUp, + currentRoute, navigation, infoNetworkModalVisible, } = this.props; @@ -1092,6 +1118,81 @@ class DrawerView extends PureComponent { + + {this.getSections().map( + (section, i) => + section?.length > 0 && ( + + {section + .filter((item) => { + if (!item) return undefined; + const { name = undefined } = item; + if ( + name && + name.toLowerCase().indexOf('etherscan') !== -1 + ) { + const type = providerConfig?.type; + return ( + (type && this.hasBlockExplorer(type)) || undefined + ); + } + return true; + }) + .map((item, j) => ( + item.action()} // eslint-disable-line + > + {item.icon + ? item.routeNames && + item.routeNames.includes(currentRoute) + ? item.selectedIcon + : item.icon + : null} + + {item.name} + + {!seedphraseBackedUp && item.warning ? ( + + + {item.warning} + + + ) : null} + + ))} + + ), + )} + ({ networkConfigurations: selectNetworkConfigurations(state), currentCurrency: selectCurrentCurrency(state), keyrings: state.engine.backgroundState.KeyringController.keyrings, + networkModalVisible: state.modals.networkModalVisible, receiveModalVisible: state.modals.receiveModalVisible, infoNetworkModalVisible: state.modals.infoNetworkModalVisible, passwordSet: state.user.passwordSet, + wizard: state.wizard, ticker: selectTicker(state), tokens: selectTokens(state), tokenBalances: selectContractBalances(state), collectibles: collectiblesSelector(state), seedphraseBackedUp: state.user.seedphraseBackedUp, + currentRoute: getCurrentRoute(state), + switchedNetwork: state.networkOnboarded.switchedNetwork, }); const mapDispatchToProps = (dispatch) => ({ + toggleNetworkModal: () => dispatch(toggleNetworkModal()), toggleReceiveModal: () => dispatch(toggleReceiveModal()), showAlert: (config) => dispatch(showAlert(config)), newAssetTransaction: (selectedAsset) => diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts index db5d8ca8913..d427681daba 100644 --- a/app/core/EngineService/EngineService.ts +++ b/app/core/EngineService/EngineService.ts @@ -13,10 +13,8 @@ interface InitializeEngineResult { error?: string; } -const UPDATE_BG_STATE_KEY = (controllerName: string) => - `UPDATE_BG_STATE_${controllerName}`; -const INIT_BG_STATE_KEY = (controllerName: string) => - `INIT_BG_STATE_${controllerName}`; +const UPDATE_BG_STATE_KEY = 'UPDATE_BG_STATE'; +const INIT_BG_STATE_KEY = 'INIT_BG_STATE'; class EngineService { private engineInitialized = false; @@ -104,13 +102,7 @@ class EngineService { engine?.datamodel?.subscribe?.(() => { if (!this.engineInitialized) { - controllers.forEach((controller) => { - const { name } = controller; - store.dispatch({ - type: INIT_BG_STATE_KEY(name), - payload: { key: name }, - }); - }); + store.dispatch({ type: INIT_BG_STATE_KEY }); this.engineInitialized = true; } }); @@ -118,10 +110,7 @@ class EngineService { controllers.forEach((controller) => { const { name, key = undefined } = controller; const update_bg_state_cb = () => { - store.dispatch({ - type: UPDATE_BG_STATE_KEY(name), - payload: { key: name }, - }); + store.dispatch({ type: UPDATE_BG_STATE_KEY, payload: { key: name } }); }; if (key) { engine.controllerMessenger.subscribe(key, update_bg_state_cb); diff --git a/app/core/redux/slices/engine/engineReducer.test.tsx b/app/core/redux/slices/engine/engineReducer.test.tsx new file mode 100644 index 00000000000..af85899f9db --- /dev/null +++ b/app/core/redux/slices/engine/engineReducer.test.tsx @@ -0,0 +1,51 @@ +import engineReducer, { initBgState, updateBgState } from '.'; + +jest.mock('../../../Engine', () => ({ + init: () => jest.requireActual('../../../Engine').default.init({}), + state: {}, +})); +// importing Engine after mocking to avoid global mock from overwriting its values +import Engine from '../../../Engine'; + +describe('engineReducer', () => { + it('should return the initial state in default', () => { + jest.isolateModules(() => { + jest.mock('../../../Engine', () => ({ + init: () => jest.requireActual('../../../Engine').default.init({}), + state: {}, + })); + }); + + const initialStatDefault = { backgroundState: {} }; + const nextState = engineReducer(undefined, {} as any); + expect(nextState).toEqual(initialStatDefault); + }); + + it('should initialize backgroundState when dispatching INIT_BG_STATE action', () => { + const initialState = { backgroundState: {} }; + const nextState = engineReducer(initialState, initBgState()); + expect(nextState).toEqual({ backgroundState: Engine.state }); + }); + + it('should update backgroundState when dispatching UPDATE_BG_STATE action', () => { + const reduxInitialState = { + backgroundState: { + AccountTrackerController: {}, + }, + }; + + const key = 'AccountTrackerController'; + // changing the mock version to suit this test manually due to our current global mock + (Engine as any).state = { + AccountTrackerController: { accounts: 'testValue' }, + }; + const { backgroundState } = engineReducer( + reduxInitialState, + updateBgState({ key }), + ); + expect(backgroundState).toEqual({ + ...reduxInitialState.backgroundState, + AccountTrackerController: Engine.state[key], + }); + }); +}); diff --git a/app/core/redux/slices/engine/index.ts b/app/core/redux/slices/engine/index.ts new file mode 100644 index 00000000000..700f13624cd --- /dev/null +++ b/app/core/redux/slices/engine/index.ts @@ -0,0 +1,39 @@ +import Engine from '../../../Engine'; +import { createAction, PayloadAction } from '@reduxjs/toolkit'; + +const initialState = { + backgroundState: {} as any, +}; + +// Create an action to initialize the background state +export const initBgState = createAction('INIT_BG_STATE'); + +// Create an action to update the background state +export const updateBgState = createAction('UPDATE_BG_STATE', (key) => ({ + payload: key, +})); + +export const counter: any = {}; +const engineReducer = ( + // eslint-disable-next-line @typescript-eslint/default-param-last + state = initialState, + action: PayloadAction<{ key: any } | undefined>, +) => { + switch (action.type) { + case initBgState.type: { + return { backgroundState: Engine.state }; + } + case updateBgState.type: { + const newState = { ...state }; + if (action.payload) { + newState.backgroundState[action.payload?.key] = + Engine.state[action.payload.key as keyof typeof Engine.state]; + } + return newState; + } + default: + return state; + } +}; + +export default engineReducer; diff --git a/app/reducers/index.ts b/app/reducers/index.ts index b6ffe6d6a51..84511b32df0 100644 --- a/app/reducers/index.ts +++ b/app/reducers/index.ts @@ -1,6 +1,6 @@ import bookmarksReducer from './bookmarks'; import browserReducer from './browser'; -import engineReducer from '../redux/slices/engine'; +import engineReducer from '../core/redux/slices/engine'; import privacyReducer from './privacy'; import modalsReducer from './modals'; import settingsReducer from './settings'; @@ -11,11 +11,11 @@ import wizardReducer from './wizard'; import onboardingReducer from './onboarding'; import fiatOrders from './fiatOrders'; import swapsReducer from './swaps'; -import navigationReducer from './navigation'; import signatureRequestReducer from './signatureRequest'; import notificationReducer from './notification'; import infuraAvailabilityReducer from './infuraAvailability'; import collectiblesReducer from './collectibles'; +import navigationReducer from './navigation'; import networkOnboardReducer from './networkSelector'; import securityReducer from './security'; import { combineReducers, Reducer } from 'redux'; @@ -83,11 +83,11 @@ const rootReducer = combineReducers({ wizard: wizardReducer, onboarding: onboardingReducer, notification: notificationReducer, - navigation: navigationReducer, signatureRequest: signatureRequestReducer, swaps: swapsReducer, fiatOrders, infuraAvailability: infuraAvailabilityReducer, + navigation: navigationReducer, networkOnboarded: networkOnboardReducer, security: securityReducer, experimentalSettings: experimentalSettingsReducer, diff --git a/app/reducers/navigation/index.ts b/app/reducers/navigation/index.ts index 738c8c0f2f5..7b20f017f07 100644 --- a/app/reducers/navigation/index.ts +++ b/app/reducers/navigation/index.ts @@ -37,5 +37,8 @@ const navigationReducer = (state = initialState, action: any = {}) => { /** * Selectors */ +export const getCurrentRoute = (state: any) => state.navigation.currentRoute; +export const getCurrentBottomNavRoute = (state: any) => + state.navigation.currentBottomNavRoute; export default navigationReducer; diff --git a/app/redux/slices/engine/engineReducer.test.tsx b/app/redux/slices/engine/engineReducer.test.tsx deleted file mode 100644 index 428b9b05d77..00000000000 --- a/app/redux/slices/engine/engineReducer.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -// TODO: Adapt tests when RTK is reintroduced for engine -import engineReducer from '.'; -import { createAction } from '@reduxjs/toolkit'; - -// Create an action to initialize the background state -const initBgState = createAction('INIT_BG_STATE'); - -jest.mock('../../../core/Engine', () => ({ - init: () => jest.requireActual('../../../core/Engine').default.init({}), - state: {}, -})); - -const backgroundState = { - AccountTrackerController: {}, - AddressBookController: {}, - ApprovalController: {}, - AssetsContractController: {}, - CurrencyRateController: {}, - GasFeeController: {}, - KeyringController: {}, - LoggingController: {}, - NetworkController: {}, - NftController: {}, - NftDetectionController: {}, - PPOMController: {}, - PermissionController: {}, - PhishingController: {}, - PreferencesController: {}, - SwapsController: {}, - SnapController: {}, - TokenBalancesController: {}, - TokenDetectionController: {}, - TokenListController: {}, - TokenRatesController: {}, - TokensController: {}, - TransactionController: {}, -}; - -describe('engineReducer', () => { - it('should return the initial state in default', () => { - jest.isolateModules(() => { - jest.mock('../../../core/Engine', () => ({ - init: () => - jest.requireActual('../../../core/Enginee').default.init({}), - state: {}, - })); - }); - - const initialStatDefault = { backgroundState }; - const nextState = engineReducer(undefined, {} as any); - expect(nextState).toEqual(initialStatDefault); - }); - - it('should initialize backgroundState when dispatching INIT_BG_STATE action', () => { - const initialState = { backgroundState }; - const nextState = engineReducer(initialState, initBgState()); - expect(nextState).toEqual({ backgroundState }); - }); -}); diff --git a/app/redux/slices/engine/index.ts b/app/redux/slices/engine/index.ts deleted file mode 100644 index c0cad04d0b7..00000000000 --- a/app/redux/slices/engine/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -import Engine from '../../../core/Engine'; -import { persistReducer } from 'redux-persist'; -import { combineReducers, Reducer } from 'redux'; -import createPersistConfig from '../../../store/persistConfig'; - -const controllerNames = [ - { name: 'AccountTrackerController', initialState: {} }, - { name: 'AddressBookController', initialState: {} }, - { name: 'AssetsContractController', initialState: {} }, - { name: 'NftController', initialState: {} }, - { name: 'TokensController', initialState: {} }, - { name: 'TokenDetectionController', initialState: {} }, - { name: 'NftDetectionController', initialState: {} }, - { - name: 'KeyringController', - initialState: {}, - }, - { name: 'AccountTrackerController', initialState: {} }, - { - name: 'NetworkController', - initialState: {}, - }, - { - name: 'PhishingController', - initialState: {}, - denyList: ['phishing', 'whitelist'], - }, - { name: 'PreferencesController', initialState: {} }, - { name: 'TokenBalancesController', initialState: {} }, - { name: 'TokenRatesController', initialState: {} }, - { name: 'TransactionController', initialState: {} }, - ///: BEGIN:ONLY_INCLUDE_IF(snaps) - { name: 'SnapController', initialState: {} }, - ///: END:ONLY_INCLUDE_IF - { - name: 'SwapsController', - initialState: {}, - denyList: [ - 'aggregatorMetadata', - 'aggregatorMetadataLastFetched', - 'chainCache', - 'tokens', - 'tokensLastFetched', - 'topAssets', - 'topAssetsLastFetched', - ], - }, - { - name: 'TokenListController', - initialState: {}, - denyList: ['tokenList, tokensChainCache'], - }, - { - name: 'CurrencyRateController', - initialState: {}, - }, - { - name: 'GasFeeController', - initialState: {}, - }, - { - name: 'ApprovalController', - initialState: {}, - }, - { - name: 'PermissionController', - initialState: {}, - }, - { - name: 'LoggingController', - initialState: {}, - }, - { - name: 'PPOMController', - initialState: {}, - }, -]; - -const controllerReducer = - ({ - controllerName, - initialState, - }: { - controllerName: string; - initialState: any; - }) => - // eslint-disable-next-line @typescript-eslint/default-param-last - (state = initialState, action: any) => { - switch (action.type) { - case `INIT_BG_STATE_${controllerName || action.key}`: { - const initialEngineValue = - Engine.state[controllerName as keyof typeof Engine.state]; - const returnedState = { - ...state, - ...initialEngineValue, - }; - - return returnedState; - } - case `UPDATE_BG_STATE_${controllerName}`: { - return { ...Engine.state[controllerName as keyof typeof Engine.state] }; - } - default: - return state; - } - }; - -export const controllerReducers = controllerNames.reduce( - (output, controllerConfig) => { - const { name, initialState, denyList = [] } = controllerConfig; - - const reducer = persistReducer( - createPersistConfig({ key: name, blacklist: denyList }), - controllerReducer({ controllerName: name, initialState }), - ); - - output[name] = reducer; - return output; - }, - {} as Record>, -); - -const engineReducer = combineReducers({ - backgroundState: combineReducers(controllerReducers), -}); - -/** - * Engine Reducer - * - * Note: This reducer is not yet using RTK (Redux Toolkit) slice. - * - * @param {object} state - The current state. - * @param {object} action - The dispatched action. - * @returns {object} - new state. - */ -export default engineReducer; diff --git a/app/redux/storage/MigratedStorage.ts b/app/redux/storage/MigratedStorage.ts deleted file mode 100644 index 4734eb47d8c..00000000000 --- a/app/redux/storage/MigratedStorage.ts +++ /dev/null @@ -1,39 +0,0 @@ -import FilesystemStorage from 'redux-persist-filesystem-storage'; -import Logger from '../../util/Logger'; -import { Platform } from 'react-native'; - -const createMigratedStorage = (reducerName: string) => ({ - async getItem(key: string) { - try { - const res = await FilesystemStorage.getItem(key); - if (res) { - // Using the new storage system - return res; - } - } catch (error) { - Logger.error(error as Error, { - message: `Failed to get persisted ${key}: ${reducerName}`, - }); - } - }, - async setItem(key: string, value: string) { - try { - return await FilesystemStorage.setItem(key, value, Platform.OS === 'ios'); - } catch (error) { - Logger.error(error as Error, { - message: `Failed to set persisted ${key}: ${reducerName}`, - }); - } - }, - async removeItem(key: string) { - try { - return await FilesystemStorage.removeItem(key); - } catch (error) { - Logger.error(error as Error, { - message: `Failed to remove persisted ${key}: ${reducerName}`, - }); - } - }, -}); - -export default createMigratedStorage; diff --git a/app/store/index.ts b/app/store/index.ts index 817a3b163bb..d89bd9d59fe 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -11,11 +11,10 @@ import ReadOnlyNetworkStore from '../util/test/network-store'; import { isTest } from '../util/test/utils'; import thunk from 'redux-thunk'; -import { rootPersistConfig } from './persistConfig'; +import persistConfig from './persistConfig'; // TODO: Improve type safety by using real Action types instead of `any` - -const pReducer = persistReducer(rootPersistConfig, rootReducer); +const pReducer = persistReducer(persistConfig, rootReducer); // TODO: Fix the Action type. It's set to `any` now because some of the // TypeScript reducers have invalid actions diff --git a/app/store/persistConfig.ts b/app/store/persistConfig.ts index 0c4019ff208..ad3be84e0ef 100644 --- a/app/store/persistConfig.ts +++ b/app/store/persistConfig.ts @@ -1,11 +1,99 @@ import { createMigrate, createTransform } from 'redux-persist'; +import AsyncStorage from './async-storage-wrapper'; +import FilesystemStorage from 'redux-persist-filesystem-storage'; import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; -import MigratedStorage from '../redux/storage/MigratedStorage'; +import { RootState } from '../reducers'; import { migrations, version } from './migrations'; import Logger from '../util/Logger'; +import Device from '../util/device'; const TIMEOUT = 40000; +const MigratedStorage = { + async getItem(key: string) { + try { + const res = await FilesystemStorage.getItem(key); + if (res) { + // Using new storage system + return res; + } + } catch { + //Fail silently + } + + // Using old storage system, should only happen once + try { + const res = await AsyncStorage.getItem(key); + if (res) { + // Using old storage system + return res; + } + } catch (error) { + Logger.error(error as Error, { message: 'Failed to run migration' }); + throw new Error('Failed async storage storage fetch.'); + } + }, + async setItem(key: string, value: string) { + try { + return await FilesystemStorage.setItem(key, value, Device.isIos()); + } catch (error) { + Logger.error(error as Error, { message: 'Failed to set item' }); + } + }, + async removeItem(key: string) { + try { + return await FilesystemStorage.removeItem(key); + } catch (error) { + Logger.error(error as Error, { message: 'Failed to remove item' }); + } + }, +}; + +/** + * Transform middleware that blacklists fields from redux persist that we deem too large for persisted storage + */ +const persistTransform = createTransform( + (inboundState: RootState['engine']) => { + const { + TokenListController, + SwapsController, + PhishingController, + ...controllers + } = inboundState.backgroundState || {}; + // TODO: Fix this type error + // @ts-expect-error Fix this typo, should be `tokensChainsCache` + const { tokenList, tokensChainCache, ...persistedTokenListController } = + TokenListController; + const { + aggregatorMetadata, + aggregatorMetadataLastFetched, + chainCache, + tokens, + tokensLastFetched, + topAssets, + topAssetsLastFetched, + ...persistedSwapsController + } = SwapsController; + // TODO: Fix this type error + // @ts-expect-error There is no `phishing` property in the phishing controller state + const { phishing, whitelist, ...persistedPhishingController } = + PhishingController; + + // Reconstruct data to persist + const newState = { + backgroundState: { + ...controllers, + TokenListController: persistedTokenListController, + SwapsController: persistedSwapsController, + PhishingController: persistedPhishingController, + }, + }; + return newState; + }, + null, + { whitelist: ['engine'] }, +); + const persistUserTransform = createTransform( // TODO: Add types for the 'user' slice (inboundState: any) => { @@ -17,44 +105,17 @@ const persistUserTransform = createTransform( { whitelist: ['user'] }, ); -interface PersistConfig { - key: string; - version?: number; - blacklist?: string[]; - transforms?: any[]; - stateReconciler?: any; - migrate?: any; - timeout?: number; - writeFailHandler?: (error: Error) => void; -} - -const createPersistConfig = ({ - key, - blacklist, - transforms, - migrate, - timeout, - writeFailHandler, -}: PersistConfig) => ({ - key, +const persistConfig = { + key: 'root', version, - blacklist, - storage: MigratedStorage(key), - transforms, + blacklist: ['onboarding', 'rpcEvents', 'accounts'], + storage: MigratedStorage, + transforms: [persistTransform, persistUserTransform], stateReconciler: autoMergeLevel2, // see "Merge Process" section for details. - migrate, - timeout, - writeFailHandler, -}); - -export const rootPersistConfig = createPersistConfig({ - key: 'root', - blacklist: ['onboarding', 'rpcEvents', 'accounts', 'engine'], - transforms: [persistUserTransform], migrate: createMigrate(migrations, { debug: false }), timeout: TIMEOUT, writeFailHandler: (error: Error) => Logger.error(error, { message: 'Error persisting data' }), // Log error if saving state fails -}); +}; -export default createPersistConfig; +export default persistConfig;