Skip to content

Commit

Permalink
Merge pull request Expensify#28031 from software-mansion-labs/ts-migr…
Browse files Browse the repository at this point in the history
…ation/API-Middleware-Authentication-Network

[No QA][TS migration] Migrate 'API.js' & 'Middleware' & 'Authentication.js' & 'Network' lib to TypeScript
  • Loading branch information
iwiznia authored Oct 17, 2023
2 parents 4c1ae82 + d47d344 commit bb22e06
Show file tree
Hide file tree
Showing 21 changed files with 211 additions and 289 deletions.
83 changes: 50 additions & 33 deletions src/libs/API.js → src/libs/API.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import _ from 'underscore';
import Onyx from 'react-native-onyx';
import Onyx, {OnyxUpdate} from 'react-native-onyx';
import {ValueOf} from 'type-fest';
import Log from './Log';
import * as Request from './Request';
import * as Middleware from './Middleware';
import * as SequentialQueue from './Network/SequentialQueue';
import pkg from '../../package.json';
import CONST from '../CONST';
import * as Pusher from './Pusher/pusher';
import OnyxRequest from '../types/onyx/Request';
import Response from '../types/onyx/Response';

// Setup API middlewares. Each request made will pass through a series of middleware functions that will get called in sequence (each one passing the result of the previous to the next).
// Note: The ordering here is intentional as we want to Log, Recheck Connection, Reauthenticate, and Save the Response in Onyx. Errors thrown in one middleware will bubble to the next.
Expand All @@ -28,25 +30,34 @@ Request.use(Middleware.HandleUnusedOptimisticID);
// middlewares after this, because the SequentialQueue depends on the result of this middleware to pause the queue (if needed) to bring the app to an up-to-date state.
Request.use(Middleware.SaveResponseInOnyx);

type OnyxData = {
optimisticData?: OnyxUpdate[];
successData?: OnyxUpdate[];
failureData?: OnyxUpdate[];
};

type ApiRequestType = ValueOf<typeof CONST.API_REQUEST_TYPE>;

/**
* All calls to API.write() will be persisted to disk as JSON with the params, successData, and failureData.
* This is so that if the network is unavailable or the app is closed, we can send the WRITE request later.
*
* @param {String} command - Name of API command to call.
* @param {Object} apiCommandParameters - Parameters to send to the API.
* @param {Object} onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
* @param command - Name of API command to call.
* @param apiCommandParameters - Parameters to send to the API.
* @param onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
* into Onyx before and after a request is made. Each nested object will be formatted in
* the same way as an API response.
* @param {Object} [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
* @param {Object} [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
* @param {Object} [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
* @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
* @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
* @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
*/
function write(command, apiCommandParameters = {}, onyxData = {}) {
function write(command: string, apiCommandParameters: Record<string, unknown> = {}, onyxData: OnyxData = {}) {
Log.info('Called API write', false, {command, ...apiCommandParameters});
const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData;

// Optimistically update Onyx
if (onyxData.optimisticData) {
Onyx.update(onyxData.optimisticData);
if (optimisticData) {
Onyx.update(optimisticData);
}

// Assemble the data we'll send to the API
Expand All @@ -61,7 +72,7 @@ function write(command, apiCommandParameters = {}, onyxData = {}) {
};

// Assemble all the request data we'll be storing in the queue
const request = {
const request: OnyxRequest = {
command,
data: {
...data,
Expand All @@ -70,7 +81,7 @@ function write(command, apiCommandParameters = {}, onyxData = {}) {
shouldRetry: true,
canCancel: true,
},
..._.omit(onyxData, 'optimisticData'),
...onyxDataWithoutOptimisticData,
};

// Write commands can be saved and retried, so push it to the SequentialQueue
Expand All @@ -85,24 +96,30 @@ function write(command, apiCommandParameters = {}, onyxData = {}) {
* Using this method is discouraged and will throw an ESLint error. Use it sparingly and only when all other alternatives have been exhausted.
* It is best to discuss it in Slack anytime you are tempted to use this method.
*
* @param {String} command - Name of API command to call.
* @param {Object} apiCommandParameters - Parameters to send to the API.
* @param {Object} onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
* @param command - Name of API command to call.
* @param apiCommandParameters - Parameters to send to the API.
* @param onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
* into Onyx before and after a request is made. Each nested object will be formatted in
* the same way as an API response.
* @param {Object} [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
* @param {Object} [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
* @param {Object} [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
* @param {String} [apiRequestType] - Can be either 'read', 'write', or 'makeRequestWithSideEffects'. We use this to either return the chained
* @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
* @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
* @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
* @param [apiRequestType] - Can be either 'read', 'write', or 'makeRequestWithSideEffects'. We use this to either return the chained
* response back to the caller or to trigger reconnection callbacks when re-authentication is required.
* @returns {Promise}
* @returns
*/
function makeRequestWithSideEffects(command, apiCommandParameters = {}, onyxData = {}, apiRequestType = CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS) {
function makeRequestWithSideEffects(
command: string,
apiCommandParameters = {},
onyxData: OnyxData = {},
apiRequestType: ApiRequestType = CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS,
): Promise<void | Response> {
Log.info('Called API makeRequestWithSideEffects', false, {command, ...apiCommandParameters});
const {optimisticData, ...onyxDataWithoutOptimisticData} = onyxData;

// Optimistically update Onyx
if (onyxData.optimisticData) {
Onyx.update(onyxData.optimisticData);
if (optimisticData) {
Onyx.update(optimisticData);
}

// Assemble the data we'll send to the API
Expand All @@ -113,10 +130,10 @@ function makeRequestWithSideEffects(command, apiCommandParameters = {}, onyxData
};

// Assemble all the request data we'll be storing
const request = {
const request: OnyxRequest = {
command,
data,
..._.omit(onyxData, 'optimisticData'),
...onyxDataWithoutOptimisticData,
};

// Return a promise containing the response from HTTPS
Expand All @@ -126,16 +143,16 @@ function makeRequestWithSideEffects(command, apiCommandParameters = {}, onyxData
/**
* Requests made with this method are not be persisted to disk. If there is no network connectivity, the request is ignored and discarded.
*
* @param {String} command - Name of API command to call.
* @param {Object} apiCommandParameters - Parameters to send to the API.
* @param {Object} onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
* @param command - Name of API command to call.
* @param apiCommandParameters - Parameters to send to the API.
* @param onyxData - Object containing errors, loading states, and optimistic UI data that will be merged
* into Onyx before and after a request is made. Each nested object will be formatted in
* the same way as an API response.
* @param {Object} [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
* @param {Object} [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
* @param {Object} [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
* @param [onyxData.optimisticData] - Onyx instructions that will be passed to Onyx.update() before the request is made.
* @param [onyxData.successData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode === 200.
* @param [onyxData.failureData] - Onyx instructions that will be passed to Onyx.update() when the response has jsonCode !== 200.
*/
function read(command, apiCommandParameters, onyxData) {
function read(command: string, apiCommandParameters: Record<string, unknown>, onyxData: OnyxData = {}) {
// Ensure all write requests on the sequential queue have finished responding before running read requests.
// Responses from read requests can overwrite the optimistic data inserted by
// write requests that use the same Onyx keys and haven't responded yet.
Expand Down
38 changes: 18 additions & 20 deletions src/libs/Authentication.js → src/libs/Authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ import redirectToSignIn from './actions/SignInRedirect';
import CONST from '../CONST';
import Log from './Log';
import * as ErrorUtils from './ErrorUtils';
import Response from '../types/onyx/Response';

/**
* @param {Object} parameters
* @param {Boolean} [parameters.useExpensifyLogin]
* @param {String} parameters.partnerName
* @param {String} parameters.partnerPassword
* @param {String} parameters.partnerUserID
* @param {String} parameters.partnerUserSecret
* @param {String} [parameters.twoFactorAuthCode]
* @param {String} [parameters.email]
* @param {String} [parameters.authToken]
* @returns {Promise}
*/
function Authenticate(parameters) {
type Parameters = {
useExpensifyLogin?: boolean;
partnerName: string;
partnerPassword: string;
partnerUserID?: string;
partnerUserSecret?: string;
twoFactorAuthCode?: string;
email?: string;
authToken?: string;
};

function Authenticate(parameters: Parameters): Promise<Response> {
const commandName = 'Authenticate';

requireParameters(['partnerName', 'partnerPassword', 'partnerUserID', 'partnerUserSecret'], parameters, commandName);
Expand Down Expand Up @@ -48,11 +48,9 @@ function Authenticate(parameters) {

/**
* Reauthenticate using the stored credentials and redirect to the sign in page if unable to do so.
*
* @param {String} [command] command name for logging purposes
* @returns {Promise}
* @param [command] command name for logging purposes
*/
function reauthenticate(command = '') {
function reauthenticate(command = ''): Promise<void> {
// Prevent any more requests from being processed while authentication happens
NetworkStore.setIsAuthenticating(true);

Expand All @@ -61,8 +59,8 @@ function reauthenticate(command = '') {
useExpensifyLogin: false,
partnerName: CONFIG.EXPENSIFY.PARTNER_NAME,
partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD,
partnerUserID: credentials.autoGeneratedLogin,
partnerUserSecret: credentials.autoGeneratedPassword,
partnerUserID: credentials?.autoGeneratedLogin,
partnerUserSecret: credentials?.autoGeneratedPassword,
}).then((response) => {
if (response.jsonCode === CONST.JSON_CODE.UNABLE_TO_RETRY) {
// If authentication fails, then the network can be unpaused
Expand Down Expand Up @@ -92,7 +90,7 @@ function reauthenticate(command = '') {
// 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);
NetworkStore.setAuthToken(response.authToken ?? null);

// The authentication process is finished so the network can be unpaused to continue processing requests
NetworkStore.setIsAuthenticating(false);
Expand Down
50 changes: 18 additions & 32 deletions src/libs/Middleware/Logging.js → src/libs/Middleware/Logging.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
import Log from '../Log';
import CONST from '../../CONST';
import Request from '../../types/onyx/Request';
import Response from '../../types/onyx/Response';
import Middleware from './types';

/**
* @param {String} message
* @param {Object} request
* @param {Object} [response]
*/
function logRequestDetails(message, request, response = {}) {
function logRequestDetails(message: string, request: Request, response?: Response | void) {
// Don't log about log or else we'd cause an infinite loop
if (request.command === 'Log') {
return;
}

const logParams = {
const logParams: Record<string, unknown> = {
command: request.command,
shouldUseSecure: request.shouldUseSecure,
};

const returnValueList = lodashGet(request, 'data.returnValueList');
const returnValueList = request?.data?.returnValueList;
if (returnValueList) {
logParams.returnValueList = returnValueList;
}

const nvpNames = lodashGet(request, 'data.nvpNames');
const nvpNames = request?.data?.nvpNames;
if (nvpNames) {
logParams.nvpNames = nvpNames;
}
Expand All @@ -37,22 +33,15 @@ function logRequestDetails(message, request, response = {}) {
Log.info(message, false, logParams);
}

/**
* Logging middleware
*
* @param {Promise} response
* @param {Object} request
* @returns {Promise}
*/
function Logging(response, request) {
const Logging: Middleware = (response, request) => {
logRequestDetails('Making API request', request);
return response
.then((data) => {
logRequestDetails('Finished API request', request, data);
return data;
})
.catch((error) => {
const logParams = {
const logParams: Record<string, unknown> = {
message: error.message,
status: error.status,
title: error.title,
Expand All @@ -73,21 +62,18 @@ function Logging(response, request) {
// incorrect url, bad cors headers returned by the server, DNS lookup failure etc.
Log.hmmm('[Network] API request error: Failed to fetch', logParams);
} else if (
_.contains(
[
CONST.ERROR.IOS_NETWORK_CONNECTION_LOST,
CONST.ERROR.NETWORK_REQUEST_FAILED,
CONST.ERROR.IOS_NETWORK_CONNECTION_LOST_RUSSIAN,
CONST.ERROR.IOS_NETWORK_CONNECTION_LOST_SWEDISH,
CONST.ERROR.IOS_NETWORK_CONNECTION_LOST_SPANISH,
],
error.message,
)
[
CONST.ERROR.IOS_NETWORK_CONNECTION_LOST,
CONST.ERROR.NETWORK_REQUEST_FAILED,
CONST.ERROR.IOS_NETWORK_CONNECTION_LOST_RUSSIAN,
CONST.ERROR.IOS_NETWORK_CONNECTION_LOST_SWEDISH,
CONST.ERROR.IOS_NETWORK_CONNECTION_LOST_SPANISH,
].includes(error.message)
) {
// These errors seem to happen for native devices with interrupted connections. Often we will see logs about Pusher disconnecting together with these.
// This type of error may also indicate a problem with SSL certs.
Log.hmmm('[Network] API request error: Connection interruption likely', logParams);
} else if (_.contains([CONST.ERROR.FIREFOX_DOCUMENT_LOAD_ABORTED, CONST.ERROR.SAFARI_DOCUMENT_LOAD_ABORTED], error.message)) {
} else if ([CONST.ERROR.FIREFOX_DOCUMENT_LOAD_ABORTED, CONST.ERROR.SAFARI_DOCUMENT_LOAD_ABORTED].includes(error.message)) {
// This message can be observed page load is interrupted (closed or navigated away).
Log.hmmm('[Network] API request error: User likely navigated away from or closed browser', logParams);
} else if (error.message === CONST.ERROR.IOS_LOAD_FAILED) {
Expand Down Expand Up @@ -123,6 +109,6 @@ function Logging(response, request) {
// Re-throw this error so the next handler can manage it
throw error;
});
}
};

export default Logging;
Loading

0 comments on commit bb22e06

Please sign in to comment.