diff --git a/src/libs/API.js b/src/libs/API.js index f7d5678d3043..f0cce0e94fc2 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -4,15 +4,13 @@ import Onyx from 'react-native-onyx'; import CONST from '../CONST'; import CONFIG from '../CONFIG'; import ONYXKEYS from '../ONYXKEYS'; -// eslint-disable-next-line import/no-cycle import redirectToSignIn from './actions/SignInRedirect'; import isViaExpensifyCashNative from './isViaExpensifyCashNative'; -// eslint-disable-next-line import/no-cycle +import requireParameters from './requireParameters'; +import Log from './Log'; import * as Network from './Network'; -// eslint-disable-next-line import/no-cycle -import LogUtil from './Log'; -// eslint-disable-next-line import/no-cycle -import * as Session from './actions/Session'; +import updateSessionAuthTokens from './actions/Session/updateSessionAuthTokens'; +import setSessionLoadingAndError from './actions/Session/setSessionLoadingAndError'; let isAuthenticating; let credentials; @@ -89,35 +87,6 @@ function addDefaultValuesToParameters(command, parameters) { // Tie into the network layer to add auth token to the parameters of all requests Network.registerParameterEnhancer(addDefaultValuesToParameters); -/** - * @throws {Error} If the "parameters" object has a null or undefined value for any of the given parameterNames - * - * @param {String[]} parameterNames Array of the required parameter names - * @param {Object} parameters A map from available parameter names to their values - * @param {String} commandName The name of the API command - */ -function requireParameters(parameterNames, parameters, commandName) { - parameterNames.forEach((parameterName) => { - if (_(parameters).has(parameterName) - && parameters[parameterName] !== null - && parameters[parameterName] !== undefined - ) { - return; - } - - const propertiesToRedact = ['authToken', 'password', 'partnerUserSecret', 'twoFactorAuthCode']; - const parametersCopy = _.chain(parameters) - .clone() - .mapObject((val, key) => (_.contains(propertiesToRedact, key) ? '' : val)) - .value(); - const keys = _(parametersCopy).keys().join(', ') || 'none'; - - let error = `Parameter ${parameterName} is required for "${commandName}". `; - error += `Supplied parameters: ${keys}`; - throw new Error(error); - }); -} - /** * Function used to handle expired auth tokens. It re-authenticates with the API and * then replays the original request @@ -153,7 +122,34 @@ function handleExpiredAuthToken(originalCommand, originalParameters, originalTyp )); } +Network.registerRequestHandler((queuedRequest, finalParameters) => { + if (queuedRequest.command === 'Log') { + return; + } + + Log.info('Making API request', false, { + command: queuedRequest.command, + type: queuedRequest.type, + shouldUseSecure: queuedRequest.type, + rvl: finalParameters.returnValueList, + }); +}); + +Network.registerRequestSkippedHandler((parameters) => { + Log.hmmm('Trying to make a request when Network is not ready', parameters); +}); + Network.registerResponseHandler((queuedRequest, response) => { + if (queuedRequest.command !== 'Log') { + Log.info('Finished API request', false, { + command: queuedRequest.command, + type: queuedRequest.type, + shouldUseSecure: queuedRequest.shouldUseSecure, + jsonCode: response.jsonCode, + requestID: response.requestID, + }); + } + if (response.jsonCode === 407) { // Credentials haven't been initialized. We will not be able to re-authenticates with the API const unableToReauthenticate = (!credentials || !credentials.autoGeneratedLogin @@ -186,13 +182,13 @@ Network.registerResponseHandler((queuedRequest, response) => { Network.registerErrorHandler((queuedRequest, error) => { if (queuedRequest.command !== 'Log') { - LogUtil.hmmm('[API] Handled error when making request', error); + Log.hmmm('[API] Handled error when making request', error); } else { console.debug('[API] There was an error in the Log API command, unable to log to server!', error); } // Set an error state and signify we are done loading - Session.setSessionLoadingAndError(false, 'Cannot connect to server'); + setSessionLoadingAndError(false, 'Cannot connect to server'); // Reject the queued request with an API offline error so that the original caller can handle it. queuedRequest.reject(new Error(CONST.ERROR.API_OFFLINE)); @@ -293,7 +289,7 @@ function reauthenticate(command = '') { // Update authToken in Onyx and in our local variables so that API requests will use the // new authToken - Session.updateSessionAuthTokens(response.authToken, response.encryptedAuthToken); + updateSessionAuthTokens(response.authToken, response.encryptedAuthToken); authToken = response.authToken; // The authentication process is finished so the network can be unpaused to continue @@ -317,7 +313,7 @@ function reauthenticate(command = '') { // If we experience something other than a network error then redirect the user to sign in redirectToSignIn(error.message); - LogUtil.hmmm('Redirecting to Sign In because we failed to reauthenticate', { + Log.hmmm('Redirecting to Sign In because we failed to reauthenticate', { command, error: error.message, }); @@ -516,21 +512,6 @@ function GetRequestCountryCode() { return Network.post(commandName); } -/** - * @param {Object} parameters - * @param {String} parameters.expensifyCashAppVersion - * @param {Object[]} parameters.logPacket - * @returns {Promise} - */ -function Log(parameters) { - const commandName = 'Log'; - requireParameters(['logPacket', 'expensifyCashAppVersion'], - parameters, commandName); - - // Note: We are forcing Log to run since it requires no authToken and should only be queued when we are offline. - return Network.post(commandName, {...parameters, forceNetworkRequest: true}); -} - /** * @param {Object} parameters * @param {String} parameters.name @@ -1141,7 +1122,6 @@ export { GetRequestCountryCode, Graphite_Timer, Inbox_CallUser, - Log, PayIOU, PayWithWallet, PersonalDetails_GetForEmails, diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index 531146c41236..f9a33ea4be05 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -4,9 +4,6 @@ import CONFIG from '../CONFIG'; import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; -// To avoid a circular dependency, we can't include Log here, so instead, we define an empty logging method and expose the setLogger method to set the logger from outside this file -let info = () => {}; - let shouldUseSecureStaging = false; Onyx.connect({ key: ONYXKEYS.USER, @@ -39,14 +36,6 @@ function processHTTPRequest(url, method = 'get', body = null) { * @returns {Promise} */ function xhr(command, data, type = CONST.NETWORK.METHOD.POST, shouldUseSecure = false) { - if (command !== 'Log') { - info('Making API request', false, { - command, - type, - shouldUseSecure, - rvl: data.returnValueList, - }); - } const formData = new FormData(); _.each(data, (val, key) => formData.append(key, val)); let apiRoot = shouldUseSecure ? CONFIG.EXPENSIFY.URL_EXPENSIFY_SECURE : CONFIG.EXPENSIFY.URL_API_ROOT; @@ -55,19 +44,7 @@ function xhr(command, data, type = CONST.NETWORK.METHOD.POST, shouldUseSecure = apiRoot = CONST.STAGING_SECURE_URL; } - return processHTTPRequest(`${apiRoot}api?command=${command}`, type, formData) - .then((response) => { - if (command !== 'Log') { - info('Finished API request', false, { - command, - type, - shouldUseSecure, - jsonCode: response.jsonCode, - requestID: response.requestID, - }); - } - return response; - }); + return processHTTPRequest(`${apiRoot}api?command=${command}`, type, formData); } /** @@ -87,12 +64,7 @@ function download(relativePath) { return processHTTPRequest(`${siteRoot}${strippedRelativePath}`); } -function setLogger(logger) { - info = logger.info; -} - export default { - setLogger, download, xhr, }; diff --git a/src/libs/Log.js b/src/libs/Log.js index fe48c6ef7f61..da7fb2445b28 100644 --- a/src/libs/Log.js +++ b/src/libs/Log.js @@ -5,14 +5,26 @@ import Logger from 'expensify-common/lib/Logger'; import CONFIG from '../CONFIG'; import getPlatform from './getPlatform'; import {version} from '../../package.json'; -import NetworkConnection from './NetworkConnection'; -import HttpUtils from './HttpUtils'; - -// eslint-disable-next-line import/no-cycle -import * as API from './API'; +import requireParameters from './requireParameters'; +import * as Network from './Network'; let timeout = null; +/** + * @param {Object} parameters + * @param {String} parameters.expensifyCashAppVersion + * @param {Object[]} parameters.logPacket + * @returns {Promise} + */ +function LogCommand(parameters) { + const commandName = 'Log'; + requireParameters(['logPacket', 'expensifyCashAppVersion'], + parameters, commandName); + + // Note: We are forcing Log to run since it requires no authToken and should only be queued when we are offline. + return Network.post(commandName, {...parameters, forceNetworkRequest: true}); +} + /** * Network interface for logger. * @@ -31,13 +43,11 @@ function serverLoggingCallback(logger, params) { } clearTimeout(timeout); timeout = setTimeout(() => logger.info('Flushing logs older than 10 minutes', true, {}, true), 10 * 60 * 1000); - return API.Log(requestParams); + return LogCommand(requestParams); } -// Note: We are importing Logger from expensify-common because it is -// used by other platforms. The server and client logging -// callback methods are passed in here so we can decouple -// the logging library from the logging methods. +// Note: We are importing Logger from expensify-common because it is used by other platforms. The server and client logging +// callback methods are passed in here so we can decouple the logging library from the logging methods. const Log = new Logger({ serverLoggingCallback, clientLoggingCallback: (message) => { @@ -47,7 +57,4 @@ const Log = new Logger({ }); timeout = setTimeout(() => Log.info('Flushing logs older than 10 minutes', true, {}, true), 10 * 60 * 1000); -NetworkConnection.registerLogInfoCallback(Log.info); -HttpUtils.setLogger(Log); - export default Log; diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.js index 85bfb3fa2daa..344d448eef80 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.js @@ -9,7 +9,6 @@ import { } from '@react-navigation/native'; import PropTypes from 'prop-types'; import Onyx from 'react-native-onyx'; -// eslint-disable-next-line import/no-cycle import Log from '../Log'; import linkTo from './linkTo'; import ROUTES from '../../ROUTES'; diff --git a/src/libs/Network.js b/src/libs/Network.js index cc9e2b8e8781..d7d74b889bf6 100644 --- a/src/libs/Network.js +++ b/src/libs/Network.js @@ -5,8 +5,7 @@ import HttpUtils from './HttpUtils'; import ONYXKEYS from '../ONYXKEYS'; import * as ActiveClientManager from './ActiveClientManager'; import CONST from '../CONST'; -// eslint-disable-next-line import/no-cycle -import LogUtil from './Log'; +import createCallback from './createCallback'; import * as NetworkRequestQueue from './actions/NetworkRequestQueue'; let isReady = false; @@ -20,10 +19,12 @@ let networkRequestQueue = []; // parameters such as authTokens or CSRF tokens, etc. let enhanceParameters; -// These handlers must be registered in order to process the response or network errors returned from the queue. -// The first argument passed will be the queuedRequest object and the second will be either the response or error. -let onResponse = () => {}; -let onError = () => {}; +// These handlers must be registered so we can process the request, response, and errors returned from the queue. +// The first argument passed will be the queuedRequest object and the second will be either the parameters, response, or error. +const [onRequest, registerRequestHandler] = createCallback(); +const [onResponse, registerResponseHandler] = createCallback(); +const [onError, registerErrorHandler] = createCallback(); +const [onRequestSkipped, registerRequestSkippedHandler] = createCallback(); let didLoadPersistedRequests; let isOffline; @@ -120,7 +121,7 @@ function setIsReady(val) { */ function canMakeRequest(request) { if (!isReady) { - LogUtil.hmmm('Trying to make a request when Network is not ready', {command: request.command, type: request.type}); + onRequestSkipped({command: request.command, type: request.type}); return false; } @@ -221,6 +222,7 @@ function processNetworkRequestQueue() { ? enhanceParameters(queuedRequest.command, requestData) : requestData; + onRequest(queuedRequest, finalParameters); HttpUtils.xhr(queuedRequest.command, finalParameters, queuedRequest.type, queuedRequest.shouldUseSecure) .then(response => onResponse(queuedRequest, response)) .catch(error => onError(queuedRequest, error)); @@ -323,23 +325,6 @@ function clearRequestQueue() { networkRequestQueue = []; } -/** - * Register a method to call when the authToken expires - * @param {Function} callback - */ -function registerResponseHandler(callback) { - onResponse = callback; -} - -/** - * The error handler will handle fetch() errors. Not used for successful responses that might send expected error codes - * e.g. jsonCode: 407. - * @param {Function} callback - */ -function registerErrorHandler(callback) { - onError = callback; -} - export { post, pauseRequestQueue, @@ -349,5 +334,7 @@ export { clearRequestQueue, registerResponseHandler, registerErrorHandler, + registerRequestHandler, setIsReady, + registerRequestSkippedHandler, }; diff --git a/src/libs/NetworkConnection.js b/src/libs/NetworkConnection.js index f8f3d7a0e768..e73a88fcb688 100644 --- a/src/libs/NetworkConnection.js +++ b/src/libs/NetworkConnection.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import NetInfo from './NetInfo'; import AppStateMonitor from './AppStateMonitor'; import promiseAllSettled from './promiseAllSettled'; +import Log from './Log'; import * as Network from './actions/Network'; // NetInfo.addEventListener() returns a function used to unsubscribe the @@ -9,7 +10,6 @@ import * as Network from './actions/Network'; let unsubscribeFromNetInfo; let unsubscribeFromAppState; let isOffline = false; -let logInfo = () => {}; // Holds all of the callbacks that need to be triggered when the network reconnects const reconnectionCallbacks = []; @@ -18,7 +18,7 @@ const reconnectionCallbacks = []; * Loop over all reconnection callbacks and fire each one */ const triggerReconnectionCallbacks = _.throttle((reason) => { - logInfo(`[NetworkConnection] Firing reconnection callbacks because ${reason}`); + Log.info(`[NetworkConnection] Firing reconnection callbacks because ${reason}`); Network.setIsLoadingAfterReconnect(true); promiseAllSettled(_.map(reconnectionCallbacks, callback => callback())) .then(() => Network.setIsLoadingAfterReconnect(false)); @@ -48,7 +48,7 @@ function setOfflineStatus(isCurrentlyOffline) { * `disconnected` event which takes about 10-15 seconds to emit. */ function listenForReconnect() { - logInfo('[NetworkConnection] listenForReconnect called'); + Log.info('[NetworkConnection] listenForReconnect called'); unsubscribeFromAppState = AppStateMonitor.addBecameActiveListener(() => { triggerReconnectionCallbacks('app became active'); @@ -57,7 +57,7 @@ function listenForReconnect() { // Subscribe to the state change event via NetInfo so we can update // whether a user has internet connectivity or not. unsubscribeFromNetInfo = NetInfo.addEventListener((state) => { - logInfo(`[NetworkConnection] NetInfo isConnected: ${state && state.isConnected}`); + Log.info(`[NetworkConnection] NetInfo isConnected: ${state && state.isConnected}`); setOfflineStatus(!state.isConnected); }); } @@ -66,7 +66,7 @@ function listenForReconnect() { * Tear down the event listeners when we are finished with them. */ function stopListeningForReconnect() { - logInfo('[NetworkConnection] stopListeningForReconnect called'); + Log.info('[NetworkConnection] stopListeningForReconnect called'); if (unsubscribeFromNetInfo) { unsubscribeFromNetInfo(); unsubscribeFromNetInfo = undefined; @@ -86,18 +86,10 @@ function onReconnect(callback) { reconnectionCallbacks.push(callback); } -/** - * @param {Function} callback - */ -function registerLogInfoCallback(callback) { - logInfo = callback; -} - export default { setOfflineStatus, listenForReconnect, stopListeningForReconnect, onReconnect, triggerReconnectionCallbacks, - registerLogInfoCallback, }; diff --git a/src/libs/Pusher/pusher.js b/src/libs/Pusher/pusher.js index b4f1a4fa1a42..88e3c8943400 100644 --- a/src/libs/Pusher/pusher.js +++ b/src/libs/Pusher/pusher.js @@ -1,7 +1,6 @@ import _ from 'underscore'; import Pusher from './library'; import TYPE from './EventType'; -// eslint-disable-next-line import/no-cycle import Log from '../Log'; let socket; diff --git a/src/libs/SignoutManager.js b/src/libs/SignoutManager.js index 9588a3f969b4..65c07e07c29a 100644 --- a/src/libs/SignoutManager.js +++ b/src/libs/SignoutManager.js @@ -1,9 +1,10 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../ONYXKEYS'; -// eslint-disable-next-line import/no-cycle -import * as Session from './actions/Session'; +import createCallback from './createCallback'; +import setShouldSignOut from './actions/Session/setShouldSignOut'; + +const [signoutCallback, registerSignoutCallback] = createCallback(); -let signoutCallback = () => {}; let errorMessage = ''; let shouldSignOut = false; Onyx.connect({ @@ -12,26 +13,19 @@ Onyx.connect({ if (!shouldSignOut && val) { signoutCallback(errorMessage); errorMessage = ''; - Session.setShouldSignOut(false); + setShouldSignOut(false); } shouldSignOut = val; }, }); -/** - * @param {Function} callback - */ -function registerSignoutCallback(callback) { - signoutCallback = callback; -} - /** * @param {String} message */ function signOut(message) { errorMessage = message; - Session.setShouldSignOut(true); + setShouldSignOut(true); } export default { diff --git a/src/libs/actions/NameValuePair.js b/src/libs/actions/NameValuePair.js index b62b217199e1..2b0740558311 100644 --- a/src/libs/actions/NameValuePair.js +++ b/src/libs/actions/NameValuePair.js @@ -1,7 +1,6 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; -// eslint-disable-next-line import/no-cycle import * as API from '../API'; /** diff --git a/src/libs/actions/Session.js b/src/libs/actions/Session/index.js similarity index 91% rename from src/libs/actions/Session.js rename to src/libs/actions/Session/index.js index b0d40358dd6a..0c16afb3b74a 100644 --- a/src/libs/actions/Session.js +++ b/src/libs/actions/Session/index.js @@ -1,26 +1,25 @@ -/* eslint-disable import/no-cycle */ import Onyx from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import _ from 'underscore'; import lodashGet from 'lodash/get'; -import ONYXKEYS from '../../ONYXKEYS'; -import redirectToSignIn from './SignInRedirect'; -import * as API from '../API'; -import CONFIG from '../../CONFIG'; -import Log from '../Log'; -import PushNotification from '../Notification/PushNotification'; -import Timing from './Timing'; -import CONST from '../../CONST'; -import Navigation from '../Navigation/Navigation'; -import ROUTES from '../../ROUTES'; -import {translateLocal} from '../translate'; -import * as Network from '../Network'; -import UnreadIndicatorUpdater from '../UnreadIndicatorUpdater'; -import Timers from '../Timers'; -import * as Pusher from '../Pusher/pusher'; -import NetworkConnection from '../NetworkConnection'; -import {getUserDetails} from './User'; -import {isNumericWithSpecialChars} from '../ValidationUtils'; +import ONYXKEYS from '../../../ONYXKEYS'; +import redirectToSignIn from '../SignInRedirect'; +import * as API from '../../API'; +import CONFIG from '../../../CONFIG'; +import Log from '../../Log'; +import PushNotification from '../../Notification/PushNotification'; +import Timing from '../Timing'; +import CONST from '../../../CONST'; +import Navigation from '../../Navigation/Navigation'; +import ROUTES from '../../../ROUTES'; +import {translateLocal} from '../../translate'; +import * as Network from '../../Network'; +import UnreadIndicatorUpdater from '../../UnreadIndicatorUpdater'; +import Timers from '../../Timers'; +import * as Pusher from '../../Pusher/pusher'; +import NetworkConnection from '../../NetworkConnection'; +import {getUserDetails} from '../User'; +import {isNumericWithSpecialChars} from '../../ValidationUtils'; let credentials = {}; Onyx.connect({ @@ -376,22 +375,6 @@ function clearAccountMessages() { Onyx.merge(ONYXKEYS.ACCOUNT, {error: '', success: ''}); } -/** - * @param {Boolean} loading - * @param {String} error - */ -function setSessionLoadingAndError(loading, error) { - Onyx.merge(ONYXKEYS.SESSION, {loading, error}); -} - -/** - * @param {String} authToken - * @param {String} encryptedAuthToken - */ -function updateSessionAuthTokens(authToken, encryptedAuthToken) { - Onyx.merge(ONYXKEYS.SESSION, {authToken, encryptedAuthToken}); -} - /** * @param {String} authToken * @param {String} password @@ -492,13 +475,6 @@ function authenticatePusher(socketID, channelName, callback) { }); } -/** - * @param {Boolean} shouldSignOut - */ -function setShouldSignOut(shouldSignOut) { - Onyx.set(ONYXKEYS.SHOULD_SIGN_OUT, shouldSignOut); -} - /** * @param {Boolean} shouldShowComposeInput */ @@ -519,11 +495,8 @@ export { clearSignInData, cleanupSession, clearAccountMessages, - setSessionLoadingAndError, - updateSessionAuthTokens, validateEmail, authenticatePusher, reauthenticatePusher, - setShouldSignOut, setShouldShowComposeInput, }; diff --git a/src/libs/actions/Session/setSessionLoadingAndError.js b/src/libs/actions/Session/setSessionLoadingAndError.js new file mode 100644 index 000000000000..e28b7613994c --- /dev/null +++ b/src/libs/actions/Session/setSessionLoadingAndError.js @@ -0,0 +1,10 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; + +/** + * @param {Boolean} loading + * @param {String} error + */ +export default function setSessionLoadingAndError(loading, error) { + Onyx.merge(ONYXKEYS.SESSION, {loading, error}); +} diff --git a/src/libs/actions/Session/setShouldSignOut.js b/src/libs/actions/Session/setShouldSignOut.js new file mode 100644 index 000000000000..75acc853e873 --- /dev/null +++ b/src/libs/actions/Session/setShouldSignOut.js @@ -0,0 +1,9 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; + +/** + * @param {Boolean} shouldSignOut + */ +export default function setShouldSignOut(shouldSignOut) { + Onyx.set(ONYXKEYS.SHOULD_SIGN_OUT, shouldSignOut); +} diff --git a/src/libs/actions/Session/updateSessionAuthTokens.js b/src/libs/actions/Session/updateSessionAuthTokens.js new file mode 100644 index 000000000000..5be53c77a92c --- /dev/null +++ b/src/libs/actions/Session/updateSessionAuthTokens.js @@ -0,0 +1,10 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../../ONYXKEYS'; + +/** + * @param {String} authToken + * @param {String} encryptedAuthToken + */ +export default function updateSessionAuthTokens(authToken, encryptedAuthToken) { + Onyx.merge(ONYXKEYS.SESSION, {authToken, encryptedAuthToken}); +} diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index 9b4b43ee1566..95e8b862d8bc 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -1,5 +1,4 @@ import Onyx from 'react-native-onyx'; -// eslint-disable-next-line import/no-cycle import SignoutManager from '../SignoutManager'; import ONYXKEYS from '../../ONYXKEYS'; diff --git a/src/libs/actions/Timing.js b/src/libs/actions/Timing.js index c7ee5a679c7e..a3a4567f2dfa 100644 --- a/src/libs/actions/Timing.js +++ b/src/libs/actions/Timing.js @@ -1,5 +1,4 @@ import getPlatform from '../getPlatform'; -// eslint-disable-next-line import/no-cycle import {Graphite_Timer} from '../API'; import {isDevelopment} from '../Environment/Environment'; import Firebase from '../Firebase'; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 4a5d60e4badd..32c6cf906601 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -1,4 +1,3 @@ -/* eslint-disable import/no-cycle */ import _ from 'underscore'; import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; diff --git a/src/libs/createCallback.js b/src/libs/createCallback.js new file mode 100644 index 000000000000..61402a6edc33 --- /dev/null +++ b/src/libs/createCallback.js @@ -0,0 +1,36 @@ +import _ from 'underscore'; + +/** + * Utility for registering a single callback. Use to inject dependencies in places where you would normally "register" a method + * that you want to call later. Can be overwritten or cleared and returns value to caller. This is intented to be used with array + * destructuring. + * + * @example + * + * const [onResponse, handleResponse, clearHandler] = createCallback(); + * + * @returns {Array} + */ +function createCallback() { + let callback = null; + + function clear() { + callback = null; + } + + function set(newCallback) { + callback = newCallback; + } + + function run(...args) { + if (!_.isFunction(callback)) { + return; + } + + return callback(...args); + } + + return [run, set, clear]; +} + +export default createCallback; diff --git a/src/libs/requireParameters.js b/src/libs/requireParameters.js new file mode 100644 index 000000000000..a40d8417cd2d --- /dev/null +++ b/src/libs/requireParameters.js @@ -0,0 +1,30 @@ +import _ from 'underscore'; + +/** + * @throws {Error} If the "parameters" object has a null or undefined value for any of the given parameterNames + * + * @param {String[]} parameterNames Array of the required parameter names + * @param {Object} parameters A map from available parameter names to their values + * @param {String} commandName The name of the API command + */ +export default function requireParameters(parameterNames, parameters, commandName) { + parameterNames.forEach((parameterName) => { + if (_(parameters).has(parameterName) + && parameters[parameterName] !== null + && parameters[parameterName] !== undefined + ) { + return; + } + + const propertiesToRedact = ['authToken', 'password', 'partnerUserSecret', 'twoFactorAuthCode']; + const parametersCopy = _.chain(parameters) + .clone() + .mapObject((val, key) => (_.contains(propertiesToRedact, key) ? '' : val)) + .value(); + const keys = _(parametersCopy).keys().join(', ') || 'none'; + + let error = `Parameter ${parameterName} is required for "${commandName}". `; + error += `Supplied parameters: ${keys}`; + throw new Error(error); + }); +} diff --git a/src/libs/translate.js b/src/libs/translate.js index a024e7e59444..3fd7ca2253f5 100644 --- a/src/libs/translate.js +++ b/src/libs/translate.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import lodashGet from 'lodash/get'; import Str from 'expensify-common/lib/str'; import Onyx from 'react-native-onyx'; -// eslint-disable-next-line import/no-cycle import Log from './Log'; import Config from '../CONFIG'; import translations from '../languages/translations'; diff --git a/tests/unit/createCallbackTest.js b/tests/unit/createCallbackTest.js new file mode 100644 index 000000000000..7180452bf9f7 --- /dev/null +++ b/tests/unit/createCallbackTest.js @@ -0,0 +1,25 @@ +import createCallback from '../../src/libs/createCallback'; + +test('Callback utility works', () => { + // GIVEN a generic callback setup + const [doSomething, registerDoSomething, clear] = createCallback(); + const mockCallback = jest.fn(); + + // WHEN we register a callback + registerDoSomething(mockCallback); + + // THEN call the callback + doSomething(); + + // THEN our callback will be called + expect(mockCallback).toHaveBeenCalledTimes(1); + + // WHEN we clear the callback + clear(); + + // and call again + doSomething(); + + // THEN expect mock callback to not have been called again + expect(mockCallback).toHaveBeenCalledTimes(1); +});