diff --git a/src/components/Alert/index.js b/src/components/Alert/index.js index dc3a722c2331..3fc90433f13e 100644 --- a/src/components/Alert/index.js +++ b/src/components/Alert/index.js @@ -5,7 +5,7 @@ import _ from 'underscore'; * * @param {String} title The title of the alert * @param {String} description The description of the alert - * @param {Object[]} options An array of objects with `style` and `onPress` properties + * @param {Object[]} [options] An array of objects with `style` and `onPress` properties */ export default (title, description, options) => { const result = _.filter(window.confirm([title, description], Boolean)).join('\n'); diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.ts similarity index 65% rename from src/libs/HttpUtils.js rename to src/libs/HttpUtils.ts index 2df7421ea91c..859c8624833c 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.ts @@ -1,13 +1,16 @@ import Onyx from 'react-native-onyx'; -import _ from 'underscore'; +import {ValueOf} from 'type-fest'; import alert from '@components/Alert'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {RequestType} from '@src/types/onyx/Request'; +import type Response from '@src/types/onyx/Response'; import * as ApiUtils from './ApiUtils'; import HttpsError from './Errors/HttpsError'; let shouldFailAllRequests = false; let shouldForceOffline = false; + Onyx.connect({ key: ONYXKEYS.NETWORK, callback: (network) => { @@ -25,14 +28,8 @@ let cancellationController = new AbortController(); /** * Send an HTTP request, and attempt to resolve the json response. * If there is a network error, we'll set the application offline. - * - * @param {String} url - * @param {String} [method] - * @param {Object} [body] - * @param {Boolean} [canCancel] - * @returns {Promise} */ -function processHTTPRequest(url, method = 'get', body = null, canCancel = true) { +function processHTTPRequest(url: string, method: RequestType = 'get', body: FormData | null = null, canCancel = true): Promise { return fetch(url, { // We hook requests to the same Controller signal, so we can cancel them all at once signal: canCancel ? cancellationController.signal : undefined, @@ -49,40 +46,41 @@ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) if (!response.ok) { // Expensify site is down or there was an internal server error, or something temporary like a Bad Gateway, or unknown error occurred - const serviceInterruptedStatuses = [ + const serviceInterruptedStatuses: Array> = [ CONST.HTTP_STATUS.INTERNAL_SERVER_ERROR, CONST.HTTP_STATUS.BAD_GATEWAY, CONST.HTTP_STATUS.GATEWAY_TIMEOUT, CONST.HTTP_STATUS.UNKNOWN_ERROR, ]; - if (_.contains(serviceInterruptedStatuses, response.status)) { + if (serviceInterruptedStatuses.indexOf(response.status as ValueOf) > -1) { throw new HttpsError({ message: CONST.ERROR.EXPENSIFY_SERVICE_INTERRUPTED, - status: response.status, + status: response.status.toString(), title: 'Issue connecting to Expensify site', }); - } else if (response.status === CONST.HTTP_STATUS.TOO_MANY_REQUESTS) { + } + if (response.status === CONST.HTTP_STATUS.TOO_MANY_REQUESTS) { throw new HttpsError({ message: CONST.ERROR.THROTTLED, - status: response.status, + status: response.status.toString(), title: 'API request throttled', }); } throw new HttpsError({ message: response.statusText, - status: response.status, + status: response.status.toString(), }); } - return response.json(); + return response.json() as Promise; }) .then((response) => { // Some retried requests will result in a "Unique Constraints Violation" error from the server, which just means the record already exists if (response.jsonCode === CONST.JSON_CODE.BAD_REQUEST && response.message === CONST.ERROR_TITLE.DUPLICATE_RECORD) { throw new HttpsError({ message: CONST.ERROR.DUPLICATE_RECORD, - status: CONST.JSON_CODE.BAD_REQUEST, + status: CONST.JSON_CODE.BAD_REQUEST.toString(), title: CONST.ERROR_TITLE.DUPLICATE_RECORD, }); } @@ -91,43 +89,42 @@ function processHTTPRequest(url, method = 'get', body = null, canCancel = true) if (response.jsonCode === CONST.JSON_CODE.EXP_ERROR && response.title === CONST.ERROR_TITLE.SOCKET && response.type === CONST.ERROR_TYPE.SOCKET) { throw new HttpsError({ message: CONST.ERROR.EXPENSIFY_SERVICE_INTERRUPTED, - status: CONST.JSON_CODE.EXP_ERROR, + status: CONST.JSON_CODE.EXP_ERROR.toString(), title: CONST.ERROR_TITLE.SOCKET, }); } if (response.jsonCode === CONST.JSON_CODE.MANY_WRITES_ERROR) { - const {phpCommandName, authWriteCommands} = response.data; - // eslint-disable-next-line max-len - const message = `The API call (${phpCommandName}) did more Auth write requests than allowed. Count ${authWriteCommands.length}, commands: ${authWriteCommands.join( - ', ', - )}. Check the APIWriteCommands class in Web-Expensify`; - alert('Too many auth writes', message); + if (response.data) { + const {phpCommandName, authWriteCommands} = response.data; + // eslint-disable-next-line max-len + const message = `The API call (${phpCommandName}) did more Auth write requests than allowed. Count ${authWriteCommands.length}, commands: ${authWriteCommands.join( + ', ', + )}. Check the APIWriteCommands class in Web-Expensify`; + alert('Too many auth writes', message); + } } - return response; + return response as Promise; }); } /** * Makes XHR request - * @param {String} command the name of the API command - * @param {Object} data parameters for the API command - * @param {String} type HTTP request type (get/post) - * @param {Boolean} shouldUseSecure should we use the secure server - * @returns {Promise} + * @param command the name of the API command + * @param data parameters for the API command + * @param type HTTP request type (get/post) + * @param shouldUseSecure should we use the secure server */ -function xhr(command, data, type = CONST.NETWORK.METHOD.POST, shouldUseSecure = false) { +function xhr(command: string, data: Record, type: RequestType = CONST.NETWORK.METHOD.POST, shouldUseSecure = false): Promise { const formData = new FormData(); - _.each(data, (val, key) => { - // Do not send undefined request parameters to our API. They will be processed as strings of 'undefined'. - if (_.isUndefined(val)) { + Object.keys(data).forEach((key) => { + if (typeof data[key] === 'undefined') { return; } - - formData.append(key, val); + formData.append(key, data[key] as string | Blob); }); const url = ApiUtils.getCommandURL({shouldUseSecure, command}); - return processHTTPRequest(url, type, formData, data.canCancel); + return processHTTPRequest(url, type, formData, Boolean(data.canCancel)); } function cancelPendingRequests() { diff --git a/src/libs/Request.ts b/src/libs/Request.ts index 335731763ec9..18fadca467ad 100644 --- a/src/libs/Request.ts +++ b/src/libs/Request.ts @@ -16,7 +16,7 @@ function makeXHR(request: Request): Promise { return new Promise((resolve) => resolve()); } - return HttpUtils.xhr(request.command, finalParameters, request.type, request.shouldUseSecure) as Promise; + return HttpUtils.xhr(request.command, finalParameters, request.type, request.shouldUseSecure); }); } diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index c97a5a21f488..746e7f75b3d5 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -7,11 +7,13 @@ type OnyxData = { optimisticData?: OnyxUpdate[]; }; +type RequestType = 'get' | 'post'; + type RequestData = { command: string; commandName?: string; data?: Record; - type?: string; + type?: RequestType; shouldUseSecure?: boolean; successData?: OnyxUpdate[]; failureData?: OnyxUpdate[]; @@ -24,4 +26,4 @@ type RequestData = { type Request = RequestData & OnyxData; export default Request; -export type {OnyxData}; +export type {OnyxData, RequestType}; diff --git a/src/types/onyx/Response.ts b/src/types/onyx/Response.ts index d36a875ea6de..66d5dcbdfd5b 100644 --- a/src/types/onyx/Response.ts +++ b/src/types/onyx/Response.ts @@ -1,5 +1,10 @@ import {OnyxUpdate} from 'react-native-onyx'; +type Data = { + phpCommandName: string; + authWriteCommands: string[]; +}; + type Response = { previousUpdateID?: number | string; lastUpdateID?: number | string; @@ -10,6 +15,9 @@ type Response = { authToken?: string; encryptedAuthToken?: string; message?: string; + title?: string; + data?: Data; + type?: string; shortLivedAuthToken?: string; auth?: string; // eslint-disable-next-line @typescript-eslint/naming-convention