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) ? '<redacted>' : 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) ? '<redacted>' : 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);
+});
