diff --git a/__tests__/sagas/networkSettings.test.ts b/__tests__/sagas/networkSettings.test.ts index 41f5e707c..33ca7839d 100644 --- a/__tests__/sagas/networkSettings.test.ts +++ b/__tests__/sagas/networkSettings.test.ts @@ -4,7 +4,7 @@ import { all, effectTypes, fork } from 'redux-saga/effects'; import createSagaMiddleware, { END, runSaga } from 'redux-saga'; import { applyMiddleware, createStore } from 'redux'; import { reducer } from '../../src/reducers/reducer'; -import { networkSettingsUpdate, networkSettingsUpdateSuccess, reloadWalletRequested, types } from '../../src/actions'; +import { networkSettingsUpdateRequest, networkSettingsUpdateSuccess, reloadWalletRequested, types } from '../../src/actions'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { networkSettingsKeyMap } from '../../src/constants'; import { STORE } from '../../src/store'; @@ -46,47 +46,47 @@ describe('updateNetworkSettings', () => { const task = middleware.run(defaultSaga); Promise.resolve() - .then(() => store.dispatch(networkSettingsUpdate(null))) - .then(() => store.dispatch(networkSettingsUpdate(undefined))) - .then(() => store.dispatch(networkSettingsUpdate({}))) - .then(() => store.dispatch(networkSettingsUpdate({ explorerUrl: undefined }))) - .then(() => store.dispatch(networkSettingsUpdate({ explorerUrl: null }))) - .then(() => store.dispatch(networkSettingsUpdate({ explorerUrl: '' }))) - .then(() => store.dispatch(networkSettingsUpdate({ explorerUrl: 1 }))) - .then(() => store.dispatch(networkSettingsUpdate({ explorerUrl: 'invalid.url.com' }))) + .then(() => store.dispatch(networkSettingsUpdateRequest(null))) + .then(() => store.dispatch(networkSettingsUpdateRequest(undefined))) + .then(() => store.dispatch(networkSettingsUpdateRequest({}))) + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: undefined }))) + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: null }))) + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: '' }))) + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 1 }))) + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'invalid.url.com' }))) // explorerUrl is valid, however it must have at least nodeUrl - .then(() => store.dispatch(networkSettingsUpdate({ explorerUrl: 'http://localhost:8081/' }))) + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/' }))) // explorerUrl is valid, but explorerServiceUrl is empty - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: '', }))) // explorerUrl is valid, but explorerServiceUrl is invalid - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'invalid.url.com', }))) // explorer urls are valid, but nodeUrl is empty - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: '', }))) // explorer urls are valid, but nodeUrl is invalid - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'invalid.url.com' }))) // explorer and node urls are valid, but waletServiceUrl is invalid - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'http://localhost:3000/', walletServiceUrl: 'invalid.url.com' }))) // explorer, node, and wallet service urls are valid, but walletServiceWsUrl is empty - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'http://localhost:3000/', @@ -94,7 +94,7 @@ describe('updateNetworkSettings', () => { walletServiceWsUrl: '' }))) // explorer, node, and wallet service urls are valid, but walletServiceWsUrl is invalid - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'http://localhost:3000/', @@ -102,7 +102,7 @@ describe('updateNetworkSettings', () => { walletServiceWsUrl: 'invalid.url.com' }))) // all urls are valid, except nodeUrl - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'invalid.url.com', @@ -171,21 +171,21 @@ describe('updateNetworkSettings', () => { Promise.resolve() // calls getFullnodeNetwork - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'http://localhost:3000/', }))) // calls getWalletServiceNetwork // it will fail because is lacking the walletServiceWsUrl - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'http://localhost:3000/', walletServiceUrl: 'http://localhost:8080/' }))) // calls getWalletServiceNetwork - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'http://localhost:3000/', @@ -193,14 +193,14 @@ describe('updateNetworkSettings', () => { walletServiceWsUrl: 'ws://ws.localhost:4040/' }))) // calls getFullnodeNetwork - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'http://localhost:3000/', }))) // calls getFullnodeNetwork // here the getFullnodeNetwork rejects throwing an error - .then(() => store.dispatch(networkSettingsUpdate({ + .then(() => store.dispatch(networkSettingsUpdateRequest({ explorerUrl: 'http://localhost:8081/', explorerServiceUrl: 'http://localhost:8082/', nodeUrl: 'http://localhost:3000/', diff --git a/src/actions.js b/src/actions.js index 89574e30a..72e80a852 100644 --- a/src/actions.js +++ b/src/actions.js @@ -120,11 +120,22 @@ export const types = { // NOTE: These actions follows a taxonomy that should be applied // to all other actions. // See: https://github.com/HathorNetwork/hathor-wallet-mobile/issues/334 - NETWORKSETTINGS_UPDATE: 'NETWORK_SETTINGS_UPDATE', + /* It initiates an update of the network settings based on user input from a form. */ + NETWORKSETTINGS_UPDATE_REQUEST: 'NETWORK_SETTINGS_UPDATE_REQUEST', + /* It updates the redux state */ + NETWORKSETTINGS_UPDATE_STATE: 'NETWORKSETTINGS_UPDATE_STATE', + /* It persists the complete structure of network settings in the app storage and updates the redux store. */ + NETWORKSETTINGS_PERSIST_STORE: 'NETWORKSETTINGS_PERSIST_STORE', + /* It indicates the persistence is complete and the wallet will be reloaded. */ + NETWORKSETTINGS_UPDATE_WAITING: 'NETWORKSETTINGS_UPDATE_WAITING', + /* It indicates the update is complete after the wallet reloads. */ NETWORKSETTINGS_UPDATE_SUCCESS: 'NETWORK_SETTINGS_UPDATE_SUCCESS', - NETWORKSETTINGS_UPDATE_READY: 'NETWORK_SETTINGS_UPDATE_READY', + /* It indicates the update request has invalid inputs. */ + NETWORKSETTINGS_UPDATE_INVALID: 'NETWORKSETTINGS_UPDATE_INVALID', + /* It indicates the update request has failed. */ NETWORKSETTINGS_UPDATE_FAILURE: 'NETWORK_SETTINGS_UPDATE_FAILURE', - NETWORKSETTINGS_UPDATE_ERRORS: 'NETWORK_SETTINGS_UPDATE_ERRORS', + /* It updates the redux state of network settings status */ + NETWORKSETTINGS_UPDATE_READY: 'NETWORK_SETTINGS_UPDATE_READY', }; export const featureToggleInitialized = () => ({ @@ -861,13 +872,13 @@ export const setWCConnectionFailed = (failed) => ({ * walletServiceWsUrl?: string * }} customNetworkRequest Request input */ -export const networkSettingsUpdate = (customNetworkRequest) => ({ - type: types.NETWORKSETTINGS_UPDATE, +export const networkSettingsUpdateRequest = (customNetworkRequest) => ({ + type: types.NETWORKSETTINGS_UPDATE_REQUEST, payload: customNetworkRequest, }); /** - * Emits the custom network settings to be stored and persisted. + * Emits the custom network settings to update the redux store. * @param {{ * stage: string, * network: string, @@ -878,22 +889,58 @@ export const networkSettingsUpdate = (customNetworkRequest) => ({ * walletServiceWsUrl?: string * }} customNetwork Settings to persist */ -export const networkSettingsUpdateSuccess = (customNetwork) => ({ - type: types.NETWORKSETTINGS_UPDATE_SUCCESS, +export const networkSettingsUpdateState = (customNetwork) => ({ + type: types.NETWORKSETTINGS_UPDATE_STATE, + payload: customNetwork, +}); + +/** + * Emits the custom network settings to persist in the app storage and update the redux store. + * @param {{ + * stage: string, + * network: string, + * nodeUrl: string, + * explorerUrl: string, + * explorerServiceUrl: string, + * walletServiceUrl?: string + * walletServiceWsUrl?: string + * }} customNetwork Settings to persist + */ +export const networkSettingsPersistStore = (customNetwork) => ({ + type: types.NETWORKSETTINGS_PERSIST_STORE, payload: customNetwork, }); /** - * Emits the failure signal for custom network settings request. + * Action indicating that the network settings update process + * is in a waiting state. + * This is used after persisting custom network configurations, + * resulting in a wallet reload. + */ +export const networkSettingsUpdateWaiting = () => ({ + type: types.NETWORKSETTINGS_UPDATE_WAITING, +}); + +/** + * Action indicating that the network settings update was successful. + * This serves as a hook for the frontend to provide feedback to the user. + */ +export const networkSettingsUpdateSuccess = () => ({ + type: types.NETWORKSETTINGS_UPDATE_SUCCESS, +}); + +/** + * Action indicating a failure state for the custom network settings request. * It means the request couldn't be processed due to internal error. + * This serves as a hook for the frontend to provide feedback to the user. */ export const networkSettingsUpdateFailure = () => ({ type: types.NETWORKSETTINGS_UPDATE_FAILURE, }); /** - * Emits errors signal for custom network settings form representing - * invalid inputs. + * Action indicating an invalid state for the custom network settings request inputs. + * It means the form should present the invalid message on the corresponding inputs. * @param {{ * message: string, * nodeUrl: string, @@ -903,8 +950,8 @@ export const networkSettingsUpdateFailure = () => ({ * walletServiceWsUrl?: string * }} errors The validation errors from custom network settings form */ -export const networkSettingsUpdateErrors = (errors) => ({ - type: types.NETWORKSETTINGS_UPDATE_ERRORS, +export const networkSettingsUpdateInvalid = (errors) => ({ + type: types.NETWORKSETTINGS_UPDATE_INVALID, payload: errors, }); diff --git a/src/components/NetworkSettings/NetworkStatusBar.js b/src/components/NetworkSettings/NetworkStatusBar.js index 64717833c..05329248b 100644 --- a/src/components/NetworkSettings/NetworkStatusBar.js +++ b/src/components/NetworkSettings/NetworkStatusBar.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ import { useSelector } from 'react-redux'; -import { eq } from 'lodash'; +import { isEqual } from 'lodash'; import { t } from 'ttag'; import { AlertUI } from '../../styles/themes'; import { ToplineBar } from '../ToplineBar'; @@ -13,18 +13,45 @@ import { PRE_SETTINGS_MAINNET } from '../../constants'; const customNetworkText = t`Custom network`; -export const NetworkStatusBar = () => { - const networkSettings = useSelector((state) => state.networkSettings); - if (eq(networkSettings, PRE_SETTINGS_MAINNET)) { - return null; +function notMainnet(networkSettings) { + // If the networkSettings has a walletServiceUrl, then + // we should run a full check against the mainnet presettings. + // This is important because the wallet service has precedence + // over fullnode. + if (networkSettings.walletServiceUrl) { + return !isEqual(networkSettings, PRE_SETTINGS_MAINNET); } - const style = { - backgroundColor: AlertUI.primaryColor, - color: AlertUI.dark40Color, + // In the absence of walletServiceUrl we can remove wallet + // service URLs from the equality check against the mainnet + // presettings. + const currNetwork = { + stage: networkSettings.stage, + network: networkSettings.network, + nodeUrl: networkSettings.nodeUrl, + explorerUrl: networkSettings.explorerUrl, + explorerServiceUrl: networkSettings.explorerServiceUrl, + }; + const mainnet = { + stage: PRE_SETTINGS_MAINNET.stage, + network: PRE_SETTINGS_MAINNET.network, + nodeUrl: PRE_SETTINGS_MAINNET.nodeUrl, + explorerUrl: PRE_SETTINGS_MAINNET.explorerUrl, + explorerServiceUrl: PRE_SETTINGS_MAINNET.explorerServiceUrl, }; - const text = `${customNetworkText}: ${networkSettings.network}`; - return ( - + return !isEqual(currNetwork, mainnet); +} + +const style = { + backgroundColor: AlertUI.primaryColor, + color: AlertUI.dark40Color, +}; + +export const NetworkStatusBar = () => { + const getStatusText = (networkSettings) => `${customNetworkText}: ${networkSettings.network}`; + const networkSettings = useSelector((state) => state.networkSettings); + + return notMainnet(networkSettings) && ( + ); }; diff --git a/src/constants.js b/src/constants.js index 6d50c7055..302eb4e6b 100644 --- a/src/constants.js +++ b/src/constants.js @@ -209,6 +209,8 @@ export const NETWORKSETTINGS_STATUS = { READY: 'ready', FAILED: 'failed', LOADING: 'loading', + WAITING: 'waiting', + SUCCESSFUL: 'successful', }; /** diff --git a/src/reducers/reducer.js b/src/reducers/reducer.js index 9dd21c39c..81211ec56 100644 --- a/src/reducers/reducer.js +++ b/src/reducers/reducer.js @@ -207,7 +207,7 @@ const initialState = { ...FEATURE_TOGGLE_DEFAULTS, }, networkSettings: PRE_SETTINGS_MAINNET, - networkSettingsErrors: {}, + networkSettingsInvalid: {}, networkSettingsStatus: NETWORKSETTINGS_STATUS.READY, }; @@ -327,6 +327,8 @@ export const reducer = (state = initialState, action) => { return onPushReset(state); case types.EXCEPTION_CAPTURED: return onExceptionCaptured(state, action); + case types.RELOAD_WALLET_REQUESTED: + return onReloadWalletRequested(state); case types.WALLET_RELOADING: return onWalletReloading(state); case types.SHARED_ADDRESS_UPDATE: @@ -345,16 +347,22 @@ export const reducer = (state = initialState, action) => { return onSetWalletConnectSessions(state, action); case types.WC_SET_CONNECTION_FAILED: return onSetWCConnectionFailed(state, action); - case types.NETWORKSETTINGS_UPDATE: - return onNetworkSettingsUpdate(state); + case types.NETWORKSETTINGS_UPDATE_REQUEST: + return onNetworkSettingsUpdateRequest(state); + case types.NETWORKSETTINGS_UPDATE_STATE: + return onNetworkSettingsUpdateState(state, action); + case types.NETWORKSETTINGS_PERSIST_STORE: + return onNetworkSettingsPersistStore(state, action); + case types.NETWORKSETTINGS_UPDATE_WAITING: + return onNetworkSettingsUpdateWaiting(state); case types.NETWORKSETTINGS_UPDATE_SUCCESS: - return onNetworkSettingsUpdateSucess(state, action); + return onNetworkSettingsUpdateSuccess(state); case types.NETWORKSETTINGS_UPDATE_READY: return onNetworkSettingsUpdateReady(state); case types.NETWORKSETTINGS_UPDATE_FAILURE: return onNetworkSettingsUpdateFailure(state); - case types.NETWORKSETTINGS_UPDATE_ERRORS: - return onNetworkSettingsUpdateErrors(state, action); + case types.NETWORKSETTINGS_UPDATE_INVALID: + return onNetworkSettingsUpdateInvalid(state, action); default: return state; } @@ -1026,6 +1034,16 @@ export const onExceptionCaptured = (state, { payload }) => { }; }; +/** + * On wallet reload, tokens data will be reloaded as well. + */ +export const onReloadWalletRequested = (state) => ({ + ...state, + tokensHistory: initialState.tokensHistory, + tokensBalance: initialState.tokensBalance, + loadHistoryStatus: initialState.loadHistoryStatus, +}); + const onWalletReloading = (state) => ({ ...state, walletStartState: WALLET_STATUS.LOADING, @@ -1086,21 +1104,46 @@ export const onSetWCConnectionFailed = (state, { payload }) => ({ * @param {Object} action.payload The network settings emitted in saga * @see updateNetworkSettings */ -export const onNetworkSettingsUpdate = (state) => ({ +export const onNetworkSettingsUpdateRequest = (state) => ({ ...state, networkSettingsStatus: NETWORKSETTINGS_STATUS.LOADING, }); /** * @param {Object} action.payload The network settings emitted in saga - * @see updateNetworkSettings + * @see networkSettingsUpdateState customNetwork + */ +export const onNetworkSettingsUpdateState = (state, { payload }) => ({ + ...state, + networkSettings: payload, +}); + +/** + * @param {Object} action.payload The network settings emitted in saga + * @see networkSettingsPersistStore customNetwork */ -export const onNetworkSettingsUpdateSucess = (state, { payload }) => ({ +export const onNetworkSettingsPersistStore = (state, { payload }) => ({ ...state, networkSettings: payload, networkSettingsStatus: NETWORKSETTINGS_STATUS.LOADING, }); +/** + * Set `WAITING` state on network settings status. + */ +export const onNetworkSettingsUpdateWaiting = (state) => ({ + ...state, + networkSettingsStatus: NETWORKSETTINGS_STATUS.WAITING, +}); + +/** + * Set `SUCCESSFUL` state on network settings status. + */ +export const onNetworkSettingsUpdateSuccess = (state) => ({ + ...state, + networkSettingsStatus: NETWORKSETTINGS_STATUS.SUCCESSFUL, +}); + /** * @param {Object} action.payload The errors from network settings input validation * @see updateNetworkSettings @@ -1111,16 +1154,19 @@ export const onNetworkSettingsUpdateReady = (state) => ({ }); /** - * @param {Object} action.payload The errors from network settings input validation - * @see updateNetworkSettings + * Set `FAILED` state on network settings status. */ export const onNetworkSettingsUpdateFailure = (state) => ({ ...state, networkSettingsStatus: NETWORKSETTINGS_STATUS.FAILED }); -export const onNetworkSettingsUpdateErrors = (state, { payload }) => ({ +/** + * @param {Object} action.payload The errors from network settings input validation + * @see networkSettingsUpdateInvalid errors + */ +export const onNetworkSettingsUpdateInvalid = (state, { payload }) => ({ ...state, - networkSettingsErrors: payload, + networkSettingsInvalid: payload, networkSettingsStatus: NETWORKSETTINGS_STATUS.READY, }); diff --git a/src/sagas/networkSettings.js b/src/sagas/networkSettings.js index 9521e0597..2fead2b7d 100644 --- a/src/sagas/networkSettings.js +++ b/src/sagas/networkSettings.js @@ -1,22 +1,53 @@ -import { all, takeEvery, put, call, race, take, delay, select } from 'redux-saga/effects'; +import { all, takeEvery, put, call, race, delay, select } from 'redux-saga/effects'; import { config } from '@hathor/wallet-lib'; import { isEmpty } from 'lodash'; -import AsyncStorage from '@react-native-async-storage/async-storage'; import { t } from 'ttag'; -import { featureToggleUpdate, networkSettingsUpdateErrors, networkSettingsUpdateFailure, networkSettingsUpdateReady, networkSettingsUpdateSuccess, reloadWalletRequested, types } from '../actions'; -import { HTTP_REQUEST_TIMEOUT, NETWORK, networkSettingsKeyMap, NETWORK_TESTNET, STAGE, STAGE_DEV_PRIVNET, STAGE_TESTNET, WALLET_SERVICE_REQUEST_TIMEOUT } from '../constants'; -import { getFullnodeNetwork, getWalletServiceNetwork } from './helpers'; +import { + networkSettingsPersistStore, + networkSettingsUpdateInvalid, + networkSettingsUpdateFailure, + networkSettingsUpdateState, + networkSettingsUpdateSuccess, + networkSettingsUpdateWaiting, + types, + reloadWalletRequested, + onExceptionCaptured, + networkSettingsUpdateReady +} from '../actions'; +import { + NETWORK, + networkSettingsKeyMap, + NETWORKSETTINGS_STATUS, + NETWORK_TESTNET, + STAGE, + STAGE_DEV_PRIVNET, + STAGE_TESTNET, + WALLET_SERVICE_REQUEST_TIMEOUT +} from '../constants'; +import { + getFullnodeNetwork, + getWalletServiceNetwork, +} from './helpers'; import { STORE } from '../store'; /** - * Initialize network settings saga. - * - * It looks up a stored network settings to update the redux state. + * Initialize the network settings saga when the wallet starts successfully. */ export function* initNetworkSettings() { const customNetwork = STORE.getItem(networkSettingsKeyMap.networkSettings); if (customNetwork) { - yield put(networkSettingsUpdateSuccess(customNetwork)); + yield put(networkSettingsUpdateState(customNetwork)); + } + + const status = yield select((state) => state.networkSettingsStatus); + if (status === NETWORKSETTINGS_STATUS.WAITING) { + // This branch completes the network update by delivering + // a success feedback to the user. + yield put(networkSettingsUpdateSuccess()); + } else { + // This branch is a fallback to set network status to READY + // after wallet initialization. + yield put(networkSettingsUpdateReady()); } } @@ -47,53 +78,50 @@ export function* updateNetworkSettings(action) { walletServiceWsUrl, } = action.payload || {}; - const errors = {}; + const invalidPayload = {}; // validates input emptyness if (isEmpty(action.payload)) { - errors.message = t`Custom Network Settings cannot be empty.`; + invalidPayload.message = t`Custom Network Settings cannot be empty.`; } // validates explorerUrl // - required // - should have a valid URL if (isEmpty(explorerUrl) || invalidUrl(explorerUrl)) { - errors.explorerUrl = t`explorerUrl should be a valid URL.`; + invalidPayload.explorerUrl = t`explorerUrl should be a valid URL.`; } // validates explorerServiceUrl // - required // - should have a valid URL if (isEmpty(explorerServiceUrl) || invalidUrl(explorerServiceUrl)) { - errors.explorerServiceUrl = t`explorerServiceUrl should be a valid URL.`; + invalidPayload.explorerServiceUrl = t`explorerServiceUrl should be a valid URL.`; } // validates nodeUrl // - required // - should have a valid URl if (isEmpty(nodeUrl) || invalidUrl(nodeUrl)) { - errors.nodeUrl = t`nodeUrl should be a valid URL.`; + invalidPayload.nodeUrl = t`nodeUrl should be a valid URL.`; } // validates walletServiceUrl // - optional // - should have a valid URL, if given if (walletServiceUrl && invalidUrl(walletServiceUrl)) { - errors.walletServiceUrl = t`walletServiceUrl should be a valid URL.`; + invalidPayload.walletServiceUrl = t`walletServiceUrl should be a valid URL.`; } // validates walletServiceWsUrl // - conditionally required // - should have a valid URL, if walletServiceUrl is given if (walletServiceUrl && invalidUrl(walletServiceWsUrl)) { - errors.walletServiceWsUrl = t`walletServiceWsUrl should be a valid URL.`; + invalidPayload.walletServiceWsUrl = t`walletServiceWsUrl should be a valid URL.`; } - // TODO: Refactor by segregating Failure from Errors - // - create networkSettingsUpdateErrors - // - implement reaction to networkSettingsUpdateFailure - yield put(networkSettingsUpdateErrors(errors)); - if (Object.keys(errors).length > 0) { + yield put(networkSettingsUpdateInvalid(invalidPayload)); + if (Object.keys(invalidPayload).length > 0) { return; } @@ -138,7 +166,6 @@ export function* updateNetworkSettings(action) { try { network = yield call(getFullnodeNetwork); } catch (err) { - // NOTE: Keep the console? console.error('Error calling the fullnode while trying to get network details in updateNetworkSettings effect..', err); rollbackConfigUrls(backupUrl); yield put(networkSettingsUpdateFailure()); @@ -148,6 +175,7 @@ export function* updateNetworkSettings(action) { // Fail after try get network from fullnode if (!network) { + console.warn('The network could not be found.'); yield put(networkSettingsUpdateFailure()); return; } @@ -171,7 +199,7 @@ export function* updateNetworkSettings(action) { walletServiceWsUrl, }; - yield put(networkSettingsUpdateSuccess(customNetwork)); + yield put(networkSettingsPersistStore(customNetwork)); } /** @@ -217,31 +245,42 @@ function invalidUrl(tryUrl) { export function* persistNetworkSettings(action) { // persists after reducer being updated const networkSettings = action.payload; - const strNetworkSettings = JSON.stringify(networkSettings); - yield call(AsyncStorage.setItem, networkSettingsKeyMap.networkSettings, strNetworkSettings); - - // trigger toggle update to be managed by featureToggle saga - yield put(featureToggleUpdate()); - - // if wallet-service is being deactivated, it will trigger the reload, - // otherwise we should trigger by ourselves - const { timeout } = yield race({ - reload: take(types.RELOAD_WALLET_REQUESTED), - timeout: delay(HTTP_REQUEST_TIMEOUT), - }); + try { + STORE.setItem(networkSettingsKeyMap.networkSettings, networkSettings); + yield put(networkSettingsUpdateWaiting()); + } catch (err) { + console.error('Error while persisting the custom network settings.', err); + yield put(networkSettingsUpdateFailure()); + return; + } - if (timeout) { - yield put(reloadWalletRequested()); + const wallet = yield select((state) => state.wallet); + if (!wallet) { + // If we fall into this situation, the app should be killed + // for the custom new network settings take effect. + const errMsg = t`Wallet not found while trying to persist the custom network settings.`; + console.warn(errMsg); + yield put(onExceptionCaptured(errMsg, /* isFatal */ true)); + return; } - yield put(networkSettingsUpdateReady()); + // Stop wallet and clean its storage without clean its access data. + wallet.stop({ cleanStorage: true, cleanAddresses: true }); + // This action should clean the tokens history on redux. + // In addition, the reload also clean the inmemory storage. + yield put(reloadWalletRequested()); } /** * Deletes the network settings from the application storage. */ export function* cleanNetworkSettings() { - STORE.removeItem(networkSettingsKeyMap.networkSettings); + try { + STORE.removeItem(networkSettingsKeyMap.networkSettings); + } catch (err) { + console.error('Error while deleting the custom network settings from app storage.', err); + yield 1; + } yield 0; } @@ -251,8 +290,8 @@ export function* cleanNetworkSettings() { export function* saga() { yield all([ takeEvery(types.START_WALLET_SUCCESS, initNetworkSettings), - takeEvery(types.NETWORKSETTINGS_UPDATE, updateNetworkSettings), - takeEvery(types.NETWORKSETTINGS_UPDATE_SUCCESS, persistNetworkSettings), + takeEvery(types.NETWORKSETTINGS_UPDATE_REQUEST, updateNetworkSettings), + takeEvery(types.NETWORKSETTINGS_PERSIST_STORE, persistNetworkSettings), takeEvery(types.RESET_WALLET, cleanNetworkSettings), ]); } diff --git a/src/screens/NetworkSettings/CustomNetworkSettingsScreen.js b/src/screens/NetworkSettings/CustomNetworkSettingsScreen.js index ef7f810ef..e0f59ec9d 100644 --- a/src/screens/NetworkSettings/CustomNetworkSettingsScreen.js +++ b/src/screens/NetworkSettings/CustomNetworkSettingsScreen.js @@ -3,65 +3,51 @@ import { View, Text, StyleSheet, Image } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; import { t } from 'ttag'; import { isEmpty } from 'lodash'; -import { networkSettingsUpdate, networkSettingsUpdateErrors, networkSettingsUpdateReady } from '../../actions'; +import { networkSettingsUpdateRequest, networkSettingsUpdateInvalid, networkSettingsUpdateReady } from '../../actions'; import FeedbackModal from '../../components/FeedbackModal'; import HathorHeader from '../../components/HathorHeader'; import NewHathorButton from '../../components/NewHathorButton'; import SimpleInput from '../../components/SimpleInput'; -import { NETWORKSETTINGS_STATUS } from '../../constants'; import errorIcon from '../../assets/images/icErrorBig.png'; +import checkIcon from '../../assets/images/icCheckBig.png'; import Spinner from '../../components/Spinner'; +import { hasSucceed, hasFailed, isLoading } from './helper'; const customNetworkSettingsTitleText = t`Custom Network Settings`.toUpperCase(); const warningText = t`Any token outside mainnet network bear no value. Only change if you know what you are doing.`; const feedbackLoadingText = t`Updating custom network settings...`; +const feedbackSucceedText = t`Network settings successfully customized.`; const feedbackFailedText = t`There was an error while customizing network settings. Please try again later.`; /** - * Check if the network settings status is failed. - * @param {object} networkSettingsStatus - status from redux store - * @returns {boolean} - true if the status is failed, false otherwise + * Verifies if the invalidModel of the form has an error message. */ -// eslint-disable-next-line max-len -const hasFailed = (networkSettingsStatus) => networkSettingsStatus === NETWORKSETTINGS_STATUS.FAILED; - -/** - * Check if the network settings status is loading. - * @param {object} networkSettingsStatus - status from redux store - * @returns {boolean} - true if the status is loading, false otherwise - */ -// eslint-disable-next-line max-len -const isLoading = (networkSettingsStatus) => networkSettingsStatus === NETWORKSETTINGS_STATUS.LOADING; - -/** - * Verifies if the errorModel of the form has an error message. - */ -function hasError(errorModel) { +function hasError(invalidModel) { return Object - .values({ ...errorModel }) + .values({ ...invalidModel }) .reduce((_hasError, currValue) => _hasError || !isEmpty(currValue), false); } /** - * Validates the formModel, returning the errorModel. - * If there is no error in the formModel, the errorModel is returned empty. + * Validates the formModel, returning the invalidModel. + * If there is no error in the formModel, the invalidModel is returned empty. */ function validate(formModel) { - const errorModel = {}; + const invalidModel = {}; if (!formModel.nodeUrl) { - errorModel.nodeUrl = t`nodeUrl is required.`; + invalidModel.nodeUrl = t`nodeUrl is required.`; } if (!formModel.explorerUrl) { - errorModel.explorerUrl = t`explorerUrl is required.`; + invalidModel.explorerUrl = t`explorerUrl is required.`; } if (!formModel.explorerServiceUrl) { - errorModel.explorerServiceUrl = t`explorerServiceUrl is required.`; + invalidModel.explorerServiceUrl = t`explorerServiceUrl is required.`; } - return errorModel; + return invalidModel; } const styles = StyleSheet.create({ @@ -103,7 +89,7 @@ export const CustomNetworkSettingsNav = Symbol('CustomNetworkSettings').toString export const CustomNetworkSettingsScreen = ({ navigation }) => { const dispatch = useDispatch(); const networkSettings = useSelector((state) => state.networkSettings); - const networkSettingsErrors = useSelector((state) => state.networkSettingsErrors); + const networkSettingsInvalid = useSelector((state) => state.networkSettingsInvalid); const networkSettingsStatus = useSelector((state) => state.networkSettingsStatus); const [formModel, setFormModel] = useState({ @@ -114,21 +100,21 @@ export const CustomNetworkSettingsScreen = ({ navigation }) => { walletServiceWsUrl: networkSettings.walletServiceWsUrl || '', }); - const [errorModel, setErrorModel] = useState({ - nodeUrl: networkSettingsErrors?.nodeUrl || '', - explorerUrl: networkSettingsErrors?.explorerUrl || '', - explorerServiceUrl: networkSettingsErrors?.explorerServiceUrl || '', - walletServiceUrl: networkSettingsErrors?.walletServiceUrl || '', - walletServiceWsUrl: networkSettingsErrors?.walletServiceWsUrl || '', + const [invalidModel, setInvalidModel] = useState({ + nodeUrl: networkSettingsInvalid?.nodeUrl || '', + explorerUrl: networkSettingsInvalid?.explorerUrl || '', + explorerServiceUrl: networkSettingsInvalid?.explorerServiceUrl || '', + walletServiceUrl: networkSettingsInvalid?.walletServiceUrl || '', + walletServiceWsUrl: networkSettingsInvalid?.walletServiceWsUrl || '', }); // eslint-disable-next-line max-len /* @param {'nodeUrl' | 'explorerUrl' | 'explorerServiceUrl' | 'walletServiceUrl' | 'walletServiceWsUrl' } name */ const handleInputChange = (name) => (value) => { - // update error model - const errors = { ...errorModel }; - delete errors[name]; - setErrorModel(errors); + // update invalid model + const invalidModelCopy = { ...invalidModel }; + delete invalidModelCopy[name]; + setInvalidModel(invalidModelCopy); // update form model const form = { @@ -137,8 +123,8 @@ export const CustomNetworkSettingsScreen = ({ navigation }) => { }; setFormModel(form); - // validate form model and update error model - setErrorModel(validate(form)); + // validate form model and update invalid model + setInvalidModel(validate(form)); }; const handleFeedbackModalDismiss = () => { @@ -146,27 +132,27 @@ export const CustomNetworkSettingsScreen = ({ navigation }) => { }; const handleSubmit = () => { - const errors = validate(formModel); - if (hasError(errors)) { - setErrorModel(errors); + const newInvalidModel = validate(formModel); + if (hasError(newInvalidModel)) { + setInvalidModel(newInvalidModel); return; } - dispatch(networkSettingsUpdate(formModel)); + dispatch(networkSettingsUpdateRequest(formModel)); }; useEffect(() => { - setErrorModel({ - nodeUrl: networkSettingsErrors?.nodeUrl || '', - explorerUrl: networkSettingsErrors?.explorerUrl || '', - explorerServiceUrl: networkSettingsErrors?.explorerServiceUrl || '', - walletServiceUrl: networkSettingsErrors?.walletServiceUrl || '', - walletServiceWsUrl: networkSettingsErrors?.walletServiceWsUrl || '', + setInvalidModel({ + nodeUrl: networkSettingsInvalid?.nodeUrl || '', + explorerUrl: networkSettingsInvalid?.explorerUrl || '', + explorerServiceUrl: networkSettingsInvalid?.explorerServiceUrl || '', + walletServiceUrl: networkSettingsInvalid?.walletServiceUrl || '', + walletServiceWsUrl: networkSettingsInvalid?.walletServiceWsUrl || '', }); - }, [networkSettingsErrors]); + }, [networkSettingsInvalid]); useEffect(() => function cleanUp() { - dispatch(networkSettingsUpdateErrors({})); + dispatch(networkSettingsUpdateInvalid({})); }, []); return ( @@ -183,6 +169,14 @@ export const CustomNetworkSettingsScreen = ({ navigation }) => { /> )} + {hasSucceed(networkSettingsStatus) && ( + )} + text={feedbackSucceedText} + onDismiss={handleFeedbackModalDismiss} + /> + )} + {hasFailed(networkSettingsStatus) && ( )} @@ -200,7 +194,7 @@ export const CustomNetworkSettingsScreen = ({ navigation }) => { label={t`Node URL`} autoFocus onChangeText={handleInputChange('nodeUrl')} - error={errorModel.nodeUrl} + error={invalidModel.nodeUrl} value={formModel.nodeUrl} /> @@ -209,7 +203,7 @@ export const CustomNetworkSettingsScreen = ({ navigation }) => { label={t`Explorer URL`} autoFocus onChangeText={handleInputChange('explorerUrl')} - error={errorModel.explorerUrl} + error={invalidModel.explorerUrl} value={formModel.explorerUrl} /> @@ -218,7 +212,7 @@ export const CustomNetworkSettingsScreen = ({ navigation }) => { label={t`Explorer Service URL`} autoFocus onChangeText={handleInputChange('explorerServiceUrl')} - error={errorModel.explorerServiceUrl} + error={invalidModel.explorerServiceUrl} value={formModel.explorerServiceUrl} /> @@ -227,7 +221,7 @@ export const CustomNetworkSettingsScreen = ({ navigation }) => { label={t`Wallet Service URL (optional)`} autoFocus onChangeText={handleInputChange('walletServiceUrl')} - error={errorModel.walletServiceUrl} + error={invalidModel.walletServiceUrl} value={formModel.walletServiceUrl} /> @@ -236,13 +230,13 @@ export const CustomNetworkSettingsScreen = ({ navigation }) => { label={t`Wallet Service WS URL (optional)`} autoFocus onChangeText={handleInputChange('walletServiceWsUrl')} - error={errorModel.walletServiceWsUrl} + error={invalidModel.walletServiceWsUrl} value={formModel.walletServiceWsUrl} /> diff --git a/src/screens/NetworkSettings/NetworkPreSettingsScreen.js b/src/screens/NetworkSettings/NetworkPreSettingsScreen.js index df7f1df63..f87064a22 100644 --- a/src/screens/NetworkSettings/NetworkPreSettingsScreen.js +++ b/src/screens/NetworkSettings/NetworkPreSettingsScreen.js @@ -18,13 +18,15 @@ import HathorHeader from '../../components/HathorHeader'; import NewHathorButton from '../../components/NewHathorButton'; import Spinner from '../../components/Spinner'; import FeedbackModal from '../../components/FeedbackModal'; -import { networkSettingsUpdateReady, networkSettingsUpdateSuccess } from '../../actions'; +import { networkSettingsPersistStore, networkSettingsUpdateReady } from '../../actions'; import { PRE_SETTINGS_MAINNET, PRE_SETTINGS_TESTNET } from '../../constants'; import { CustomNetworkSettingsNav } from './CustomNetworkSettingsScreen'; -import { feedbackFailedText, feedbackLoadingText, hasFailed, isLoading } from './helper'; +import { feedbackSucceedText, feedbackFailedText, feedbackLoadingText, hasFailed, isLoading, hasSucceed } from './helper'; import errorIcon from '../../assets/images/icErrorBig.png'; +import checkIcon from '../../assets/images/icCheckBig.png'; const presettingsTitleText = t`Network Pre-Settings`.toUpperCase(); + const styles = StyleSheet.create({ container: { flex: 1, @@ -76,8 +78,8 @@ export const NetworkPreSettingsNav = Symbol('NetworkPreSettings').toString(); export function NetworkPreSettingsScreen({ navigation }) { const dispatch = useDispatch(); const networkSettingsStatus = useSelector((state) => state.networkSettingsStatus); - const setMainnetNetwork = () => dispatch(networkSettingsUpdateSuccess(PRE_SETTINGS_MAINNET)); - const setTestnetNetwork = () => dispatch(networkSettingsUpdateSuccess(PRE_SETTINGS_TESTNET)); + const setMainnetNetwork = () => dispatch(networkSettingsPersistStore(PRE_SETTINGS_MAINNET)); + const setTestnetNetwork = () => dispatch(networkSettingsPersistStore(PRE_SETTINGS_TESTNET)); const setCustomNetwork = () => { navigation.push(CustomNetworkSettingsNav); }; @@ -100,6 +102,14 @@ export function NetworkPreSettingsScreen({ navigation }) { /> )} + {hasSucceed(networkSettingsStatus) && ( + )} + text={feedbackSucceedText} + onDismiss={handleFeedbackModalDismiss} + /> + )} + {hasFailed(networkSettingsStatus) && ( )} diff --git a/src/screens/NetworkSettings/NetworkSettingsFlowStack.js b/src/screens/NetworkSettings/NetworkSettingsFlowStack.js index ac7589054..61c5c44c9 100644 --- a/src/screens/NetworkSettings/NetworkSettingsFlowStack.js +++ b/src/screens/NetworkSettings/NetworkSettingsFlowStack.js @@ -24,8 +24,14 @@ export const NetworkSettingsFlowStack = () => { name={NetworkSettingsDisclaimerNav} component={NetworkSettingsDisclaimerScreen} /> - - + + ); }; diff --git a/src/screens/NetworkSettings/helper.js b/src/screens/NetworkSettings/helper.js index 229d386dd..11b7ffd45 100644 --- a/src/screens/NetworkSettings/helper.js +++ b/src/screens/NetworkSettings/helper.js @@ -2,8 +2,18 @@ import { t } from 'ttag'; import { NETWORKSETTINGS_STATUS } from '../../constants'; export const feedbackLoadingText = t`Updating custom network settings...`; +export const feedbackSucceedText = t`Network settings successfully customized.`; export const feedbackFailedText = t`There was an error while customizing network settings. Please try again later.`; +/** + * Check if the network settings status is successful. + * @param {object} networkSettingsStatus - status from redux store + * @returns {boolean} - true if the status is successful, false otherwise + */ +export function hasSucceed(networkSettingsStatus) { + return networkSettingsStatus === NETWORKSETTINGS_STATUS.SUCCESSFUL; +} + /** * Check if the network settings status is failed. * @param {object} networkSettingsStatus - status from redux store