Skip to content

Commit

Permalink
Merge pull request #8306 from Expensify/marcaaron-networkCleanup
Browse files Browse the repository at this point in the history
Cleanup Network.js code. Fix persisted request handling.
  • Loading branch information
marcaaron authored Mar 25, 2022
2 parents 4d646e7 + fb3904c commit ee5d18e
Show file tree
Hide file tree
Showing 11 changed files with 431 additions and 207 deletions.
7 changes: 4 additions & 3 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ const CONST = {
API_OFFLINE: 'session.offlineMessageRetry',
UNKNOWN_ERROR: 'Unknown error',
REQUEST_CANCELLED: 'AbortError',
FAILED_TO_FETCH: 'Failed to fetch',
ENSURE_BUGBOT: 'ENSURE_BUGBOT',
},
NETWORK: {
METHOD: {
Expand All @@ -323,10 +325,9 @@ const CONST = {
PROCESS_REQUEST_DELAY_MS: 1000,
MAX_PENDING_TIME_MS: 10 * 1000,
},
HTTP_STATUS_CODE: {
JSON_CODE: {
SUCCESS: 200,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
NOT_AUTHENTICATED: 407,
},
NVP: {
IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'isFirstTimeNewExpensifyUser',
Expand Down
120 changes: 30 additions & 90 deletions src/libs/API.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
import Onyx from 'react-native-onyx';
import CONST from '../CONST';
import CONFIG from '../CONFIG';
import ONYXKEYS from '../ONYXKEYS';
import getPlaidLinkTokenParameters from './getPlaidLinkTokenParameters';
import redirectToSignIn from './actions/SignInRedirect';
import isViaExpensifyCashNative from './isViaExpensifyCashNative';
Expand All @@ -12,87 +10,10 @@ import Log from './Log';
import * as Network from './Network';
import updateSessionAuthTokens from './actions/Session/updateSessionAuthTokens';
import setSessionLoadingAndError from './actions/Session/setSessionLoadingAndError';
import * as NetworkStore from './Network/NetworkStore';
import enhanceParameters from './Network/enhanceParameters';

let isAuthenticating;
let credentials;
let authToken;
let currentUserEmail;

function checkRequiredDataAndSetNetworkReady() {
if (_.isUndefined(authToken) || _.isUndefined(credentials)) {
return;
}

Network.setIsReady(true);
}

Onyx.connect({
key: ONYXKEYS.CREDENTIALS,
callback: (val) => {
credentials = val || null;
checkRequiredDataAndSetNetworkReady();
},
});

Onyx.connect({
key: ONYXKEYS.SESSION,
callback: (val) => {
authToken = lodashGet(val, 'authToken', null);
currentUserEmail = lodashGet(val, 'email', null);
checkRequiredDataAndSetNetworkReady();
},
});

/**
* Does this command require an authToken?
*
* @param {String} command
* @return {Boolean}
*/
function isAuthTokenRequired(command) {
return !_.contains([
'Log',
'Graphite_Timer',
'Authenticate',
'GetAccountStatus',
'SetPassword',
'User_SignUp',
'ResendValidateCode',
'ResetPassword',
'User_ReopenAccount',
'ValidateEmail',
], command);
}

/**
* Adds default values to our request data
*
* @param {String} command
* @param {Object} parameters
* @returns {Object}
*/
function addDefaultValuesToParameters(command, parameters) {
const finalParameters = {...parameters};

if (isAuthTokenRequired(command) && !parameters.authToken) {
finalParameters.authToken = authToken;
}

finalParameters.referer = CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER;

// This application does not save its authToken in cookies like the classic Expensify app.
// Setting api_setCookie to false will ensure that the Expensify API doesn't set any cookies
// and prevents interfering with the cookie authToken that Expensify classic uses.
finalParameters.api_setCookie = false;

// Unless email is already set include current user's email in every request and the server logs
finalParameters.email = lodashGet(parameters, 'email', currentUserEmail);

return finalParameters;
}

// Tie into the network layer to add auth token to the parameters of all requests
Network.registerParameterEnhancer(addDefaultValuesToParameters);

/**
* Function used to handle expired auth tokens. It re-authenticates with the API and
Expand All @@ -118,7 +39,7 @@ function handleExpiredAuthToken(originalCommand, originalParameters, originalTyp
return reauthenticate(originalCommand)
.then(() => {
// Now that the API is authenticated, make the original request again with the new authToken
const params = addDefaultValuesToParameters(originalCommand, originalParameters);
const params = enhanceParameters(originalCommand, originalParameters);
return Network.post(originalCommand, params, originalType);
})
.catch(() => (
Expand Down Expand Up @@ -159,8 +80,10 @@ Network.registerResponseHandler((queuedRequest, response) => {
});
}

if (response.jsonCode === 407) {
// Credentials haven't been initialized. We will not be able to re-authenticates with the API
if (response.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) {
const credentials = NetworkStore.getCredentials();

// Credentials haven't been initialized. We will not be able to re-authenticate with the API
const unableToReauthenticate = (!credentials || !credentials.autoGeneratedLogin
|| !credentials.autoGeneratedPassword);

Expand All @@ -169,13 +92,21 @@ Network.registerResponseHandler((queuedRequest, response) => {
// of the new response created by handleExpiredAuthToken.
const shouldRetry = lodashGet(queuedRequest, 'data.shouldRetry');
if (!shouldRetry || unableToReauthenticate) {
queuedRequest.resolve(response);
// Check to see if queuedRequest has a resolve method as this could be a persisted request which had it's promise handling logic stripped
// from it when persisted to storage
if (queuedRequest.resolve) {
queuedRequest.resolve(response);
}
return;
}

handleExpiredAuthToken(queuedRequest.command, queuedRequest.data, queuedRequest.type)
.then(queuedRequest.resolve)
.catch(queuedRequest.reject);
.then(queuedRequest.resolve || (() => Promise.resolve()))
.catch(queuedRequest.reject || (() => Promise.resolve()));
return;
}

if (!queuedRequest.resolve) {
return;
}

Expand All @@ -197,8 +128,12 @@ Network.registerErrorHandler((queuedRequest, error) => {
// Set an error state and signify we are done loading
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));
// Reject the queued request with an API offline error so that the original caller can handle it
// Note: We are checking for the reject method as this could be a persisted request which had it's promise handling logic stripped
// from it when persisted to storage
if (queuedRequest.reject) {
queuedRequest.reject(new Error(CONST.ERROR.API_OFFLINE));
}
});

/**
Expand Down Expand Up @@ -280,6 +215,7 @@ function Authenticate(parameters) {
* @returns {Promise}
*/
function reauthenticate(command = '') {
const credentials = NetworkStore.getCredentials();
return Authenticate({
useExpensifyLogin: false,
partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
Expand All @@ -297,7 +233,11 @@ function reauthenticate(command = '') {
// Update authToken in Onyx and in our local variables so that API requests will use the
// new authToken
updateSessionAuthTokens(response.authToken, response.encryptedAuthToken);
authToken = response.authToken;

// Note: It is important to manually set the authToken that is in the store here since any requests that are hooked into
// reauthenticate .then() will immediate post and use the local authToken. Onyx updates subscribers lately so it is not
// enough to do the updateSessionAuthTokens() call above.
NetworkStore.setAuthToken(response.authToken);

// The authentication process is finished so the network can be unpaused to continue
// processing requests
Expand Down
8 changes: 7 additions & 1 deletion src/libs/HttpUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ function processHTTPRequest(url, method = 'get', body = null, canCancel = true)
method,
body,
})
.then(response => response.json());
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}

return response.json();
});
}

/**
Expand Down
89 changes: 89 additions & 0 deletions src/libs/Network/NetworkStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import lodashGet from 'lodash/get';
import Onyx from 'react-native-onyx';
import _ from 'underscore';
import ONYXKEYS from '../../ONYXKEYS';

let credentials;
let authToken;
let currentUserEmail;
let networkReady = false;

/**
* @param {Boolean} ready
*/
function setIsReady(ready) {
networkReady = ready;
}

/**
* This is a hack to workaround the fact that Onyx may not yet have read these values from storage by the time Network starts processing requests.
* If the values are undefined we haven't read them yet. If they are null or have a value then we have and the network is "ready".
*/
function checkRequiredDataAndSetNetworkReady() {
if (_.isUndefined(authToken) || _.isUndefined(credentials)) {
return;
}

setIsReady(true);
}

Onyx.connect({
key: ONYXKEYS.SESSION,
callback: (val) => {
authToken = lodashGet(val, 'authToken', null);
currentUserEmail = lodashGet(val, 'email', null);
checkRequiredDataAndSetNetworkReady();
},
});

Onyx.connect({
key: ONYXKEYS.CREDENTIALS,
callback: (val) => {
credentials = val || null;
checkRequiredDataAndSetNetworkReady();
},
});

/**
* @returns {String}
*/
function getAuthToken() {
return authToken;
}

/**
* @param {String} newAuthToken
*/
function setAuthToken(newAuthToken) {
authToken = newAuthToken;
}

/**
* @returns {Object}
*/
function getCredentials() {
return credentials;
}

/**
* @returns {String}
*/
function getCurrentUserEmail() {
return currentUserEmail;
}

/**
* @returns {Boolean}
*/
function isReady() {
return networkReady;
}

export {
getAuthToken,
setAuthToken,
getCredentials,
getCurrentUserEmail,
isReady,
setIsReady,
};
52 changes: 52 additions & 0 deletions src/libs/Network/enhanceParameters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import lodashGet from 'lodash/get';
import _ from 'underscore';
import CONFIG from '../../CONFIG';
import * as NetworkStore from './NetworkStore';

/**
* Does this command require an authToken?
*
* @param {String} command
* @return {Boolean}
*/
function isAuthTokenRequired(command) {
return !_.contains([
'Log',
'Graphite_Timer',
'Authenticate',
'GetAccountStatus',
'SetPassword',
'User_SignUp',
'ResendValidateCode',
'ResetPassword',
'User_ReopenAccount',
'ValidateEmail',
], command);
}

/**
* Adds default values to our request data
*
* @param {String} command
* @param {Object} parameters
* @returns {Object}
*/
export default function enhanceParameters(command, parameters) {
const finalParameters = {...parameters};

if (isAuthTokenRequired(command) && !parameters.authToken) {
finalParameters.authToken = NetworkStore.getAuthToken();
}

finalParameters.referer = CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER;

// This application does not save its authToken in cookies like the classic Expensify app.
// Setting api_setCookie to false will ensure that the Expensify API doesn't set any cookies
// and prevents interfering with the cookie authToken that Expensify classic uses.
finalParameters.api_setCookie = false;

// Include current user's email in every request and the server logs
finalParameters.email = lodashGet(parameters, 'email', NetworkStore.getCurrentUserEmail());

return finalParameters;
}
Loading

0 comments on commit ee5d18e

Please sign in to comment.