diff --git a/src/CONST.js b/src/CONST.js index de79fdcb71cc..89bc4303f4f2 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -229,9 +229,6 @@ const CONST = { REMOVED_FROM_POLICY: 'removedFromPolicy', POLICY_DELETED: 'policyDeleted', }, - ERROR: { - INACCESSIBLE_REPORT: 'Report not found', - }, MESSAGE: { TYPE: { COMMENT: 'COMMENT', @@ -329,6 +326,7 @@ const CONST = { JSON_CODE: { SUCCESS: 200, NOT_AUTHENTICATED: 407, + REQUEST_FAILED: 0, }, NVP: { IS_FIRST_TIME_NEW_EXPENSIFY_USER: 'isFirstTimeNewExpensifyUser', diff --git a/src/libs/API.js b/src/libs/API.js index 255efbf221bd..2a09f65d0f6b 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -12,6 +12,7 @@ import updateSessionAuthTokens from './actions/Session/updateSessionAuthTokens'; import * as NetworkStore from './Network/NetworkStore'; import enhanceParameters from './Network/enhanceParameters'; import * as NetworkEvents from './Network/NetworkEvents'; +import * as ErrorUtils from './ErrorUtils'; /** * Function used to handle expired auth tokens. It re-authenticates with the API and @@ -34,17 +35,16 @@ function handleExpiredAuthToken(originalCommand, originalParameters, originalTyp // eslint-disable-next-line no-use-before-define return reauthenticate(originalCommand) - .then(() => { - // Now that the API is authenticated, make the original request again with the new authToken - const params = enhanceParameters(originalCommand, originalParameters); - return Network.post(originalCommand, params, originalType); - }) - .catch(() => ( + .then((response) => { + if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { + // Now that the API is authenticated, make the original request again with the new authToken + const params = enhanceParameters(originalCommand, originalParameters); + return Network.post(originalCommand, params, originalType); + } - // If the request did not succeed because of a networking issue or the server did not respond requeue the - // original request. - Network.post(originalCommand, originalParameters, originalType) - )); + // If the request did not succeed because of a networking issue or the server did not respond requeue the original request + return Network.post(originalCommand, originalParameters, originalType); + }); } // We set the logger for Network here so that we can avoid a circular dependency @@ -83,8 +83,7 @@ NetworkEvents.onResponse((queuedRequest, response) => { } handleExpiredAuthToken(queuedRequest.command, queuedRequest.data, queuedRequest.type) - .then(queuedRequest.resolve || (() => Promise.resolve())) - .catch(queuedRequest.reject || (() => Promise.resolve())); + .then(queuedRequest.resolve || (() => Promise.resolve())); return; } @@ -137,39 +136,7 @@ function Authenticate(parameters) { // Add email param so the first Authenticate request is logged on the server w/ this email email: parameters.email, - }) - .then((response) => { - // If we didn't get a 200 response from Authenticate we either failed to Authenticate with - // an expensify login or the login credentials we created after the initial authentication. - // In both cases, we need the user to sign in again with their expensify credentials - if (response.jsonCode !== 200) { - switch (response.jsonCode) { - case 401: - throw new Error('passwordForm.error.incorrectLoginOrPassword'); - case 402: - // If too few characters are passed as the password, the WAF will pass it to the API as an empty - // string, which results in a 402 error from Auth. - if (response.message === '402 Missing partnerUserSecret') { - throw new Error('passwordForm.error.incorrectLoginOrPassword'); - } - throw new Error('passwordForm.error.twoFactorAuthenticationEnabled'); - case 403: - if (response.message === 'Invalid code') { - throw new Error('passwordForm.error.incorrect2fa'); - } - throw new Error('passwordForm.error.invalidLoginOrPassword'); - case 404: - throw new Error('passwordForm.error.unableToResetPassword'); - case 405: - throw new Error('passwordForm.error.noAccess'); - case 413: - throw new Error('passwordForm.error.accountLocked'); - default: - throw new Error('passwordForm.error.fallback'); - } - } - return response; - }); + }); } /** @@ -188,10 +155,24 @@ function reauthenticate(command = '') { partnerUserSecret: credentials.autoGeneratedPassword, }) .then((response) => { - // If authentication fails throw so that we hit - // the catch below and redirect to sign in - if (response.jsonCode !== 200) { - throw new Error(response.message); + NetworkStore.setIsAuthenticating(false); + + if (response.jsonCode !== CONST.JSON_CODE.SUCCESS) { + // If the reauthenticate request failed in flight then don't log the user out. We only want to do this if the API returns a + // non 200 response code and the user will not be able to authenticate with the stored credentials. + if (response.jsonCode === CONST.JSON_CODE.REQUEST_FAILED) { + return response; + } + + const errorTranslationKey = ErrorUtils.getErrorKeyFromAuthenticateResponse(response); + + // If we experience something other than a network error then redirect the user to sign in + redirectToSignIn(errorTranslationKey); + + Log.hmmm('Redirecting to Sign In because we failed to reauthenticate', { + command, + error: response.message, + }); } // Update authToken in Onyx and in our local variables so that API requests will use the @@ -202,30 +183,7 @@ function reauthenticate(command = '') { // 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 - NetworkStore.setIsAuthenticating(false); - }) - - .catch((error) => { - // If authentication fails, then the network can be unpaused - NetworkStore.setIsAuthenticating(false); - - // When a fetch() fails and the "API is offline" error is thrown we won't log the user out. Most likely they - // have a spotty connection and will need to try to reauthenticate when they come back online. We will - // re-throw this error so it can be handled by callers of reauthenticate(). - if (error.message === CONST.ERROR.API_OFFLINE) { - throw error; - } - - // If we experience something other than a network error then redirect the user to sign in - redirectToSignIn(error.message); - - Log.hmmm('Redirecting to Sign In because we failed to reauthenticate', { - command, - error: error.message, - }); + return response; }); } diff --git a/src/libs/ErrorUtils.js b/src/libs/ErrorUtils.js new file mode 100644 index 000000000000..ddcb5d490c68 --- /dev/null +++ b/src/libs/ErrorUtils.js @@ -0,0 +1,44 @@ +import CONST from '../CONST'; + +/** + * @param {Object} response + * @returns {String} + */ +function getErrorKeyFromAuthenticateResponse(response) { + // If we didn't get a 200 response from Authenticate we either failed to Authenticate with + // an expensify login or the login credentials we created after the initial authentication. + // In both cases, we need the user to sign in again with their expensify credentials + if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { + return ''; + } + + switch (response.jsonCode) { + case 401: + return 'passwordForm.error.incorrectLoginOrPassword'; + case 402: + // If too few characters are passed as the password, the WAF will pass it to the API as an empty + // string, which results in a 402 error from Auth. + if (response.message === '402 Missing partnerUserSecret') { + return 'passwordForm.error.incorrectLoginOrPassword'; + } + return 'passwordForm.error.twoFactorAuthenticationEnabled'; + case 403: + if (response.message === 'Invalid code') { + return 'passwordForm.error.incorrect2fa'; + } + return 'passwordForm.error.invalidLoginOrPassword'; + case 404: + return 'passwordForm.error.unableToResetPassword'; + case 405: + return 'passwordForm.error.noAccess'; + case 413: + return 'passwordForm.error.accountLocked'; + default: + return 'passwordForm.error.fallback'; + } +} + +export { + // eslint-disable-next-line import/prefer-default-export + getErrorKeyFromAuthenticateResponse, +}; diff --git a/src/libs/Network/PersistedRequestsQueue.js b/src/libs/Network/PersistedRequestsQueue.js index 3f89f2bc0bff..47d2376053b9 100644 --- a/src/libs/Network/PersistedRequestsQueue.js +++ b/src/libs/Network/PersistedRequestsQueue.js @@ -3,10 +3,10 @@ import Onyx from 'react-native-onyx'; import * as PersistedRequests from '../actions/PersistedRequests'; import * as NetworkStore from './NetworkStore'; import * as NetworkEvents from './NetworkEvents'; -import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; import * as ActiveClientManager from '../ActiveClientManager'; import processRequest from './processRequest'; +import CONST from '../../CONST'; let isPersistedRequestsQueueRunning = false; diff --git a/src/libs/Network/index.js b/src/libs/Network/index.js index d65ec7380cba..74c9b0fa66da 100644 --- a/src/libs/Network/index.js +++ b/src/libs/Network/index.js @@ -129,17 +129,19 @@ function processNetworkRequestQueue() { .catch((error) => { // Because we ran into an error we assume we might be offline and do a "connection" health test NetworkEvents.triggerRecheckNeeded(); + if (retryFailedRequest(queuedRequest, error)) { return; } if (queuedRequest.command !== 'Log') { - NetworkEvents.getLogger().hmmm('[Network] Handled error when making request', error); + NetworkEvents.getLogger().hmmm('[Network] Handled error when making request', {error, command: queuedRequest.command}); } else { console.debug('[Network] There was an error in the Log API command, unable to log to server!', error); } - queuedRequest.reject(new Error(CONST.ERROR.API_OFFLINE)); + // Resolve with a special client-side jsonCode so API method handlers can identify this scenario + NetworkEvents.triggerResponse(queuedRequest, {jsonCode: CONST.JSON_CODE.REQUEST_FAILED}); }); }); diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 47822a904208..ee06f385fed8 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -91,9 +91,6 @@ function addPersonalBankAccount(account, password, plaidLinkToken) { ReimbursementAccount.setBankAccountFormValidationErrors({password: true}); } Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); - }) - .catch(() => { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); }); } @@ -112,8 +109,6 @@ function deleteBankAccount(bankAccountID) { } else { Growl.show(Localize.translateLocal('common.genericErrorMessage'), CONST.GROWL.ERROR, 3000); } - }).catch(() => { - Growl.show(Localize.translateLocal('common.genericErrorMessage'), CONST.GROWL.ERROR, 3000); }); } diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index fe7405dfd388..d2f93ccc969d 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -28,9 +28,6 @@ function deleteDebitCard(fundID) { } else { Growl.show(Localize.translateLocal('common.genericErrorMessage'), CONST.GROWL.ERROR, 3000); } - }) - .catch(() => { - Growl.show(Localize.translateLocal('common.genericErrorMessage'), CONST.GROWL.ERROR, 3000); }); } @@ -211,14 +208,14 @@ function transferWalletBalance(paymentMethod) { API.TransferWalletBalance(parameters) .then((response) => { if (response.jsonCode !== 200) { - throw new Error(response.message); + Growl.error(Localize.translateLocal('transferAmountPage.failedTransfer')); + Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {loading: false}); + return; } + Onyx.merge(ONYXKEYS.USER_WALLET, {currentBalance: 0}); Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {shouldShowConfirmModal: true, loading: false}); Navigation.navigate(ROUTES.SETTINGS_PAYMENTS); - }).catch(() => { - Growl.error(Localize.translateLocal('transferAmountPage.failedTransfer')); - Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {loading: false}); }); } diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index c86abc9f8830..49deb6ce52f0 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -289,8 +289,6 @@ function setPersonalDetails(details, shouldGrowl) { } else if (response.jsonCode === 401) { Growl.error(Localize.translateLocal('personalDetails.error.lastNameLength'), 3000); } - }).catch((error) => { - console.debug('Error while setting personal details', error); }); } diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index b4939272b050..0e31bd6b3dee 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -380,19 +380,18 @@ function update(policyID, values, shouldGrowl = false) { API.UpdatePolicy({policyID, value: JSON.stringify(values), lastModified: null}) .then((policyResponse) => { if (policyResponse.jsonCode !== 200) { - throw new Error(); + updateLocalPolicyValues(policyID, {isPolicyUpdating: false}); + + // Show the user feedback + const errorMessage = Localize.translateLocal('workspace.editor.genericFailureMessage'); + Growl.error(errorMessage, 5000); + return; } updateLocalPolicyValues(policyID, {...values, isPolicyUpdating: false}); if (shouldGrowl) { Growl.show(Localize.translateLocal('workspace.common.growlMessageOnSave'), CONST.GROWL.SUCCESS, 3000); } - }).catch(() => { - updateLocalPolicyValues(policyID, {isPolicyUpdating: false}); - - // Show the user feedback - const errorMessage = Localize.translateLocal('workspace.editor.genericFailureMessage'); - Growl.error(errorMessage, 5000); }); } @@ -447,7 +446,9 @@ function setCustomUnit(policyID, values) { }) .then((response) => { if (response.jsonCode !== 200) { - throw new Error(); + // Show the user feedback + Growl.error(Localize.translateLocal('workspace.editor.genericFailureMessage'), 5000); + return; } updateLocalPolicyValues(policyID, { @@ -457,9 +458,6 @@ function setCustomUnit(policyID, values) { value: values.attributes.unit, }, }); - }).catch(() => { - // Show the user feedback - Growl.error(Localize.translateLocal('workspace.editor.genericFailureMessage'), 5000); }); } @@ -477,7 +475,9 @@ function setCustomUnitRate(policyID, customUnitID, values) { }) .then((response) => { if (response.jsonCode !== 200) { - throw new Error(); + // Show the user feedback + Growl.error(Localize.translateLocal('workspace.editor.genericFailureMessage'), 5000); + return; } updateLocalPolicyValues(policyID, { @@ -489,9 +489,6 @@ function setCustomUnitRate(policyID, customUnitID, values) { }, }, }); - }).catch(() => { - // Show the user feedback - Growl.error(Localize.translateLocal('workspace.editor.genericFailureMessage'), 5000); }); } diff --git a/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js b/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js index c3c713c512c6..c8ddbb89598e 100644 --- a/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js +++ b/src/libs/actions/ReimbursementAccount/setupWithdrawalAccount.js @@ -208,6 +208,12 @@ function setupWithdrawalAccount(params) { const updatedACHData = mergeParamsWithLocalACHData(params); API.BankAccount_SetupWithdrawal(updatedACHData) .then((response) => { + if (response.jsonCode === CONST.JSON_CODE.REQUEST_FAILED) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false, achData: {...updatedACHData}}); + errors.showBankAccountErrorModal(Localize.translateLocal('common.genericErrorMessage')); + return; + } + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {...updatedACHData}}); const currentStep = updatedACHData.currentStep; const responseACHData = lodashGet(response, 'achData', {}); @@ -253,11 +259,6 @@ function setupWithdrawalAccount(params) { navigation.goToWithdrawalAccountSetupStep(nextStep, responseACHData); } Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); - }) - .catch((response) => { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false, achData: {...updatedACHData}}); - console.error(response.stack); - errors.showBankAccountErrorModal(Localize.translateLocal('common.genericErrorMessage')); }); } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index eb01d496f6a3..375037e27767 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -297,13 +297,11 @@ function fetchIOUReport(iouReportID, chatReportID) { shouldLoadOptionalKeys: true, includePinnedReports: true, }).then((response) => { - if (!response) { - return; - } - if (response.jsonCode !== 200) { - console.error(response.message); + if (!response || response.jsonCode !== 200) { + Log.hmmm('[Report] Failed to populate IOU Collection:', response.message); return; } + const iouReportData = response.reports[iouReportID]; if (!iouReportData) { // IOU data for a report will be missing when the IOU report has already been paid. @@ -311,8 +309,6 @@ function fetchIOUReport(iouReportID, chatReportID) { return; } return getSimplifiedIOUReport(iouReportData, chatReportID); - }).catch((error) => { - Log.hmmm('[Report] Failed to populate IOU Collection:', error.message); }); } @@ -360,7 +356,9 @@ function fetchChatReportsByIDs(chatList, shouldRedirectIfInaccessible = false) { // If we receive a 404 response while fetching a single report, treat that report as inaccessible. if (jsonCode === 404 && shouldRedirectIfInaccessible) { - throw new Error(CONST.REPORT.ERROR.INACCESSIBLE_REPORT); + // eslint-disable-next-line no-use-before-define + handleInaccessibleReport(); + return []; } return Promise.all(_.map(fetchedReports, (chatReport) => { @@ -425,14 +423,6 @@ function fetchChatReportsByIDs(chatList, shouldRedirectIfInaccessible = false) { // Fetch the personal details if there are any PersonalDetails.getFromReportParticipants(_.values(simplifiedReports)); return simplifiedReports; - }) - .catch((err) => { - if (err.message !== CONST.REPORT.ERROR.INACCESSIBLE_REPORT) { - return; - } - - // eslint-disable-next-line no-use-before-define - handleInaccessibleReport(); }); } @@ -1421,7 +1411,11 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { reportComment: htmlForNewComment, sequenceNumber, }) - .catch(() => { + .then((response) => { + if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { + return; + } + // If it fails, reset Onyx actionToMerge[sequenceNumber] = originalReportAction; Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, actionToMerge); @@ -1517,9 +1511,10 @@ function createPolicyRoom(policyID, reportName, visibility) { return API.CreatePolicyRoom({policyID, reportName, visibility}) .then((response) => { if (response.jsonCode !== 200) { - Growl.error(response.message); + Growl.error(Localize.translateLocal('newRoomPage.growlMessageOnError')); return; } + return fetchChatReportsByIDs([response.reportID]); }) .then(([{reportID}]) => { @@ -1529,9 +1524,6 @@ function createPolicyRoom(policyID, reportName, visibility) { } Navigation.navigate(ROUTES.getReportRoute(reportID)); }) - .catch(() => { - Growl.error(Localize.translateLocal('newRoomPage.growlMessageOnError')); - }) .finally(() => Onyx.set(ONYXKEYS.IS_LOADING_CREATE_POLICY_ROOM, false)); } @@ -1545,17 +1537,15 @@ function renameReport(reportID, reportName) { API.RenameReport({reportID, reportName}) .then((response) => { if (response.jsonCode !== 200) { - Growl.error(response.message); + Growl.error(Localize.translateLocal('newRoomPage.growlMessageOnRenameError')); return; } + Growl.success(Localize.translateLocal('newRoomPage.policyRoomRenamed')); // Update the report name so that the LHN and header display the updated name Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {reportName}); }) - .catch(() => { - Growl.error(Localize.translateLocal('newRoomPage.growlMessageOnRenameError')); - }) .finally(() => Onyx.set(ONYXKEYS.IS_LOADING_RENAME_POLICY_ROOM, false)); } diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 719a0a7be7dc..c61e6c13bdd5 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -19,6 +19,7 @@ import * as Pusher from '../../Pusher/pusher'; import NetworkConnection from '../../NetworkConnection'; import * as User from '../User'; import * as ValidationUtils from '../../ValidationUtils'; +import * as ErrorUtils from '../../ErrorUtils'; let credentials = {}; Onyx.connect({ @@ -76,8 +77,7 @@ function signOut() { partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, shouldRetry: false, - }) - .catch(error => Onyx.merge(ONYXKEYS.SESSION, {error: error.message})); + }); } Onyx.set(ONYXKEYS.SESSION, null); Onyx.set(ONYXKEYS.CREDENTIALS, null); @@ -126,7 +126,7 @@ function fetchAccountDetails(login) { API.GetAccountStatus({email: login, forceNetworkRequest: true}) .then((response) => { - if (response.jsonCode === 200) { + if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { Onyx.merge(ONYXKEYS.CREDENTIALS, { login: response.normalizedLogin, }); @@ -145,6 +145,8 @@ function fetchAccountDetails(login) { } else if (!response.validated) { resendValidationLink(login); } + } else if (response.jsonCode === CONST.JSON_CODE.REQUEST_FAILED) { + Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal('session.offlineMessageRetry')}); } else if (response.jsonCode === 402) { Onyx.merge(ONYXKEYS.ACCOUNT, { error: ValidationUtils.isNumericWithSpecialChars(login) @@ -154,11 +156,7 @@ function fetchAccountDetails(login) { } else { Onyx.merge(ONYXKEYS.ACCOUNT, {error: response.message}); } - }) - .catch(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal('session.offlineMessageRetry')}); - }) - .finally(() => { + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); }); } @@ -189,7 +187,8 @@ function createTemporaryLogin(authToken, email) { }) .then((createLoginResponse) => { if (createLoginResponse.jsonCode !== 200) { - throw new Error(createLoginResponse.message); + Onyx.merge(ONYXKEYS.ACCOUNT, {error: createLoginResponse.message}); + return; } setSuccessfulSignInData(createLoginResponse); @@ -202,8 +201,7 @@ function createTemporaryLogin(authToken, email) { partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, shouldRetry: false, - }) - .catch(Log.info); + }); } Onyx.merge(ONYXKEYS.CREDENTIALS, { @@ -212,9 +210,6 @@ function createTemporaryLogin(authToken, email) { }); return createLoginResponse; }) - .catch((error) => { - Onyx.merge(ONYXKEYS.ACCOUNT, {error: error.message}); - }) .finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); }); @@ -240,15 +235,19 @@ function signIn(password, twoFactorAuthCode) { twoFactorAuthCode, email: credentials.login, }) - .then(({authToken, email}) => { - createTemporaryLogin(authToken, email); - }) - .catch((error) => { - if (error.message === 'passwordForm.error.twoFactorAuthenticationEnabled') { + .then((response) => { + if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { + createTemporaryLogin(response.authToken, response.email); + return; + } + + const errorTranslationKey = ErrorUtils.getErrorKeyFromAuthenticateResponse(response); + if (errorTranslationKey === 'passwordForm.error.twoFactorAuthenticationEnabled') { Onyx.merge(ONYXKEYS.ACCOUNT, {requiresTwoFactorAuth: true, loading: false}); return; } - Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal(error.message), loading: false}); + + Onyx.merge(ONYXKEYS.ACCOUNT, {error: Localize.translateLocal(errorTranslationKey), loading: false}); }); } @@ -308,7 +307,7 @@ function setPassword(password, validateCode, accountID) { accountID, }) .then((response) => { - if (response.jsonCode === 200) { + if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { createTemporaryLogin(response.authToken, response.email); return; } @@ -316,20 +315,6 @@ function setPassword(password, validateCode, accountID) { // This request can fail if the password is not complex enough Onyx.merge(ONYXKEYS.ACCOUNT, {error: response.message}); }) - .catch((response) => { - if (response.title !== CONST.PASSWORD_PAGE.ERROR.VALIDATE_CODE_FAILED) { - return; - } - - const login = lodashGet(response, 'data.email', null); - Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validateCodeExpired: true, error: null}); - - // The login might not be set if the user hits a url in a new session. We set it here to ensure calls to resendValidationLink() will succeed. - if (login) { - Onyx.merge(ONYXKEYS.CREDENTIALS, {login}); - } - Navigation.navigate(ROUTES.HOME); - }) .finally(() => { Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); }); @@ -449,12 +434,13 @@ function validateEmail(accountID, validateCode) { const reauthenticatePusher = _.throttle(() => { Log.info('[Pusher] Re-authenticating and then reconnecting'); API.reauthenticate('Push_Authenticate') - .then(Pusher.reconnect) - .catch(() => { - console.debug( - '[PusherConnectionManager]', - 'Unable to re-authenticate Pusher because we are offline.', - ); + .then((response) => { + if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { + Pusher.reconnect(); + return; + } + + Log.hmmm('[PusherConnectionManager] Unable to reauthenticate and reconnect to Pusher', {message: response.message, jsonCode: response.jsonCode}); }); }, 5000, {trailing: false}); diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index ee9fa46a2e00..332c5be70afd 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -154,9 +154,6 @@ function setExpensifyNewsStatus(subscribed) { return; } - Onyx.merge(ONYXKEYS.USER, {expensifyNewsStatus: !subscribed}); - }) - .catch(() => { Onyx.merge(ONYXKEYS.USER, {expensifyNewsStatus: !subscribed}); }); } diff --git a/src/libs/actions/Wallet.js b/src/libs/actions/Wallet.js index 9ff5c67fce1b..df1b4484f544 100644 --- a/src/libs/actions/Wallet.js +++ b/src/libs/actions/Wallet.js @@ -22,15 +22,19 @@ function fetchOnfidoToken(firstName, lastName, dob) { Onyx.set(ONYXKEYS.WALLET_ONFIDO, {loading: true}); API.Wallet_GetOnfidoSDKToken(firstName, lastName, dob) .then((response) => { - const apiResult = lodashGet(response, ['requestorIdentityOnfido', 'apiResult'], {}); - Onyx.merge(ONYXKEYS.WALLET_ONFIDO, { - applicantID: apiResult.applicantID, - sdkToken: apiResult.sdkToken, - loading: false, - hasAcceptedPrivacyPolicy: true, - }); - }) - .catch(() => Onyx.set(ONYXKEYS.WALLET_ONFIDO, {loading: false, error: CONST.WALLET.ERROR.UNEXPECTED})); + if (response.jsonCode === CONST.JSON_CODE.SUCCESS) { + const apiResult = lodashGet(response, ['requestorIdentityOnfido', 'apiResult'], {}); + Onyx.merge(ONYXKEYS.WALLET_ONFIDO, { + applicantID: apiResult.applicantID, + sdkToken: apiResult.sdkToken, + loading: false, + hasAcceptedPrivacyPolicy: true, + }); + return; + } + + Onyx.set(ONYXKEYS.WALLET_ONFIDO, {loading: false, error: CONST.WALLET.ERROR.UNEXPECTED}); + }); } /** diff --git a/tests/unit/NetworkTest.js b/tests/unit/NetworkTest.js index 3a05284dda72..66aaa3c9d82d 100644 --- a/tests/unit/NetworkTest.js +++ b/tests/unit/NetworkTest.js @@ -63,8 +63,8 @@ test('failing to reauthenticate while offline should not log out user', () => { .then(() => { expect(isOffline).toBe(null); - // Mock fetch() so that it throws a TypeError to simulate a bad network connection - global.fetch = jest.fn(() => new Promise((_resolve, reject) => reject(new TypeError('Failed to fetch')))); + // Mock fetch() so that it throws a TypeError to simulate a bad network connection and a request that failed in flight + global.fetch = jest.fn().mockRejectedValueOnce(new TypeError(CONST.ERROR.FAILED_TO_FETCH)); const actualXhr = HttpUtils.xhr; HttpUtils.xhr = jest.fn(); @@ -92,14 +92,12 @@ test('failing to reauthenticate while offline should not log out user', () => { jsonCode: CONST.JSON_CODE.SUCCESS, })); - // This should first trigger re-authentication and then an API is offline error + // This should first trigger re-authentication and the Authenticate request should throw an error API.Get({returnValueList: 'chatList'}); return waitForPromisesToResolve() .then(() => Onyx.set(ONYXKEYS.NETWORK, {isOffline: false})) .then(() => { expect(isOffline).toBe(false); - - // Advance the network request queue by 1 second so that it can realize it's back online jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); return waitForPromisesToResolve(); }) @@ -364,7 +362,7 @@ test('requests should resume when we are online', () => { }); }); -test('persisted request should not be cleared until a backend response occurs', () => { +test('persisted request should not be cleared until they succeed', () => { // We're setting up xhr handler that will resolve calls programmatically const xhrCalls = []; const promises = []; @@ -467,9 +465,10 @@ test('test bad response will log alert', () => { }); test('test Failed to fetch error for requests not flagged with shouldRetry will throw API OFFLINE error', () => { - // Setup xhr handler that rejects once with a 502 Bad Gateway + // Setup xhr handler that rejects once with Failed to fetch global.fetch = jest.fn(() => new Promise((_resolve, reject) => reject(new Error(CONST.ERROR.FAILED_TO_FETCH)))); + const onSuccess = jest.fn(); const onRejected = jest.fn(); // Given we have a request made while online @@ -477,14 +476,15 @@ test('test Failed to fetch error for requests not flagged with shouldRetry will .then(() => { // When network calls with are made Network.post('mock command', {param1: 'value1', shouldRetry: false}) + .then(onSuccess) .catch(onRejected); return waitForPromisesToResolve(); }) .then(() => { - const error = onRejected.mock.calls[0][0]; - expect(onRejected).toHaveBeenCalled(); - expect(error.message).toBe(CONST.ERROR.API_OFFLINE); + const response = onSuccess.mock.calls[0][0]; + expect(onRejected).not.toHaveBeenCalled(); + expect(response).toEqual({jsonCode: CONST.JSON_CODE.REQUEST_FAILED}); }); }); @@ -522,3 +522,33 @@ test('persisted request can trigger reauthentication for anything retryable', () expect(commandName3).toBe('Mock'); }); }); + +test('Network.post() / API methods will not throw errors and cannot be caught with a .catch()', () => { + const MockCommand = () => Network.post('MockCommand'); + jest.spyOn(HttpUtils, 'xhr') + .mockRejectedValue(new TypeError(CONST.ERROR.FAILED_TO_FETCH)); + + /** + * @param {Number} numberOfTicks + * @returns {Promise} + */ + function waitForMainQueueToProcess(numberOfTicks) { + return _.reduce([...new Array(numberOfTicks)], promise => promise.then(() => { + jest.advanceTimersByTime(CONST.NETWORK.PROCESS_REQUEST_DELAY_MS); + return waitForPromisesToResolve(); + }), Promise.resolve()); + } + + const onSuccess = jest.fn(); + const onFail = jest.fn(); + + MockCommand() + .then(onSuccess) + .catch(onFail); + + return waitForMainQueueToProcess(CONST.NETWORK.MAX_REQUEST_RETRIES) + .then(() => { + expect(onSuccess.mock.calls[0][0]).toEqual({jsonCode: CONST.JSON_CODE.REQUEST_FAILED}); + expect(onFail).not.toHaveBeenCalled(); + }); +});