diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 17c2ef0c1998..ba035c8b3baf 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -186,7 +186,9 @@ function OptionRowLHN(props) { onSecondaryInteraction={(e) => { showPopover(e); // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time - DomUtils.getActiveElement().blur(); + if (DomUtils.getActiveElement()) { + DomUtils.getActiveElement().blur(); + } }} withoutFocusOnSecondaryInteraction activeOpacity={0.8} diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 6b2b4e16db65..49681f396181 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -62,7 +62,7 @@ const defaultProps = { function MoneyReportHeader({session, personalDetails, policy, chatReport, report: moneyRequestReport, isSmallScreenWidth}) { const {translate} = useLocalize(); - const reportTotal = ReportUtils.getMoneyRequestTotal(moneyRequestReport); + const reimbursableTotal = ReportUtils.getMoneyRequestReimbursableTotal(moneyRequestReport); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const policyType = lodashGet(policy, 'type'); @@ -71,8 +71,8 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, report const isPayer = policyType === CONST.POLICY.TYPE.CORPORATE ? isPolicyAdmin && isApproved : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); const isDraft = ReportUtils.isReportDraft(moneyRequestReport); const shouldShowSettlementButton = useMemo( - () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reportTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport), - [isPayer, isDraft, isSettled, moneyRequestReport, reportTotal, chatReport], + () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport), + [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport], ); const shouldShowApproveButton = useMemo(() => { if (policyType !== CONST.POLICY.TYPE.CORPORATE) { @@ -80,10 +80,10 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, report } return isManager && !isDraft && !isApproved && !isSettled; }, [policyType, isManager, isDraft, isApproved, isSettled]); - const shouldShowSubmitButton = isDraft && reportTotal !== 0; + const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); - const formattedAmount = CurrencyUtils.convertToDisplayString(reportTotal, moneyRequestReport.currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableTotal, moneyRequestReport.currency); return ( diff --git a/src/components/ReportActionItem/MoneyReportView.js b/src/components/ReportActionItem/MoneyReportView.js index bfdcc59bf89f..f4be86cf12bd 100644 --- a/src/components/ReportActionItem/MoneyReportView.js +++ b/src/components/ReportActionItem/MoneyReportView.js @@ -28,19 +28,27 @@ const propTypes = { }; function MoneyReportView(props) { - const formattedAmount = CurrencyUtils.convertToDisplayString(ReportUtils.getMoneyRequestTotal(props.report), props.report.currency); - const isSettled = ReportUtils.isSettled(props.report.reportID); const {translate} = useLocalize(); + const isSettled = ReportUtils.isSettled(props.report.reportID); + + const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(props.report); + + const shouldShowBreakdown = nonReimbursableSpend && reimbursableSpend; + const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.report.currency); + const formattedOutOfPocketAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, props.report.currency); + const formattedCompanySpendAmount = CurrencyUtils.convertToDisplayString(nonReimbursableSpend, props.report.currency); + + const subAmountTextStyles = [styles.taskTitleMenuItem, styles.alignSelfCenter, StyleUtils.getFontSizeStyle(variables.fontSizeh1), StyleUtils.getColorStyle(themeColors.textSupporting)]; return ( - + {translate('common.total')} @@ -59,10 +67,50 @@ function MoneyReportView(props) { numberOfLines={1} style={[styles.taskTitleMenuItem, styles.alignSelfCenter]} > - {formattedAmount} + {formattedTotalAmount} + {shouldShowBreakdown ? ( + <> + + + + {translate('cardTransactions.outOfPocket')} + + + + + {formattedOutOfPocketAmount} + + + + + + + {translate('cardTransactions.companySpend')} + + + + + {formattedCompanySpendAmount} + + + + + ) : undefined} { - if (reportTotal) { - return CurrencyUtils.convertToDisplayString(reportTotal, props.iouReport.currency); + if (totalDisplaySpend) { + return CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.iouReport.currency); } if (isScanning) { return props.translate('iou.receiptScanning'); @@ -176,7 +176,7 @@ function ReportPreview(props) { const bankAccountRoute = ReportUtils.getBankAccountRoute(props.chatReport); const shouldShowSettlementButton = ReportUtils.isControlPolicyExpenseChat(props.chatReport) ? props.policy.role === CONST.POLICY.ROLE.ADMIN && ReportUtils.isReportApproved(props.iouReport) && !iouSettled && !iouCanceled - : !_.isEmpty(props.iouReport) && isCurrentUserManager && !isReportDraft && !iouSettled && !iouCanceled && !props.iouReport.isWaitingOnBankAccount && reportTotal !== 0; + : !_.isEmpty(props.iouReport) && isCurrentUserManager && !isReportDraft && !iouSettled && !iouCanceled && !props.iouReport.isWaitingOnBankAccount && reimbursableSpend !== 0; return ( diff --git a/src/languages/en.ts b/src/languages/en.ts index 4e4bd9376c03..e7f71e755dd8 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1852,7 +1852,7 @@ export default { }, cardTransactions: { notActivated: 'Not activated', - outOfPocketSpend: 'Out-of-pocket spend', + outOfPocket: 'Out of pocket', companySpend: 'Company spend', }, distance: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 403643b992dd..6020ded30b92 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2337,7 +2337,7 @@ export default { }, cardTransactions: { notActivated: 'No activado', - outOfPocketSpend: 'Gastos por cuenta propia', + outOfPocket: 'Por cuenta propia', companySpend: 'Gastos de empresa', }, distance: { diff --git a/src/libs/API.js b/src/libs/API.ts similarity index 64% rename from src/libs/API.js rename to src/libs/API.ts index 2ad1f32347d9..ce3d6bab19bc 100644 --- a/src/libs/API.js +++ b/src/libs/API.ts @@ -1,5 +1,5 @@ -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'; @@ -7,6 +7,8 @@ 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. @@ -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; + /** * 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 = {}, 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 @@ -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, @@ -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 @@ -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 { 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 @@ -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 @@ -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, 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. diff --git a/src/libs/Authentication.js b/src/libs/Authentication.ts similarity index 83% rename from src/libs/Authentication.js rename to src/libs/Authentication.ts index 9f1967ecf0d8..cec20504dd04 100644 --- a/src/libs/Authentication.js +++ b/src/libs/Authentication.ts @@ -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 { const commandName = 'Authenticate'; requireParameters(['partnerName', 'partnerPassword', 'partnerUserID', 'partnerUserSecret'], parameters, commandName); @@ -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 { // Prevent any more requests from being processed while authentication happens NetworkStore.setIsAuthenticating(true); @@ -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 @@ -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); diff --git a/src/libs/Middleware/Logging.js b/src/libs/Middleware/Logging.ts similarity index 82% rename from src/libs/Middleware/Logging.js rename to src/libs/Middleware/Logging.ts index fdc9f0083abb..171cb4b9ab4c 100644 --- a/src/libs/Middleware/Logging.js +++ b/src/libs/Middleware/Logging.ts @@ -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 = { 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; } @@ -37,14 +33,7 @@ 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) => { @@ -52,7 +41,7 @@ function Logging(response, request) { return data; }) .catch((error) => { - const logParams = { + const logParams: Record = { message: error.message, status: error.status, title: error.title, @@ -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) { @@ -123,6 +109,6 @@ function Logging(response, request) { // Re-throw this error so the next handler can manage it throw error; }); -} +}; export default Logging; diff --git a/src/libs/Middleware/Reauthentication.js b/src/libs/Middleware/Reauthentication.ts similarity index 86% rename from src/libs/Middleware/Reauthentication.js rename to src/libs/Middleware/Reauthentication.ts index dfe4e1b7fda8..aec09227e441 100644 --- a/src/libs/Middleware/Reauthentication.js +++ b/src/libs/Middleware/Reauthentication.ts @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import CONST from '../../CONST'; import * as NetworkStore from '../Network/NetworkStore'; import * as MainQueue from '../Network/MainQueue'; @@ -6,15 +5,12 @@ import * as Authentication from '../Authentication'; import * as Request from '../Request'; import Log from '../Log'; import NetworkConnection from '../NetworkConnection'; +import Middleware from './types'; // We store a reference to the active authentication request so that we are only ever making one request to authenticate at a time. -let isAuthenticating = null; +let isAuthenticating: Promise | null = null; -/** - * @param {String} commandName - * @returns {Promise} - */ -function reauthenticate(commandName) { +function reauthenticate(commandName?: string): Promise { if (isAuthenticating) { return isAuthenticating; } @@ -32,16 +28,8 @@ function reauthenticate(commandName) { return isAuthenticating; } -/** - * Reauthentication middleware - * - * @param {Promise} response - * @param {Object} request - * @param {Boolean} isFromSequentialQueue - * @returns {Promise} - */ -function Reauthentication(response, request, isFromSequentialQueue) { - return response +const Reauthentication: Middleware = (response, request, isFromSequentialQueue) => + response .then((data) => { // If there is no data for some reason then we cannot reauthenticate if (!data) { @@ -58,13 +46,13 @@ function Reauthentication(response, request, isFromSequentialQueue) { // There are some API requests that should not be retried when there is an auth failure like // creating and deleting logins. In those cases, they should handle the original response instead // of the new response created by handleExpiredAuthToken. - const shouldRetry = lodashGet(request, 'data.shouldRetry'); - const apiRequestType = lodashGet(request, 'data.apiRequestType'); + const shouldRetry = request?.data?.shouldRetry; + const apiRequestType = request?.data?.apiRequestType; // For the SignInWithShortLivedAuthToken command, if the short token expires, the server returns a 407 error, // and credentials are still empty at this time, which causes reauthenticate to throw an error (requireParameters), // and the subsequent SaveResponseInOnyx also cannot be executed, so we need this parameter to skip the reauthentication logic. - const skipReauthentication = lodashGet(request, 'data.skipReauthentication'); + const skipReauthentication = request?.data?.skipReauthentication; if ((!shouldRetry && !apiRequestType) || skipReauthentication) { if (isFromSequentialQueue) { return data; @@ -82,7 +70,7 @@ function Reauthentication(response, request, isFromSequentialQueue) { return data; } - return reauthenticate(request.commandName) + return reauthenticate(request?.commandName) .then((authenticateResponse) => { if (isFromSequentialQueue || apiRequestType === CONST.API_REQUEST_TYPE.MAKE_REQUEST_WITH_SIDE_EFFECTS) { return Request.processWithMiddleware(request, isFromSequentialQueue); @@ -128,6 +116,5 @@ function Reauthentication(response, request, isFromSequentialQueue) { request.resolve({jsonCode: CONST.JSON_CODE.UNABLE_TO_RETRY}); } }); -} export default Reauthentication; diff --git a/src/libs/Middleware/RecheckConnection.js b/src/libs/Middleware/RecheckConnection.ts similarity index 83% rename from src/libs/Middleware/RecheckConnection.js rename to src/libs/Middleware/RecheckConnection.ts index 58f5cfa601c8..5a685d66fd02 100644 --- a/src/libs/Middleware/RecheckConnection.js +++ b/src/libs/Middleware/RecheckConnection.ts @@ -1,20 +1,17 @@ import CONST from '../../CONST'; import NetworkConnection from '../NetworkConnection'; +import Middleware from './types'; /** - * @returns {Function} cancel timer + * @returns cancel timer */ -function startRecheckTimeoutTimer() { +function startRecheckTimeoutTimer(): () => void { // If request is still in processing after this time, we might be offline const timerID = setTimeout(NetworkConnection.recheckNetworkConnection, CONST.NETWORK.MAX_PENDING_TIME_MS); return () => clearTimeout(timerID); } -/** - * @param {Promise} response - * @returns {Promise} - */ -function RecheckConnection(response) { +const RecheckConnection: Middleware = (response) => { // When the request goes past a certain amount of time we trigger a re-check of the connection const cancelRequestTimeoutTimer = startRecheckTimeoutTimer(); return response @@ -27,6 +24,6 @@ function RecheckConnection(response) { throw error; }) .finally(cancelRequestTimeoutTimer); -} +}; export default RecheckConnection; diff --git a/src/libs/Middleware/SaveResponseInOnyx.js b/src/libs/Middleware/SaveResponseInOnyx.ts similarity index 74% rename from src/libs/Middleware/SaveResponseInOnyx.js rename to src/libs/Middleware/SaveResponseInOnyx.ts index d8c47d4c01dd..0a279a7425b4 100644 --- a/src/libs/Middleware/SaveResponseInOnyx.js +++ b/src/libs/Middleware/SaveResponseInOnyx.ts @@ -1,21 +1,16 @@ -import _ from 'underscore'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import * as MemoryOnlyKeys from '../actions/MemoryOnlyKeys/MemoryOnlyKeys'; import * as OnyxUpdates from '../actions/OnyxUpdates'; +import Middleware from './types'; // If we're executing any of these requests, we don't need to trigger our OnyxUpdates flow to update the current data even if our current value is out of // date because all these requests are updating the app to the most current state. const requestsToIgnoreLastUpdateID = ['OpenApp', 'ReconnectApp', 'GetMissingOnyxMessages']; -/** - * @param {Promise} requestResponse - * @param {Object} request - * @returns {Promise} - */ -function SaveResponseInOnyx(requestResponse, request) { - return requestResponse.then((response = {}) => { - const onyxUpdates = response.onyxData; +const SaveResponseInOnyx: Middleware = (requestResponse, request) => + requestResponse.then((response = {}) => { + const onyxUpdates = response?.onyxData ?? []; // Sometimes we call requests that are successfull but they don't have any response or any success/failure data to set. Let's return early since // we don't need to store anything here. @@ -24,7 +19,7 @@ function SaveResponseInOnyx(requestResponse, request) { } // If there is an OnyxUpdate for using memory only keys, enable them - _.find(onyxUpdates, ({key, value}) => { + onyxUpdates?.find(({key, value}) => { if (key !== ONYXKEYS.IS_USING_MEMORY_ONLY_KEYS || !value) { return false; } @@ -35,13 +30,13 @@ function SaveResponseInOnyx(requestResponse, request) { const responseToApply = { type: CONST.ONYX_UPDATE_TYPES.HTTPS, - lastUpdateID: Number(response.lastUpdateID || 0), - previousUpdateID: Number(response.previousUpdateID || 0), + lastUpdateID: Number(response?.lastUpdateID ?? 0), + previousUpdateID: Number(response?.previousUpdateID ?? 0), request, - response, + response: response ?? {}, }; - if (_.includes(requestsToIgnoreLastUpdateID, request.command) || !OnyxUpdates.doesClientNeedToBeUpdated(Number(response.previousUpdateID || 0))) { + if (requestsToIgnoreLastUpdateID.includes(request.command) || !OnyxUpdates.doesClientNeedToBeUpdated(Number(response?.previousUpdateID ?? 0))) { return OnyxUpdates.apply(responseToApply); } @@ -54,6 +49,5 @@ function SaveResponseInOnyx(requestResponse, request) { shouldPauseQueue: true, }); }); -} export default SaveResponseInOnyx; diff --git a/src/libs/Middleware/index.js b/src/libs/Middleware/index.ts similarity index 100% rename from src/libs/Middleware/index.js rename to src/libs/Middleware/index.ts diff --git a/src/libs/Middleware/types.ts b/src/libs/Middleware/types.ts new file mode 100644 index 000000000000..ece210ffe2af --- /dev/null +++ b/src/libs/Middleware/types.ts @@ -0,0 +1,6 @@ +import Request from '../../types/onyx/Request'; +import Response from '../../types/onyx/Response'; + +type Middleware = (response: Promise, request: Request, isFromSequentialQueue: boolean) => Promise; + +export default Middleware; diff --git a/src/libs/Network/MainQueue.js b/src/libs/Network/MainQueue.ts similarity index 71% rename from src/libs/Network/MainQueue.js rename to src/libs/Network/MainQueue.ts index 5b5b928d3284..5f069e2d0ed4 100644 --- a/src/libs/Network/MainQueue.js +++ b/src/libs/Network/MainQueue.ts @@ -1,42 +1,28 @@ -import _ from 'underscore'; -import lodashGet from 'lodash/get'; import * as NetworkStore from './NetworkStore'; import * as SequentialQueue from './SequentialQueue'; import * as Request from '../Request'; +import OnyxRequest from '../../types/onyx/Request'; // Queue for network requests so we don't lose actions done by the user while offline -let networkRequestQueue = []; +let networkRequestQueue: OnyxRequest[] = []; /** * Checks to see if a request can be made. - * - * @param {Object} request - * @param {String} request.type - * @param {String} request.command - * @param {Object} [request.data] - * @param {Boolean} request.data.forceNetworkRequest - * @return {Boolean} */ -function canMakeRequest(request) { +function canMakeRequest(request: OnyxRequest): boolean { // Some requests are always made even when we are in the process of authenticating (typically because they require no authToken e.g. Log, BeginSignIn) // However, if we are in the process of authenticating we always want to queue requests until we are no longer authenticating. - return request.data.forceNetworkRequest === true || (!NetworkStore.isAuthenticating() && !SequentialQueue.isRunning()); + return request.data?.forceNetworkRequest === true || (!NetworkStore.isAuthenticating() && !SequentialQueue.isRunning()); } -/** - * @param {Object} request - */ -function push(request) { +function push(request: OnyxRequest) { networkRequestQueue.push(request); } -/** - * @param {Object} request - */ -function replay(request) { +function replay(request: OnyxRequest) { push(request); - // eslint-disable-next-line no-use-before-define + // eslint-disable-next-line @typescript-eslint/no-use-before-define process(); } @@ -57,12 +43,12 @@ function process() { // - we are in the process of authenticating and the request is retryable (most are) // - the request does not have forceNetworkRequest === true (this will trigger it to process immediately) // - the request does not have shouldRetry === false (specified when we do not want to retry, defaults to true) - const requestsToProcessOnNextRun = []; + const requestsToProcessOnNextRun: OnyxRequest[] = []; - _.each(networkRequestQueue, (queuedRequest) => { + networkRequestQueue.forEach((queuedRequest) => { // Check if we can make this request at all and if we can't see if we should save it for the next run or chuck it into the ether if (!canMakeRequest(queuedRequest)) { - const shouldRetry = lodashGet(queuedRequest, 'data.shouldRetry'); + const shouldRetry = queuedRequest?.data?.shouldRetry; if (shouldRetry) { requestsToProcessOnNextRun.push(queuedRequest); } else { @@ -84,13 +70,10 @@ function process() { * Non-cancellable requests like Log would not be cleared */ function clear() { - networkRequestQueue = _.filter(networkRequestQueue, (request) => !request.data.canCancel); + networkRequestQueue = networkRequestQueue.filter((request) => !request.data?.canCancel); } -/** - * @returns {Array} - */ -function getAll() { +function getAll(): OnyxRequest[] { return networkRequestQueue; } diff --git a/src/libs/Network/NetworkStore.js b/src/libs/Network/NetworkStore.ts similarity index 61% rename from src/libs/Network/NetworkStore.js rename to src/libs/Network/NetworkStore.ts index 5ab46a4d65fa..0910031bdb63 100644 --- a/src/libs/Network/NetworkStore.js +++ b/src/libs/Network/NetworkStore.ts @@ -1,32 +1,28 @@ -import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; -import _ from 'underscore'; import ONYXKEYS from '../../ONYXKEYS'; +import Credentials from '../../types/onyx/Credentials'; -let credentials; -let authToken; -let supportAuthToken; -let currentUserEmail; +let credentials: Credentials | null = null; +let authToken: string | null = null; +let supportAuthToken: string | null = null; +let currentUserEmail: string | null = null; let offline = false; let authenticating = false; // Allow code that is outside of the network listen for when a reconnection happens so that it can execute any side-effects (like flushing the sequential network queue) -let reconnectCallback; +let reconnectCallback: () => void; function triggerReconnectCallback() { - if (!_.isFunction(reconnectCallback)) { + if (typeof reconnectCallback !== 'function') { return; } return reconnectCallback(); } -/** - * @param {Function} callbackFunction - */ -function onReconnection(callbackFunction) { +function onReconnection(callbackFunction: () => void) { reconnectCallback = callbackFunction; } -let resolveIsReadyPromise; +let resolveIsReadyPromise: (args?: unknown[]) => void; let isReadyPromise = new Promise((resolve) => { resolveIsReadyPromise = resolve; }); @@ -36,7 +32,7 @@ let isReadyPromise = new Promise((resolve) => { * 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 checkRequiredData() { - if (_.isUndefined(authToken) || _.isUndefined(credentials)) { + if (authToken === undefined || credentials === undefined) { return; } @@ -53,9 +49,9 @@ function resetHasReadRequiredDataFromStorage() { Onyx.connect({ key: ONYXKEYS.SESSION, callback: (val) => { - authToken = lodashGet(val, 'authToken', null); - supportAuthToken = lodashGet(val, 'supportAuthToken', null); - currentUserEmail = lodashGet(val, 'email', null); + authToken = val?.authToken ?? null; + supportAuthToken = val?.supportAuthToken ?? null; + currentUserEmail = val?.email ?? null; checkRequiredData(); }, }); @@ -63,7 +59,7 @@ Onyx.connect({ Onyx.connect({ key: ONYXKEYS.CREDENTIALS, callback: (val) => { - credentials = val || {}; + credentials = val; checkRequiredData(); }, }); @@ -82,85 +78,51 @@ Onyx.connect({ triggerReconnectCallback(); } - offline = Boolean(network.shouldForceOffline) || network.isOffline; + offline = Boolean(network.shouldForceOffline) || !!network.isOffline; }, }); -/** - * @returns {Object} - */ -function getCredentials() { +function getCredentials(): Credentials | null { return credentials; } -/** - * @returns {Boolean} - */ -function isOffline() { +function isOffline(): boolean { return offline; } -/** - * @returns {String} - */ -function getAuthToken() { +function getAuthToken(): string | null { return authToken; } -/** - * @param {String} command - * @returns {[String]} - */ -function isSupportRequest(command) { - return _.contains(['OpenApp', 'ReconnectApp', 'OpenReport'], command); +function isSupportRequest(command: string): boolean { + return ['OpenApp', 'ReconnectApp', 'OpenReport'].includes(command); } -/** - * @returns {String} - */ -function getSupportAuthToken() { +function getSupportAuthToken(): string | null { return supportAuthToken; } -/** - * @param {String} newSupportAuthToken - */ -function setSupportAuthToken(newSupportAuthToken) { +function setSupportAuthToken(newSupportAuthToken: string) { supportAuthToken = newSupportAuthToken; } -/** - * @param {String} newAuthToken - */ -function setAuthToken(newAuthToken) { +function setAuthToken(newAuthToken: string | null) { authToken = newAuthToken; } -/** - * @returns {String} - */ -function getCurrentUserEmail() { +function getCurrentUserEmail(): string | null { return currentUserEmail; } -/** - * @returns {Promise} - */ -function hasReadRequiredDataFromStorage() { +function hasReadRequiredDataFromStorage(): Promise { return isReadyPromise; } -/** - * @returns {Boolean} - */ -function isAuthenticating() { +function isAuthenticating(): boolean { return authenticating; } -/** - * @param {Boolean} val - */ -function setIsAuthenticating(val) { +function setIsAuthenticating(val: boolean) { authenticating = val; } diff --git a/src/libs/Network/SequentialQueue.js b/src/libs/Network/SequentialQueue.ts similarity index 90% rename from src/libs/Network/SequentialQueue.js rename to src/libs/Network/SequentialQueue.ts index 5c74f791e073..980bbc0efc44 100644 --- a/src/libs/Network/SequentialQueue.js +++ b/src/libs/Network/SequentialQueue.ts @@ -1,4 +1,3 @@ -import _ from 'underscore'; import Onyx from 'react-native-onyx'; import * as PersistedRequests from '../actions/PersistedRequests'; import * as NetworkStore from './NetworkStore'; @@ -8,17 +7,18 @@ import * as Request from '../Request'; import * as RequestThrottle from '../RequestThrottle'; import CONST from '../../CONST'; import * as QueuedOnyxUpdates from '../actions/QueuedOnyxUpdates'; +import OnyxRequest from '../../types/onyx/Request'; -let resolveIsReadyPromise; +let resolveIsReadyPromise: ((args?: unknown[]) => void) | undefined; let isReadyPromise = new Promise((resolve) => { resolveIsReadyPromise = resolve; }); // Resolve the isReadyPromise immediately so that the queue starts working as soon as the page loads -resolveIsReadyPromise(); +resolveIsReadyPromise?.(); let isSequentialQueueRunning = false; -let currentRequest = null; +let currentRequest: Promise | null = null; let isQueuePaused = false; /** @@ -52,16 +52,15 @@ function flushOnyxUpdatesQueue() { * is successfully returned. The first time a request fails we set a random, small, initial wait time. After waiting, we retry the request. If there are subsequent failures the request wait * time is doubled creating an exponential back off in the frequency of requests hitting the server. Since the initial wait time is random and it increases exponentially, the load of * requests to our backend is evenly distributed and it gradually decreases with time, which helps the servers catch up. - * @returns {Promise} */ -function process() { +function process(): Promise { // When the queue is paused, return early. This prevents any new requests from happening. The queue will be flushed again when the queue is unpaused. if (isQueuePaused) { return Promise.resolve(); } const persistedRequests = PersistedRequests.getAll(); - if (_.isEmpty(persistedRequests) || NetworkStore.isOffline()) { + if (persistedRequests.length === 0 || NetworkStore.isOffline()) { return Promise.resolve(); } const requestToProcess = persistedRequests[0]; @@ -71,7 +70,7 @@ function process() { .then((response) => { // A response might indicate that the queue should be paused. This happens when a gap in onyx updates is detected between the client and the server and // that gap needs resolved before the queue can continue. - if (response.shouldPauseQueue) { + if (response?.shouldPauseQueue) { pause(); } PersistedRequests.remove(requestToProcess); @@ -89,12 +88,13 @@ function process() { return RequestThrottle.sleep() .then(process) .catch(() => { - Onyx.update(requestToProcess.failureData); + Onyx.update(requestToProcess.failureData ?? []); PersistedRequests.remove(requestToProcess); RequestThrottle.clear(); return process(); }); }); + return currentRequest; } @@ -104,7 +104,7 @@ function flush() { return; } - if (isSequentialQueueRunning || _.isEmpty(PersistedRequests.getAll())) { + if (isSequentialQueueRunning || PersistedRequests.getAll().length === 0) { return; } @@ -128,7 +128,7 @@ function flush() { Onyx.disconnect(connectionID); process().finally(() => { isSequentialQueueRunning = false; - resolveIsReadyPromise(); + resolveIsReadyPromise?.(); currentRequest = null; flushOnyxUpdatesQueue(); }); @@ -151,20 +151,14 @@ function unpause() { flush(); } -/** - * @returns {Boolean} - */ -function isRunning() { +function isRunning(): boolean { return isSequentialQueueRunning; } // Flush the queue when the connection resumes NetworkStore.onReconnection(flush); -/** - * @param {Object} request - */ -function push(request) { +function push(request: OnyxRequest) { // Add request to Persisted Requests so that it can be retried if it fails PersistedRequests.save([request]); @@ -182,10 +176,7 @@ function push(request) { flush(); } -/** - * @returns {Promise} - */ -function getCurrentRequest() { +function getCurrentRequest(): OnyxRequest | Promise { if (currentRequest === null) { return Promise.resolve(); } @@ -194,9 +185,8 @@ function getCurrentRequest() { /** * Returns a promise that resolves when the sequential queue is done processing all persisted write requests. - * @returns {Promise} */ -function waitForIdle() { +function waitForIdle(): Promise { return isReadyPromise; } diff --git a/src/libs/Network/enhanceParameters.js b/src/libs/Network/enhanceParameters.ts similarity index 72% rename from src/libs/Network/enhanceParameters.js rename to src/libs/Network/enhanceParameters.ts index 778be881cb98..54d72a7c6c99 100644 --- a/src/libs/Network/enhanceParameters.js +++ b/src/libs/Network/enhanceParameters.ts @@ -1,27 +1,18 @@ -import lodashGet from 'lodash/get'; -import _ from 'underscore'; import CONFIG from '../../CONFIG'; import getPlatform from '../getPlatform'; import * as NetworkStore from './NetworkStore'; /** * Does this command require an authToken? - * - * @param {String} command - * @return {Boolean} */ -function isAuthTokenRequired(command) { - return !_.contains(['Log', 'Authenticate', 'BeginSignIn', 'SetPassword'], command); +function isAuthTokenRequired(command: string): boolean { + return !['Log', 'Authenticate', 'BeginSignIn', 'SetPassword'].includes(command); } /** * Adds default values to our request data - * - * @param {String} command - * @param {Object} parameters - * @returns {Object} */ -export default function enhanceParameters(command, parameters) { +export default function enhanceParameters(command: string, parameters: Record): Record { const finalParameters = {...parameters}; if (isAuthTokenRequired(command)) { @@ -44,7 +35,7 @@ export default function enhanceParameters(command, parameters) { finalParameters.api_setCookie = false; // Include current user's email in every request and the server logs - finalParameters.email = lodashGet(parameters, 'email', NetworkStore.getCurrentUserEmail()); + finalParameters.email = parameters.email ?? NetworkStore.getCurrentUserEmail(); return finalParameters; } diff --git a/src/libs/Network/index.js b/src/libs/Network/index.ts similarity index 77% rename from src/libs/Network/index.js rename to src/libs/Network/index.ts index 2f5dc9460e60..bf38bc33e95a 100644 --- a/src/libs/Network/index.js +++ b/src/libs/Network/index.ts @@ -1,9 +1,10 @@ -import lodashGet from 'lodash/get'; import * as ActiveClientManager from '../ActiveClientManager'; import CONST from '../../CONST'; import * as MainQueue from './MainQueue'; import * as SequentialQueue from './SequentialQueue'; import pkg from '../../../package.json'; +import {Request} from '../../types/onyx'; +import Response from '../../types/onyx/Response'; // We must wait until the ActiveClientManager is ready so that we ensure only the "leader" tab processes any persisted requests ActiveClientManager.isReady().then(() => { @@ -15,16 +16,10 @@ ActiveClientManager.isReady().then(() => { /** * Perform a queued post request - * - * @param {String} command - * @param {*} [data] - * @param {String} [type] - * @param {Boolean} [shouldUseSecure] - Whether we should use the secure API - * @returns {Promise} */ -function post(command, data = {}, type = CONST.NETWORK.METHOD.POST, shouldUseSecure = false) { +function post(command: string, data: Record = {}, type = CONST.NETWORK.METHOD.POST, shouldUseSecure = false): Promise { return new Promise((resolve, reject) => { - const request = { + const request: Request = { command, data, type, @@ -35,8 +30,8 @@ function post(command, data = {}, type = CONST.NETWORK.METHOD.POST, shouldUseSec // (e.g. any requests currently happening when the user logs out are cancelled) request.data = { ...data, - shouldRetry: lodashGet(data, 'shouldRetry', true), - canCancel: lodashGet(data, 'canCancel', true), + shouldRetry: data?.shouldRetry ?? true, + canCancel: data?.canCancel ?? true, appversion: pkg.version, }; @@ -50,7 +45,7 @@ function post(command, data = {}, type = CONST.NETWORK.METHOD.POST, shouldUseSec // This check is mainly used to prevent API commands from triggering calls to MainQueue.process() from inside the context of a previous // call to MainQueue.process() e.g. calling a Log command without this would cause the requests in mainQueue to double process // since we call Log inside MainQueue.process(). - const shouldProcessImmediately = lodashGet(request, 'data.shouldProcessImmediately', true); + const shouldProcessImmediately = request?.data?.shouldProcessImmediately ?? true; if (!shouldProcessImmediately) { return; } diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index fb6c6e4f493e..2e53d15c23aa 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -522,7 +522,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); - result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result); + result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result); if (!hasMultipleParticipants) { result.login = personalDetail.login; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c04c71fbd625..011907c2c88b 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1283,7 +1283,7 @@ function hasNonReimbursableTransactions(iouReportID) { * @param {Object} allReportsDict * @returns {Number} */ -function getMoneyRequestTotal(report, allReportsDict = null) { +function getMoneyRequestReimbursableTotal(report, allReportsDict = null) { const allAvailableReports = allReportsDict || allReports; let moneyRequestReport; if (isMoneyRequestReport(report)) { @@ -1294,7 +1294,6 @@ function getMoneyRequestTotal(report, allReportsDict = null) { } if (moneyRequestReport) { const total = lodashGet(moneyRequestReport, 'total', 0); - if (total !== 0) { // There is a possibility that if the Expense report has a negative total. // This is because there are instances where you can get a credit back on your card, @@ -1305,6 +1304,45 @@ function getMoneyRequestTotal(report, allReportsDict = null) { return 0; } +/** + * @param {Object} report + * @param {Object} allReportsDict + * @returns {Object} + */ +function getMoneyRequestSpendBreakdown(report, allReportsDict = null) { + const allAvailableReports = allReportsDict || allReports; + let moneyRequestReport; + if (isMoneyRequestReport(report)) { + moneyRequestReport = report; + } + if (allAvailableReports && report.hasOutstandingIOU && report.iouReportID) { + moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`]; + } + if (moneyRequestReport) { + let nonReimbursableSpend = lodashGet(moneyRequestReport, 'nonReimbursableTotal', 0); + let reimbursableSpend = lodashGet(moneyRequestReport, 'total', 0); + + if (nonReimbursableSpend + reimbursableSpend !== 0) { + // There is a possibility that if the Expense report has a negative total. + // This is because there are instances where you can get a credit back on your card, + // or you enter a negative expense to “offset” future expenses + nonReimbursableSpend = isExpenseReport(moneyRequestReport) ? nonReimbursableSpend * -1 : Math.abs(nonReimbursableSpend); + reimbursableSpend = isExpenseReport(moneyRequestReport) ? reimbursableSpend * -1 : Math.abs(reimbursableSpend); + const totalDisplaySpend = nonReimbursableSpend + reimbursableSpend; + return { + nonReimbursableSpend, + reimbursableSpend, + totalDisplaySpend, + }; + } + } + return { + nonReimbursableSpend: 0, + reimbursableSpend: 0, + totalDisplaySpend: 0, + }; +} + /** * Get the title for a policy expense chat which depends on the role of the policy member seeing this report * @@ -1344,7 +1382,7 @@ function getPolicyExpenseChatName(report, policy = undefined) { * @returns {String} */ function getMoneyRequestReportName(report, policy = undefined) { - const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(report), report.currency); + const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(report), report.currency); const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID); const payerPaidAmountMesssage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerName, @@ -1550,7 +1588,7 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment}); } - const totalAmount = getMoneyRequestTotal(report); + const totalAmount = getMoneyRequestReimbursableTotal(report); const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerID, true); const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency); @@ -2219,7 +2257,7 @@ function buildOptimisticExpenseReport(chatReportID, policyID, payeeAccountID, to function getIOUReportActionMessage(iouReportID, type, total, comment, currency, paymentType = '', isSettlingUp = false) { const amount = type === CONST.IOU.REPORT_ACTION_TYPE.PAY - ? CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(getReport(iouReportID)), currency) + ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(getReport(iouReportID)), currency) : CurrencyUtils.convertToDisplayString(total, currency); let paymentMethodMessage; @@ -3912,7 +3950,8 @@ export { hasExpensifyGuidesEmails, isWaitingForIOUActionFromCurrentUser, isIOUOwnedByCurrentUser, - getMoneyRequestTotal, + getMoneyRequestReimbursableTotal, + getMoneyRequestSpendBreakdown, canShowReportRecipientLocalTime, formatReportLastMessageText, chatIncludesConcierge, diff --git a/src/libs/Request.ts b/src/libs/Request.ts index 903e70358da9..9c4af4aa7e18 100644 --- a/src/libs/Request.ts +++ b/src/libs/Request.ts @@ -3,24 +3,24 @@ import enhanceParameters from './Network/enhanceParameters'; import * as NetworkStore from './Network/NetworkStore'; import Request from '../types/onyx/Request'; import Response from '../types/onyx/Response'; - -type Middleware = (response: Promise, request: Request, isFromSequentialQueue: boolean) => Promise; +import Middleware from './Middleware/types'; let middlewares: Middleware[] = []; -function makeXHR(request: Request): Promise { +function makeXHR(request: Request): Promise { const finalParameters = enhanceParameters(request.command, request?.data ?? {}); - return NetworkStore.hasReadRequiredDataFromStorage().then(() => { + return NetworkStore.hasReadRequiredDataFromStorage().then((): Promise => { // If we're using the Supportal token and this is not a Supportal request // let's just return a promise that will resolve itself. if (NetworkStore.getSupportAuthToken() && !NetworkStore.isSupportRequest(request.command)) { 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) as Promise; + }); } -function processWithMiddleware(request: Request, isFromSequentialQueue = false): Promise { +function processWithMiddleware(request: Request, isFromSequentialQueue = false): Promise { return middlewares.reduce((last, middleware) => middleware(last, request, isFromSequentialQueue), makeXHR(request)); } diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 314a1d63760e..dd6db33902fb 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -158,7 +158,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p report.displayName = ReportUtils.getReportName(report); // eslint-disable-next-line no-param-reassign - report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReportsDict); + report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReportsDict); }); // The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order: @@ -384,7 +384,7 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); - result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result); + result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result); if (!hasMultipleParticipants) { result.accountID = personalDetail.accountID; diff --git a/src/libs/actions/Chronos.ts b/src/libs/actions/Chronos.ts index 1b46a68a1afe..ce821e524722 100644 --- a/src/libs/actions/Chronos.ts +++ b/src/libs/actions/Chronos.ts @@ -1,11 +1,11 @@ -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxUpdate} from 'react-native-onyx'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; import {ChronosOOOEvent} from '../../types/onyx/OriginalMessage'; const removeEvent = (reportID: string, reportActionID: string, eventID: string, events: ChronosOOOEvent[]) => { - const optimisticData = [ + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, @@ -20,7 +20,7 @@ const removeEvent = (reportID: string, reportActionID: string, eventID: string, }, ]; - const successData = [ + const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, @@ -32,7 +32,7 @@ const removeEvent = (reportID: string, reportActionID: string, eventID: string, }, ]; - const failureData = [ + const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index dbe3eb27438e..c03335959e71 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -3,6 +3,7 @@ import {Linking} from 'react-native'; import {ValueOf} from 'type-fest'; import throttle from 'lodash/throttle'; import {ChannelAuthorizationCallback} from 'pusher-js/with-encryption'; +import {ChannelAuthorizationData} from 'pusher-js/types/src/core/auth/options'; import clearCache from './clearCache'; import ONYXKEYS from '../../../ONYXKEYS'; import redirectToSignIn from '../SignInRedirect'; @@ -641,7 +642,7 @@ function authenticatePusher(socketID: string, channelName: string, callback: Cha // eslint-disable-next-line rulesdir/no-api-side-effects-method API.makeRequestWithSideEffects('AuthenticatePusher', params) .then((response) => { - if (response.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) { + if (response?.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) { Log.hmmm('[PusherAuthorizer] Unable to authenticate Pusher because authToken is expired'); callback(new Error('Pusher failed to authenticate because authToken is expired'), {auth: ''}); @@ -650,14 +651,14 @@ function authenticatePusher(socketID: string, channelName: string, callback: Cha return; } - if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { + if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) { Log.hmmm('[PusherAuthorizer] Unable to authenticate Pusher for reason other than expired session'); - callback(new Error(`Pusher failed to authenticate because code: ${response.jsonCode} message: ${response.message}`), {auth: ''}); + callback(new Error(`Pusher failed to authenticate because code: ${response?.jsonCode} message: ${response?.message}`), {auth: ''}); return; } Log.info('[PusherAuthorizer] Pusher authenticated successfully', false, {channelName}); - callback(null, response); + callback(null, response as ChannelAuthorizationData); }) .catch((error) => { Log.hmmm('[PusherAuthorizer] Unhandled error: ', {channelName, error}); diff --git a/src/libs/actions/Session/updateSessionAuthTokens.ts b/src/libs/actions/Session/updateSessionAuthTokens.ts index da547eba0504..9614face2070 100644 --- a/src/libs/actions/Session/updateSessionAuthTokens.ts +++ b/src/libs/actions/Session/updateSessionAuthTokens.ts @@ -1,6 +1,6 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../../ONYXKEYS'; -export default function updateSessionAuthTokens(authToken: string, encryptedAuthToken: string) { +export default function updateSessionAuthTokens(authToken?: string, encryptedAuthToken?: string) { Onyx.merge(ONYXKEYS.SESSION, {authToken, encryptedAuthToken}); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index d0e84499a443..f5ca7080249c 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -730,6 +730,7 @@ export default compose( prevProps.report.managerEmail === nextProps.report.managerEmail && prevProps.shouldHideThreadDividerLine === nextProps.shouldHideThreadDividerLine && lodashGet(prevProps.report, 'total', 0) === lodashGet(nextProps.report, 'total', 0) && + lodashGet(prevProps.report, 'nonReimbursableTotal', 0) === lodashGet(nextProps.report, 'nonReimbursableTotal', 0) && prevProps.linkedReportActionID === nextProps.linkedReportActionID, ), ); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f58c6644cd47..a3671faf194c 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -278,6 +278,10 @@ function arePropsEqual(oldProps, newProps) { return false; } + if (lodashGet(newProps, 'report.nonReimbursableTotal') !== lodashGet(oldProps, 'report.nonReimbursableTotal')) { + return false; + } + if (lodashGet(newProps, 'report.writeCapability') !== lodashGet(oldProps, 'report.writeCapability')) { return false; } diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 9dbdde14c50d..394f6c5ddc5a 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -141,6 +141,7 @@ const chatReportSelector = (report) => lastVisibleActionCreated: report.lastVisibleActionCreated, iouReportID: report.iouReportID, total: report.total, + nonReimbursableTotal: report.nonReimbursableTotal, hasOutstandingIOU: report.hasOutstandingIOU, isWaitingOnBankAccount: report.isWaitingOnBankAccount, statusNum: report.statusNum, diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.js b/src/pages/workspace/WorkspaceInviteMessagePage.js index 65e60fda119d..5e7efadd3778 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.js +++ b/src/pages/workspace/WorkspaceInviteMessagePage.js @@ -82,7 +82,7 @@ class WorkspaceInviteMessagePage extends React.Component { componentDidMount() { if (_.isEmpty(this.props.invitedEmailsToAccountIDsDraft)) { - Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(this.props.route.params.policyID), true); + Navigation.goBack(ROUTES.WORKSPACE_INVITE.getRoute(this.props.route.params.policyID), true); return; } this.focusWelcomeMessageInput(); diff --git a/src/types/onyx/Credentials.ts b/src/types/onyx/Credentials.ts index f6a9ce669ad0..13404a9a6643 100644 --- a/src/types/onyx/Credentials.ts +++ b/src/types/onyx/Credentials.ts @@ -10,6 +10,9 @@ type Credentials = { autoGeneratedLogin?: string; autoGeneratedPassword?: string; accountID?: number; + + partnerUserID?: string; + partnerUserSecret?: string; }; export default Credentials; diff --git a/src/types/onyx/Request.ts b/src/types/onyx/Request.ts index a3032401b346..836138ca99ba 100644 --- a/src/types/onyx/Request.ts +++ b/src/types/onyx/Request.ts @@ -1,4 +1,5 @@ import {OnyxUpdate} from 'react-native-onyx'; +import Response from './Response'; type OnyxData = { successData?: OnyxUpdate[]; @@ -8,9 +9,15 @@ type OnyxData = { type RequestData = { command: string; + commandName?: string; data?: Record; type?: string; shouldUseSecure?: boolean; + successData?: OnyxUpdate[]; + failureData?: OnyxUpdate[]; + + resolve?: (value: Response) => void; + reject?: (value?: unknown) => void; }; type Request = RequestData & OnyxData; diff --git a/src/types/onyx/Response.ts b/src/types/onyx/Response.ts index 255ac6d9bae4..3d834d0bcb2b 100644 --- a/src/types/onyx/Response.ts +++ b/src/types/onyx/Response.ts @@ -6,6 +6,9 @@ type Response = { jsonCode?: number | string; onyxData?: OnyxUpdate[]; requestID?: string; + shouldPauseQueue?: boolean; + authToken?: string; + encryptedAuthToken?: string; message?: string; }; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 292addbb142e..9da3ef610571 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -7,6 +7,7 @@ type WaypointCollection = Record; type Comment = { comment?: string; waypoints?: WaypointCollection; + isLoading?: boolean; type?: string; customUnit?: Record; source?: string;