From c94e8f4794c557e7bb9a4aca1cee18348893421a Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 08:37:38 -0400 Subject: [PATCH 01/85] Create TransactionUtils.buildOptimisticTransaction --- src/libs/TransactionUtils.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/libs/TransactionUtils.js diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js new file mode 100644 index 000000000000..57b4b6c60614 --- /dev/null +++ b/src/libs/TransactionUtils.js @@ -0,0 +1,22 @@ +import DateUtils from './DateUtils'; +import * as NumberUtils from './NumberUtils'; + +/** + * Optimistically generate a transaction. + * + * @param {Number} amount – in cents + * @param {String} comment + * @param {String} currency + */ +function buildOptimisticTransaction(amount, comment, currency) { + // transactionIDs are random, positive, 64-bit numbers. + // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) + const transactionID = NumberUtils.rand64(); + const created = DateUtils.getDBTime(); + return { + transactionID, + amount, + comment, + created, + }; +} From 5705e7715ee642ab997dbc50f09d395497280c65 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 08:38:14 -0400 Subject: [PATCH 02/85] Oops, add export --- src/libs/TransactionUtils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 57b4b6c60614..7e844fc8deb4 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -20,3 +20,7 @@ function buildOptimisticTransaction(amount, comment, currency) { created, }; } + +export default { + buildOptimisticTransaction, +}; From 962009fc71f5684d3959f8c1fb1ca947ca331ee9 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 08:44:14 -0400 Subject: [PATCH 03/85] Make comment optional in buildOptimisticTransaction --- src/libs/TransactionUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 7e844fc8deb4..7b82bdcfcfb9 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -5,10 +5,10 @@ import * as NumberUtils from './NumberUtils'; * Optimistically generate a transaction. * * @param {Number} amount – in cents - * @param {String} comment * @param {String} currency + * @param {String} comment */ -function buildOptimisticTransaction(amount, comment, currency) { +function buildOptimisticTransaction(amount, currency, comment = '') { // transactionIDs are random, positive, 64-bit numbers. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) const transactionID = NumberUtils.rand64(); From c2ab2fcbcfa8fd97674774d6cabf7e930862ff02 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 08:53:51 -0400 Subject: [PATCH 04/85] Setup transaction data w/ RBR in RequestMoney flow --- src/libs/TransactionUtils.js | 2 ++ src/libs/actions/IOU.js | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 7b82bdcfcfb9..541bbe0a9704 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -1,3 +1,4 @@ +import CONST from '../CONST'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; @@ -18,6 +19,7 @@ function buildOptimisticTransaction(amount, currency, comment = '') { amount, comment, created, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }; } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 4b6415bae599..f2b86d68cd09 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -13,6 +13,7 @@ import * as ReportUtils from '../ReportUtils'; import * as IOUUtils from '../IOUUtils'; import * as OptionsListUtils from '../OptionsListUtils'; import DateUtils from '../DateUtils'; +import TransactionUtils from '../TransactionUtils'; const chatReports = {}; const iouReports = {}; @@ -175,14 +176,37 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com reportActionsFailureData.value[optimisticCreatedAction.reportActionID] = {pendingAction: null}; } + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency); + const optimisticTransactionData = { + onyxMethod: Onyx.METHOD.MERGE, + key: optimisticTransaction.transactionID, + value: optimisticTransaction, + }; + const transactionSuccessData = { + onyxMethod: Onyx.METHOD.MERGE, + key: optimisticTransaction.transactionID, + value: { + pendingAction: null, + }, + }; + const transactionFailureData = { + pendingAction: null, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), + }, + }; + + const optimisticData = [ optimisticChatReportData, optimisticIOUReportData, optimisticReportActionsData, + optimisticTransactionData, ]; const successData = [ reportActionsSuccessData, + transactionSuccessData, ]; if (!_.isEmpty(chatReportSuccessData)) { successData.push(chatReportSuccessData); @@ -191,6 +215,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com const failureData = [ chatReportFailureData, reportActionsFailureData, + transactionFailureData, ]; const parsedComment = ReportUtils.getParsedComment(comment); From 8d998d9090864ee7a845337de13ec034fa60f299 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 09:07:00 -0400 Subject: [PATCH 05/85] Make transactionID required in buildOptimisticIOUAction --- src/libs/ReportUtils.js | 9 ++++----- src/libs/actions/IOU.js | 15 ++++++++++----- tests/unit/IOUUtilsTest.js | 4 ++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 677e842f6855..e916af1660b6 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1078,15 +1078,14 @@ function getIOUReportActionMessage(type, total, participants, comment, currency, * @param {String} currency * @param {String} comment - User comment for the IOU. * @param {Array} participants - An array with participants details. - * @param {String} [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, payPal, Expensify). * @param {String} [iouTransactionID] - Only required if the IOUReportAction type is oneOf(cancel, decline). Generates a randomID as default. + * + * @param {String} [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, payPal, Expensify). * @param {String} [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. * @param {Boolean} [isSettlingUp] - Whether we are settling up an IOU. - * * @returns {Object} */ -function buildOptimisticIOUReportAction(type, amount, currency, comment, participants, paymentType = '', iouTransactionID = '', iouReportID = '', isSettlingUp = false) { - const IOUTransactionID = iouTransactionID || NumberUtils.rand64(); +function buildOptimisticIOUReportAction(type, amount, currency, comment, participants, iouTransactionID, paymentType = '', iouReportID = '', isSettlingUp = false) { const IOUReportID = iouReportID || generateReportID(); const parser = new ExpensiMark(); const commentText = getParsedComment(comment); @@ -1096,7 +1095,7 @@ function buildOptimisticIOUReportAction(type, amount, currency, comment, partici amount, comment: textForNewComment, currency, - IOUTransactionID, + iouTransactionID, IOUReportID, type, }; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f2b86d68cd09..34e6f4bd0969 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -78,6 +78,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(recipientEmail); + // TODO: optimistic TransactionID const optimisticReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.CREATE, amount, @@ -264,6 +265,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const groupCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); + // TODO: Optimistic transactionID const groupIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.SPLIT, Math.round(amount * 100), @@ -375,6 +377,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const oneOnOneCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); + // TODO: optimistic transactionID const oneOnOneIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.CREATE, splitAmount, @@ -580,8 +583,8 @@ function cancelMoneyRequest(chatReportID, iouReportID, type, moneyRequestAction) moneyRequestAction.originalMessage.currency, Str.htmlDecode(moneyRequestAction.originalMessage.comment), [], - '', transactionID, + '', iouReportID, ); @@ -714,15 +717,16 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(recipientEmail); + // TODO: optimistic transactionID const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.PAY, amount, currency, comment, [recipient], - paymentMethodType, '', - optimisticIOUReport.reportID, + paymentMethodType, + optimisticIOUReport.reportID ); // First, add data that will be used in all cases @@ -829,16 +833,17 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType * @returns {Object} */ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMethodType) { + // TODO: transactionID const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.PAY, iouReport.total, iouReport.currency, '', [recipient], - paymentMethodType, '', + paymentMethodType, iouReport.reportID, - true, + true ); const optimisticData = [ diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 4ae4f3c5bc4b..51551c2be442 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -18,9 +18,9 @@ function createIOUReportAction(type, amount, currency, {IOUTransactionID, isOnli currency, 'Test comment', [managerEmail], - '', IOUTransactionID, - iouReport.reportID, + '', + iouReport.reportID ); // Default is to create requests offline, if this is specified then we need to remove the pendingAction From b68362d4f58d41d886c4b41858cfc15bc66351a1 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 09:12:07 -0400 Subject: [PATCH 06/85] Finish RequestMoney flow --- src/libs/ReportUtils.js | 2 +- src/libs/actions/IOU.js | 45 ++++++++++++++++++++--------------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index e916af1660b6..48884cb6acc0 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1095,7 +1095,7 @@ function buildOptimisticIOUReportAction(type, amount, currency, comment, partici amount, comment: textForNewComment, currency, - iouTransactionID, + IOUTransactionID: iouTransactionID, IOUReportID, type, }; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 34e6f4bd0969..cf45169e8f92 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -76,6 +76,26 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com iouReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, debtorEmail, amount, chatReport.reportID, currency, preferredLocale); } + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency); + const optimisticTransactionData = { + onyxMethod: Onyx.METHOD.MERGE, + key: optimisticTransaction.transactionID, + value: optimisticTransaction, + }; + const transactionSuccessData = { + onyxMethod: Onyx.METHOD.MERGE, + key: optimisticTransaction.transactionID, + value: { + pendingAction: null, + }, + }; + const transactionFailureData = { + pendingAction: null, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), + }, + }; + // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(recipientEmail); // TODO: optimistic TransactionID @@ -85,7 +105,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com currency, comment, [participant], - '', + optimisticTransaction.transactionID, '', iouReport.reportID, ); @@ -177,27 +197,6 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com reportActionsFailureData.value[optimisticCreatedAction.reportActionID] = {pendingAction: null}; } - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency); - const optimisticTransactionData = { - onyxMethod: Onyx.METHOD.MERGE, - key: optimisticTransaction.transactionID, - value: optimisticTransaction, - }; - const transactionSuccessData = { - onyxMethod: Onyx.METHOD.MERGE, - key: optimisticTransaction.transactionID, - value: { - pendingAction: null, - }, - }; - const transactionFailureData = { - pendingAction: null, - errors: { - [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), - }, - }; - - const optimisticData = [ optimisticChatReportData, optimisticIOUReportData, @@ -227,7 +226,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com comment: parsedComment, iouReportID: iouReport.reportID, chatReportID: chatReport.reportID, - transactionID: optimisticReportAction.originalMessage.IOUTransactionID, + transactionID: optimisticTransaction.transactionID, reportActionID: optimisticReportAction.reportActionID, createdReportActionID: isNewChat ? optimisticCreatedAction.reportActionID : 0, }, {optimisticData, successData, failureData}); From a6aec7f0b99ab9e0e4a489504388b8094b229a9b Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 10:32:38 -0400 Subject: [PATCH 07/85] Implement createSplitsAndOnyxData --- src/libs/TransactionUtils.js | 3 +- src/libs/actions/IOU.js | 71 ++++++++++++++++++++++++++++++------ 2 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 541bbe0a9704..fc1c4696eef3 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -13,12 +13,11 @@ function buildOptimisticTransaction(amount, currency, comment = '') { // transactionIDs are random, positive, 64-bit numbers. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) const transactionID = NumberUtils.rand64(); - const created = DateUtils.getDBTime(); return { transactionID, amount, comment, - created, + created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }; } diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index cf45169e8f92..ef6c17c17096 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -79,26 +79,29 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency); const optimisticTransactionData = { onyxMethod: Onyx.METHOD.MERGE, - key: optimisticTransaction.transactionID, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, value: optimisticTransaction, }; const transactionSuccessData = { onyxMethod: Onyx.METHOD.MERGE, - key: optimisticTransaction.transactionID, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, value: { pendingAction: null, }, }; const transactionFailureData = { - pendingAction: null, - errors: { - [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, + value: { + pendingAction: null, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), + }, }, }; // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(recipientEmail); - // TODO: optimistic TransactionID const optimisticReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.CREATE, amount, @@ -262,15 +265,18 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment : ReportUtils.getChatByParticipants(participantLogins); const groupChatReport = existingGroupChatReport || ReportUtils.buildOptimisticChatReport(participantLogins); + const amountInCents = Math.round(amount * 100); + const groupTransaction = TransactionUtils.buildOptimisticTransaction(amountInCents, currency); + // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const groupCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); - // TODO: Optimistic transactionID const groupIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.SPLIT, - Math.round(amount * 100), + amountInCents, currency, comment, participants, + groupTransaction.transactionID, ); groupChatReport.lastReadTime = DateUtils.getDBTime(); @@ -300,6 +306,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment [groupIOUReportAction.reportActionID]: groupIOUReportAction, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${groupTransaction.transactionID}`, + value: groupTransaction, + } ]; const successData = [ @@ -316,6 +327,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment [groupIOUReportAction.reportActionID]: {pendingAction: null}, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${groupTransaction.transactionID}`, + value: {pendingAction: null}, + } ]; const failureData = [ @@ -334,6 +350,16 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment [groupIOUReportAction.reportActionID]: {pendingAction: null}, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${groupTransaction.transactionID}`, + value: { + pendingFields: null, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), + }, + }, + }, ]; // Loop through participants creating individual chats, iouReports and reportActionIDs as needed @@ -374,16 +400,17 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment oneOnOneChatReport.iouReportID = oneOnOneIOUReport.reportID; } + const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction(amountInCents, currency); + // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const oneOnOneCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); - // TODO: optimistic transactionID const oneOnOneIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.CREATE, splitAmount, currency, comment, [participant], - '', + oneOnOneTransaction.transactionID, '', oneOnOneIOUReport.reportID, ); @@ -414,6 +441,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment [oneOnOneIOUReportAction.reportActionID]: oneOnOneIOUReportAction, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${oneOnOneTransaction.transactionID}`, + value: oneOnOneTransaction, + }, ); successData.push( @@ -433,6 +465,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment [oneOnOneIOUReportAction.reportActionID]: {pendingAction: null}, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${oneOnOneTransaction.transactionID}`, + value: {pendingAction: null}, + }, ); failureData.push( @@ -456,6 +493,16 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment [oneOnOneIOUReportAction.reportActionID]: {pendingAction: null}, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${oneOnOneTransaction.transactionID}`, + value: { + pendingFields: null, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), + }, + }, + }, ); // Regardless of the number of participants, we always want to push the iouReport update to onyxData @@ -479,7 +526,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment amount: splitAmount, iouReportID: oneOnOneIOUReport.reportID, chatReportID: oneOnOneChatReport.reportID, - transactionID: oneOnOneIOUReportAction.originalMessage.IOUTransactionID, + transactionID: oneOnOneTransaction.transactionID, reportActionID: oneOnOneIOUReportAction.reportActionID, }; @@ -492,7 +539,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment const groupData = { chatReportID: groupChatReport.reportID, - transactionID: groupIOUReportAction.originalMessage.IOUTransactionID, + transactionID: groupTransaction.transactionID, reportActionID: groupIOUReportAction.reportActionID, }; From ae89202c22feeed426f193b02f19a244a9fd3e8f Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 10:49:56 -0400 Subject: [PATCH 08/85] Implement getPayMoneyRequestParams --- src/libs/actions/IOU.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ef6c17c17096..960baeb72144 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -879,14 +879,14 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType * @returns {Object} */ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMethodType) { - // TODO: transactionID + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(iouReport.total, iouReport.currency); const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.PAY, iouReport.total, iouReport.currency, '', [recipient], - '', + optimisticTransaction.transactionID, paymentMethodType, iouReport.reportID, true @@ -925,6 +925,11 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, + value: optimisticTransaction, + }, ]; const successData = [ @@ -944,6 +949,13 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho iouReportID: null, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, + value: { + pendingAction: null, + }, + }, ]; const failureData = [ @@ -959,6 +971,16 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, + value: { + pendingAction: null, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), + }, + }, + }, ]; return { From 42ff9e4fb58edb6274b9460972a64b50197961bf Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 11:04:25 -0400 Subject: [PATCH 09/85] Implement getSendMoneyParams --- src/libs/actions/IOU.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 960baeb72144..394efc5d9d1c 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -761,16 +761,22 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType } const optimisticIOUReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, managerEmail, amount, chatReport.reportID, currency, preferredLocale, true); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency, comment); + const optimisticTransactionData = { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, + value: optimisticTransaction, + }; + // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const optimisticCreatedAction = ReportUtils.buildOptimisticCreatedReportAction(recipientEmail); - // TODO: optimistic transactionID const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.PAY, amount, currency, comment, [recipient], - '', + optimisticTransaction.transactionID, paymentMethodType, optimisticIOUReport.reportID ); @@ -813,6 +819,11 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, + value: {pendingAction: null}, + } ]; const failureData = [ @@ -827,6 +838,16 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, + value: { + pendingAction: null, + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.other'), + }, + }, + }, ]; // Now, let's add the data we need just when we are creating a new chat report @@ -861,7 +882,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType chatReportID: chatReport.reportID, reportActionID: optimisticIOUReportAction.reportActionID, paymentMethodType, - transactionID: optimisticIOUReportAction.originalMessage.IOUTransactionID, + transactionID: optimisticTransaction.transactionID, newIOUReportDetails, createdReportActionID: isNewChat ? optimisticCreatedAction.reportActionID : 0, }, From d99710495c437f7f30f463ff8e9a80097ef88236 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 11:08:23 -0400 Subject: [PATCH 10/85] Update JSDoc comment to make it clear iouTransactionID is not optional --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 48884cb6acc0..e5061e927e64 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1078,7 +1078,7 @@ function getIOUReportActionMessage(type, total, participants, comment, currency, * @param {String} currency * @param {String} comment - User comment for the IOU. * @param {Array} participants - An array with participants details. - * @param {String} [iouTransactionID] - Only required if the IOUReportAction type is oneOf(cancel, decline). Generates a randomID as default. + * @param {String} iouTransactionID - Only required if the IOUReportAction type is oneOf(cancel, decline). Generates a randomID as default. * * @param {String} [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, payPal, Expensify). * @param {String} [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. From 6d8d11d87e2accb0848a1f21df450d834d52c438 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 11:30:32 -0400 Subject: [PATCH 11/85] Fix lint --- src/libs/TransactionUtils.js | 1 + src/libs/actions/IOU.js | 11 ++++++----- tests/unit/IOUUtilsTest.js | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index fc1c4696eef3..aaf6b9ca075c 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -8,6 +8,7 @@ import * as NumberUtils from './NumberUtils'; * @param {Number} amount – in cents * @param {String} currency * @param {String} comment + * @returns {Object} */ function buildOptimisticTransaction(amount, currency, comment = '') { // transactionIDs are random, positive, 64-bit numbers. diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 394efc5d9d1c..b842d8483111 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -310,7 +310,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${groupTransaction.transactionID}`, value: groupTransaction, - } + }, ]; const successData = [ @@ -331,7 +331,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${groupTransaction.transactionID}`, value: {pendingAction: null}, - } + }, ]; const failureData = [ @@ -778,7 +778,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType [recipient], optimisticTransaction.transactionID, paymentMethodType, - optimisticIOUReport.reportID + optimisticIOUReport.reportID, ); // First, add data that will be used in all cases @@ -823,7 +823,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, value: {pendingAction: null}, - } + }, ]; const failureData = [ @@ -874,6 +874,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType optimisticChatReportData, optimisticIOUReportData, optimisticReportActionsData, + optimisticTransactionData, ]; return { @@ -910,7 +911,7 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho optimisticTransaction.transactionID, paymentMethodType, iouReport.reportID, - true + true, ); const optimisticData = [ diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 51551c2be442..3fef8221f9ab 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -20,7 +20,7 @@ function createIOUReportAction(type, amount, currency, {IOUTransactionID, isOnli [managerEmail], IOUTransactionID, '', - iouReport.reportID + iouReport.reportID, ); // Default is to create requests offline, if this is specified then we need to remove the pendingAction From d85e4a0c7502757d11f854d32ec8518e1d9f9298 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 14:14:05 -0400 Subject: [PATCH 12/85] Create PusherHelper abstraction --- tests/actions/ReportTest.js | 37 ++++++------------------------- tests/utils/PusherHelper.js | 43 +++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 30 deletions(-) create mode 100644 tests/utils/PusherHelper.js diff --git a/tests/actions/ReportTest.js b/tests/actions/ReportTest.js index 974708dcf4bf..f7140d978165 100644 --- a/tests/actions/ReportTest.js +++ b/tests/actions/ReportTest.js @@ -6,12 +6,10 @@ import { beforeEach, beforeAll, afterEach, describe, it, expect, } from '@jest/globals'; import ONYXKEYS from '../../src/ONYXKEYS'; -import * as Pusher from '../../src/libs/Pusher/pusher'; -import PusherConnectionManager from '../../src/libs/PusherConnectionManager'; -import CONFIG from '../../src/CONFIG'; import CONST from '../../src/CONST'; import * as Report from '../../src/libs/actions/Report'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; import Log from '../../src/libs/Log'; import * as PersistedRequests from '../../src/libs/actions/PersistedRequests'; @@ -30,21 +28,7 @@ jest.mock('../../src/libs/actions/Report', () => { describe('actions/Report', () => { beforeAll(() => { - // When using the Pusher mock the act of calling Pusher.isSubscribed will create a - // channel already in a subscribed state. These methods are normally used to prevent - // duplicated subscriptions, but we don't need them for this test so forcing them to - // return false will make the testing less complex. - Pusher.isSubscribed = jest.fn().mockReturnValue(false); - Pusher.isAlreadySubscribing = jest.fn().mockReturnValue(false); - - // Connect to Pusher - PusherConnectionManager.init(); - Pusher.init({ - appKey: CONFIG.PUSHER.APP_KEY, - cluster: CONFIG.PUSHER.CLUSTER, - authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api?command=AuthenticatePusher`, - }); - + PusherHelper.setup(); Onyx.init({ keys: ONYXKEYS, registerStorageEventListener: () => {}, @@ -53,11 +37,7 @@ describe('actions/Report', () => { beforeEach(() => Onyx.clear().then(waitForPromisesToResolve)); - afterEach(() => { - // Unsubscribe from account channel after each test since we subscribe in the function - // subscribeToUserEvents and we don't want duplicate event subscriptions. - Pusher.unsubscribe(`${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}1${CONFIG.PUSHER.SUFFIX}`); - }); + afterEach(PusherHelper.teardown); it('should store a new report action in Onyx when onyxApiUpdate event is handled via Pusher', () => { global.fetch = TestHelper.getGlobalFetchMock(); @@ -106,8 +86,7 @@ describe('actions/Report', () => { // We subscribed to the Pusher channel above and now we need to simulate a reportComment action // Pusher event so we can verify that action was handled correctly and merged into the reportActions. - const channel = Pusher.getChannel(`${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}1${CONFIG.PUSHER.SUFFIX}`); - channel.emit(Pusher.TYPE.ONYX_API_UPDATE, [ + PusherHelper.emitOnyxUpdate([ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, @@ -225,7 +204,6 @@ describe('actions/Report', () => { const USER_1_ACCOUNT_ID = 1; const USER_2_LOGIN = 'different-user@test.com'; const USER_2_ACCOUNT_ID = 2; - const channel = Pusher.getChannel(`${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${USER_1_ACCOUNT_ID}${CONFIG.PUSHER.SUFFIX}`); return Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, {reportName: 'Test', reportID: REPORT_ID}) .then(() => TestHelper.signInWithTestUser(USER_1_ACCOUNT_ID, USER_1_LOGIN)) .then(() => { @@ -237,7 +215,7 @@ describe('actions/Report', () => { .then(() => { // When a Pusher event is handled for a new report comment reportActionCreatedDate = DateUtils.getDBTime(); - channel.emit(Pusher.TYPE.ONYX_API_UPDATE, [ + PusherHelper.emitOnyxUpdate([ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, @@ -373,7 +351,7 @@ describe('actions/Report', () => { optimisticReportActions.value[400].created = reportActionCreatedDate; // When we emit the events for these pending created actions to update them to not pending - channel.emit(Pusher.TYPE.ONYX_API_UPDATE, [ + PusherHelper.emitOnyxUpdate([ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, @@ -513,8 +491,7 @@ describe('actions/Report', () => { }) .then(() => { // Simulate a Pusher Onyx update with a report action with shouldNotify - const channel = Pusher.getChannel(`${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${TEST_USER_ACCOUNT_ID}${CONFIG.PUSHER.SUFFIX}`); - channel.emit(Pusher.TYPE.ONYX_API_UPDATE, [ + PusherHelper.emitOnyxUpdate([ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, diff --git a/tests/utils/PusherHelper.js b/tests/utils/PusherHelper.js new file mode 100644 index 000000000000..d3d7cdede559 --- /dev/null +++ b/tests/utils/PusherHelper.js @@ -0,0 +1,43 @@ +import * as Pusher from '../../src/libs/Pusher/pusher'; +import PusherConnectionManager from '../../src/libs/PusherConnectionManager'; +import CONFIG from '../../src/CONFIG'; +import CONST from '../../src/CONST'; + +const CHANNEL_NAME = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}1${CONFIG.PUSHER.SUFFIX}`; + +function setup() { + // When using the Pusher mock the act of calling Pusher.isSubscribed will create a + // channel already in a subscribed state. These methods are normally used to prevent + // duplicated subscriptions, but we don't need them for this test so forcing them to + // return false will make the testing less complex. + Pusher.isSubscribed = jest.fn().mockReturnValue(false); + Pusher.isAlreadySubscribing = jest.fn().mockReturnValue(false); + + // Connect to Pusher + PusherConnectionManager.init(); + Pusher.init({ + appKey: CONFIG.PUSHER.APP_KEY, + cluster: CONFIG.PUSHER.CLUSTER, + authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api?command=AuthenticatePusher`, + }); +} + +/** + * @param {Array} args + */ +function emitOnyxUpdate(args) { + const channel = Pusher.getChannel(CHANNEL_NAME); + channel.emit(Pusher.TYPE.ONYX_API_UPDATE, args); +} + +function teardown() { + // Unsubscribe from account channel after each test since we subscribe in the function + // subscribeToUserEvents and we don't want duplicate event subscriptions. + Pusher.unsubscribe(CHANNEL_NAME); +} + +export default { + setup, + emitOnyxUpdate, + teardown, +}; From c858075f49cc7c6d39f6a097a9da024eaebb954d Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 20 Apr 2023 20:50:55 -0400 Subject: [PATCH 13/85] Create test for requestMoney --- tests/actions/IOUTest.js | 163 ++++++++++++++++++++++++++++++++++++++ tests/utils/TestHelper.js | 40 ++++++++++ 2 files changed, 203 insertions(+) create mode 100644 tests/actions/IOUTest.js diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js new file mode 100644 index 000000000000..5cac9c25c03c --- /dev/null +++ b/tests/actions/IOUTest.js @@ -0,0 +1,163 @@ +import _ from 'underscore'; +import Onyx from 'react-native-onyx'; +import CONST from '../../src/CONST'; +import ONYXKEYS from '../../src/ONYXKEYS'; +import PusherHelper from '../utils/PusherHelper'; +import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import * as IOU from '../../src/libs/actions/IOU'; +import * as TestHelper from '../utils/TestHelper'; + +const RORY_EMAIL = 'rory@expensifail.com'; +const CARLOS_EMAIL = 'cmartins@expensifail.com'; + +describe('actions/IOU', () => { + beforeAll(() => { + PusherHelper.setup(); + Onyx.init({ + keys: ONYXKEYS, + }); + global.fetch = TestHelper.getOnDemandFetchMock(); + // global.fetch = TestHelper.getGlobalFetchMock(); + }); + + beforeEach(() => { + // jest.resetAllMocks(); + return Onyx.clear().then(waitForPromisesToResolve); + }); + + afterEach(PusherHelper.teardown); + + describe('requestMoney', () => { + it('creates new chat if needed', () => { + const amount = 100; + const comment = 'Giv money plz'; + IOU.requestMoney({}, amount, CONST.CURRENCY.USD, RORY_EMAIL, {login: CARLOS_EMAIL}, comment); + let chatReportID; + let iouReportID; + let createdAction; + let iouAction; + let transactionID; + return waitForPromisesToResolve() + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // A chat report and an iou report should be created + const chatReports = _.filter(allReports, report => report.type === CONST.REPORT.TYPE.CHAT); + const iouReports = _.filter(allReports, report => report.type === CONST.REPORT.TYPE.IOU); + expect(_.size(chatReports)).toBe(1); + expect(_.size(iouReports)).toBe(1); + const chatReport = chatReports[0]; + chatReportID = chatReport.reportID; + const iouReport = iouReports[0]; + iouReportID = iouReport.reportID; + + // They should be linked together + expect(chatReport.participants).toEqual([CARLOS_EMAIL]); + expect(chatReport.iouReportID).toBe(iouReport.reportID); + expect(chatReport.hasOutstandingIOU).toBe(true); + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, + waitForCollectionCallback: true, + callback: (reportActionsForChatReport) => { + Onyx.disconnect(connectionID); + + // The chat report should have a CREATED action and IOU action + expect(_.size(reportActionsForChatReport)).toBe(2); + const createdActions = _.filter(reportActionsForChatReport, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); + const iouActions = _.filter(reportActionsForChatReport, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + expect(_.size(createdActions)).toBe(1); + expect(_.size(iouActions)).toBe(1); + createdAction = createdActions[0]; + iouAction = iouActions[0]; + + // The CREATED action should not be created after the IOU action + expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + + // The comment should be included in the IOU action + expect(iouAction.originalMessage.comment).toBe(comment); + + // The amount in the IOU action should be correct + expect(iouAction.originalMessage.amount).toBe(amount); + + // Both actions should be pending + expect(createdAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(iouAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // There should be one transaction + expect(_.size(allTransactions)).toBe(1); + const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); + transactionID = transaction.transactionID; + + // Its amount should match the amount of the request + expect(transaction.amount).toBe(amount); + + // It should be pending + expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + // The transactionID on the iou action should match the one from the transactions collection + expect(iouAction.originalMessage.IOUTransactionID).toBe(transactionID); + + resolve(); + }, + }); + })) + .then(fetch.flush) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, + waitForCollectionCallback: true, + callback: reportActionsForChatReport => { + Onyx.disconnect(connectionID); + expect(_.size(reportActionsForChatReport)).toBe(2); + _.each(reportActionsForChatReport, reportAction => expect(reportAction.pendingAction).not.toBeTruthy()); + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + waitForCollectionCallback: true, + callback: transaction => { + Onyx.disconnect(connectionID); + expect(transaction.pendingAction).not.toBeTruthy(); + resolve(); + }, + }); + })); + }); + + it('updates existing chat report if there is one', () => { + + }); + + it('updates existing IOU report if there is one', () => { + + }); + + it('correctly implements RedBrickRoad error handling', () => { + + }); + }); +}); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index c45e9096f55b..3fc28810d8b6 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import Onyx from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import CONST from '../../src/CONST'; @@ -154,6 +155,44 @@ function getGlobalFetchMock() { }); } +/** + * Mocks fetch, but requests won't resolve until you call `flush` on the object returned by this function. + * + * @example + * + * beforeAll(() => { + * global.fetch = TestHelper.getOnDemandFetchMock(); + * }) + * + * it("doesn't reply to fetch until you tell it to", () => { + * const myRequest = fetch('something'); + * + * // Make some assertion that will only be true before your request resolves + * + * fetch.flush() + * .then(() => { + * // Make some assertion that will only be true after your request resolves + * }); + * }) + * @returns {Function} + */ +function getOnDemandFetchMock() { + let queue = []; + const mockFetch = jest.fn().mockImplementation(() => new Promise(resolve => queue.push(resolve))); + mockFetch.flush = () => { + _.each(queue, resolve => { + resolve({ + ok: true, + json: () => Promise.resolve({ + jsonCode: 200, + }), + }); + }); + return waitForPromisesToResolve(); + } + return mockFetch; +} + /** * @param {String} login * @param {Number} accountID @@ -188,6 +227,7 @@ function buildTestReportComment(actorEmail, created, actorAccountID, actionID = export { getGlobalFetchMock, + getOnDemandFetchMock, signInWithTestUser, signOutTestUser, setPersonalDetails, From 9176a1747994b338eb1aa17bde0915b9408d7088 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 21 Apr 2023 11:11:36 -0400 Subject: [PATCH 14/85] Refactor fetch mock --- tests/actions/IOUTest.js | 13 ++++---- tests/utils/TestHelper.js | 70 +++++++++++++++++---------------------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 5cac9c25c03c..ddb2d0fd6064 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -16,12 +16,10 @@ describe('actions/IOU', () => { Onyx.init({ keys: ONYXKEYS, }); - global.fetch = TestHelper.getOnDemandFetchMock(); - // global.fetch = TestHelper.getGlobalFetchMock(); }); beforeEach(() => { - // jest.resetAllMocks(); + global.fetch = TestHelper.getGlobalFetchMock(); return Onyx.clear().then(waitForPromisesToResolve); }); @@ -31,12 +29,13 @@ describe('actions/IOU', () => { it('creates new chat if needed', () => { const amount = 100; const comment = 'Giv money plz'; - IOU.requestMoney({}, amount, CONST.CURRENCY.USD, RORY_EMAIL, {login: CARLOS_EMAIL}, comment); let chatReportID; let iouReportID; let createdAction; let iouAction; let transactionID; + fetch.pause(); + IOU.requestMoney({}, amount, CONST.CURRENCY.USD, RORY_EMAIL, {login: CARLOS_EMAIL}, comment); return waitForPromisesToResolve() .then(() => new Promise((resolve) => { const connectionID = Onyx.connect({ @@ -122,7 +121,7 @@ describe('actions/IOU', () => { }, }); })) - .then(fetch.flush) + .then(fetch.resume) .then(() => new Promise((resolve) => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, @@ -130,7 +129,7 @@ describe('actions/IOU', () => { callback: reportActionsForChatReport => { Onyx.disconnect(connectionID); expect(_.size(reportActionsForChatReport)).toBe(2); - _.each(reportActionsForChatReport, reportAction => expect(reportAction.pendingAction).not.toBeTruthy()); + _.each(reportActionsForChatReport, reportAction => expect(reportAction.pendingAction).toBeFalsy()); resolve(); }, }); @@ -141,7 +140,7 @@ describe('actions/IOU', () => { waitForCollectionCallback: true, callback: transaction => { Onyx.disconnect(connectionID); - expect(transaction.pendingAction).not.toBeTruthy(); + expect(transaction.pendingAction).toBeFalsy(); resolve(); }, }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 3fc28810d8b6..a933a72d7b3d 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -137,6 +137,13 @@ function signOutTestUser() { /** * Use for situations where fetch() is required. * + * It also has some additional methods: + * + * - pause() – stop resolving promises until you call resume() + * - resume() - flush the queue of promises, and start resolving new promises immediately + * - fail() - start returning a failure response + * - success() - go back to returning a success response + * * @example * * beforeAll(() => { @@ -146,50 +153,36 @@ function signOutTestUser() { * @returns {Function} */ function getGlobalFetchMock() { - return jest.fn() - .mockResolvedValue({ + let queue = []; + let isPaused = false; + let shouldFail = false; + + const getResponse = () => shouldFail + ? {ok: false} + : { ok: true, json: () => Promise.resolve({ - jsonCode: 200, + jsonCode: 200 }), - }); -} + }; -/** - * Mocks fetch, but requests won't resolve until you call `flush` on the object returned by this function. - * - * @example - * - * beforeAll(() => { - * global.fetch = TestHelper.getOnDemandFetchMock(); - * }) - * - * it("doesn't reply to fetch until you tell it to", () => { - * const myRequest = fetch('something'); - * - * // Make some assertion that will only be true before your request resolves - * - * fetch.flush() - * .then(() => { - * // Make some assertion that will only be true after your request resolves - * }); - * }) - * @returns {Function} - */ -function getOnDemandFetchMock() { - let queue = []; - const mockFetch = jest.fn().mockImplementation(() => new Promise(resolve => queue.push(resolve))); - mockFetch.flush = () => { - _.each(queue, resolve => { - resolve({ - ok: true, - json: () => Promise.resolve({ - jsonCode: 200, - }), - }); + const mockFetch = jest.fn() + .mockImplementation(() => { + if (!isPaused) { + return Promise.resolve(getResponse()); + } + return new Promise(resolve => queue.push(resolve)); }); + + mockFetch.pause = () => isPaused = true; + mockFetch.resume = () => { + isPaused = false; + _.each(queue, resolve => resolve(getResponse())); return waitForPromisesToResolve(); - } + }; + mockFetch.fail = () => shouldFail = true; + mockFetch.succeed = () => shouldFail = false; + return mockFetch; } @@ -227,7 +220,6 @@ function buildTestReportComment(actorEmail, created, actorAccountID, actionID = export { getGlobalFetchMock, - getOnDemandFetchMock, signInWithTestUser, signOutTestUser, setPersonalDetails, From f622e5cb2a9de4b80473cd54bc681eb0b1664573 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 21 Apr 2023 12:23:16 -0400 Subject: [PATCH 15/85] Create test for requestMoney with existing chatReport --- tests/actions/IOUTest.js | 130 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index ddb2d0fd6064..5ca2a3cfafbb 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -6,6 +6,8 @@ import PusherHelper from '../utils/PusherHelper'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; import * as IOU from '../../src/libs/actions/IOU'; import * as TestHelper from '../utils/TestHelper'; +import DateUtils from '../../src/libs/DateUtils'; +import * as NumberUtils from '../../src/libs/NumberUtils'; const RORY_EMAIL = 'rory@expensifail.com'; const CARLOS_EMAIL = 'cmartins@expensifail.com'; @@ -148,7 +150,135 @@ describe('actions/IOU', () => { }); it('updates existing chat report if there is one', () => { + const amount = 100; + const comment = 'Giv money plz'; + let chatReport = { + reportID: 1234, + type: CONST.REPORT.TYPE.CHAT, + hasOutstandingIOU: false, + participants: [CARLOS_EMAIL], + }; + const createdAction = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: DateUtils.getDBTime(), + }; + let iouReportID; + let iouAction; + let transactionID; + fetch.pause(); + return Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, { + [createdAction.reportActionID]: createdAction, + })) + .then(() => { + IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, RORY_EMAIL, {login: CARLOS_EMAIL}, comment); + return waitForPromisesToResolve(); + }) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + allReports = _.filter(allReports, report => report !== null); + + // The same chat report should be reused, and an IOU report should be created + expect(_.size(allReports)).toBe(2); + expect(_.find(allReports, report => report.type === CONST.REPORT.TYPE.CHAT).reportID).toBe(chatReport.reportID); + chatReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.CHAT); + const iouReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.IOU); + iouReportID = iouReport.reportID; + + // They should be linked together + expect(chatReport.iouReportID).toBe(iouReportID); + expect(chatReport.hasOutstandingIOU).toBe(true); + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + + // The chat report should have a CREATED and an IOU action + expect(_.size(allReportActions)).toBe(2); + iouAction = _.find(allReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + + // The CREATED action should not be created after the IOU action + expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + + // The comment should be included in the IOU action + expect(iouAction.originalMessage.comment).toBe(comment); + // The amount in the IOU action should be correct + expect(iouAction.originalMessage.amount).toBe(amount); + + // The IOU action should be pending + expect(iouAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + allTransactions = _.filter(allTransactions, transaction => transaction !== null); + + // There should be one transaction + expect(_.size(allTransactions)).toBe(1); + const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); + transactionID = transaction.transactionID; + + // Its amount should match the amount of the request + expect(transaction.amount).toBe(amount); + + // It should be pending + expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + // The transactionID on the iou action should match the one from the transactions collection + expect(iouAction.originalMessage.IOUTransactionID).toBe(transactionID); + + resolve(); + } + }) + })) + .then(fetch.resume) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, + waitForCollectionCallback: true, + callback: reportActionsForChatReport => { + Onyx.disconnect(connectionID); + expect(_.size(reportActionsForChatReport)).toBe(2); + _.each(reportActionsForChatReport, reportAction => expect(reportAction.pendingAction).toBeFalsy()); + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + waitForCollectionCallback: true, + callback: transaction => { + Onyx.disconnect(connectionID); + expect(transaction.pendingAction).toBeFalsy(); + resolve(); + }, + }); + })); }); it('updates existing IOU report if there is one', () => { From 4b79c279f778c540dcac48b37e2530de2392180e Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 21 Apr 2023 13:00:35 -0400 Subject: [PATCH 16/85] Create test for requestMoney with existing chatReport and iouReport --- tests/actions/IOUTest.js | 161 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 5ca2a3cfafbb..6efd4b1334cd 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -282,7 +282,168 @@ describe('actions/IOU', () => { }); it('updates existing IOU report if there is one', () => { + const amount = 100; + const comment = 'Giv money plz'; + const chatReportID = 1234; + const iouReportID = 5678; + let chatReport = { + reportID: chatReportID, + type: CONST.REPORT.TYPE.CHAT, + hasOutstandingIOU: true, + iouReportID, + participants: [CARLOS_EMAIL], + }; + const createdAction = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: DateUtils.getDBTime(), + }; + const existingTransaction = { + transactionID: NumberUtils.rand64(), + amount: 1000, + comment: '', + created: DateUtils.getDBTime(), + }; + let iouReport = { + reportID: iouReportID, + chatReportID, + type: CONST.REPORT.TYPE.IOU, + ownerEmail: RORY_EMAIL, + managerEmail: CARLOS_EMAIL, + currency: CONST.CURRENCY.USD, + total: existingTransaction.amount, + }; + const iouAction = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorEmail: RORY_EMAIL, + created: DateUtils.getDBTime(), + originalMessage: { + IOUReportID: iouReportID, + IOUTransactionID: existingTransaction.transactionID, + amount: existingTransaction.amount, + currency: CONST.CURRENCY.USD, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + participants: [RORY_EMAIL, CARLOS_EMAIL], + }, + }; + let newIOUAction; + let newTransaction; + fetch.pause(); + return Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, chatReport) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, iouReport)) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { + [createdAction.reportActionID]: createdAction, + [iouAction.reportActionID]: iouAction, + })) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${existingTransaction.transactionID}`, existingTransaction)) + .then(() => { + IOU.requestMoney(chatReport, amount, CONST.CURRENCY.USD, RORY_EMAIL, {login: CARLOS_EMAIL}, comment); + return waitForPromisesToResolve(); + }) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + allReports = _.filter(allReports, report => report !== null); + + // No new reports should be created + expect(_.size(allReports)).toBe(2); + expect(_.find(allReports, report => report.reportID === chatReportID)).toBeTruthy(); + expect(_.find(allReports, report => report.reportID === iouReportID)).toBeTruthy(); + + chatReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.CHAT); + iouReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.IOU); + + // The total on the iou report should be updated + expect(iouReport.total).toBe(1100); + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, + waitForCollectionCallback: true, + callback: (reportActionsForChatReport) => { + Onyx.disconnect(connectionID); + + expect(_.size(reportActionsForChatReport)).toBe(3); + newIOUAction = _.find(reportActionsForChatReport, reportAction => reportAction.reportActionID !== createdAction.reportActionID && reportAction.reportActionID !== iouAction.reportActionID); + // The comment should be included in the IOU action + expect(newIOUAction.originalMessage.comment).toBe(comment); + + // The amount in the IOU action should be correct + expect(newIOUAction.originalMessage.amount).toBe(amount); + + // The IOU action should be pending + expect(newIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + allTransactions = _.filter(allTransactions, transaction => transaction !== null); + + // There should be two transactions + expect(_.size(allTransactions)).toBe(2); + + // The amount on the new transaction should be correct + newTransaction = _.find(allTransactions, transaction => transaction.transactionID !== existingTransaction.transactionID); + expect(newTransaction.amount).toBe(amount); + + // The new transaction should be pending + expect(newTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + // The transactionID on the iou action should match the one from the transactions collection + expect(newIOUAction.originalMessage.IOUTransactionID).toBe(newTransaction.transactionID); + + resolve(); + }, + }); + })) + .then(fetch.resume) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, + waitForCollectionCallback: true, + callback: reportActionsForChatReport => { + Onyx.disconnect(connectionID); + expect(_.size(reportActionsForChatReport)).toBe(3); + _.each(reportActionsForChatReport, reportAction => expect(reportAction.pendingAction).toBeFalsy()); + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + allTransactions = _.filter(allTransactions, transaction => transaction !== null); + + _.each(allTransactions, transaction => expect(transaction.pendingAction).toBeFalsy()); + resolve(); + }, + }); + })); }); it('correctly implements RedBrickRoad error handling', () => { From 619e5cb62a97cf36297dabb5424aafa532715d18 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 21 Apr 2023 13:27:28 -0400 Subject: [PATCH 17/85] Add test for failing scenario --- tests/actions/IOUTest.js | 136 ++++++++++++++++++++++++++++++++++++++ tests/utils/TestHelper.js | 8 ++- 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 6efd4b1334cd..1556ed70b553 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -8,6 +8,7 @@ import * as IOU from '../../src/libs/actions/IOU'; import * as TestHelper from '../utils/TestHelper'; import DateUtils from '../../src/libs/DateUtils'; import * as NumberUtils from '../../src/libs/NumberUtils'; +import * as Localize from '../../src/libs/Localize'; const RORY_EMAIL = 'rory@expensifail.com'; const CARLOS_EMAIL = 'cmartins@expensifail.com'; @@ -447,7 +448,142 @@ describe('actions/IOU', () => { }); it('correctly implements RedBrickRoad error handling', () => { + const amount = 100; + const comment = 'Giv money plz'; + let chatReportID; + let iouReportID; + let createdAction; + let iouAction; + let transactionID; + fetch.pause(); + IOU.requestMoney({}, amount, CONST.CURRENCY.USD, RORY_EMAIL, {login: CARLOS_EMAIL}, comment); + return waitForPromisesToResolve() + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + allReports = _.filter(allReports, report => report !== null); + + // A chat report and an iou report should be created + const chatReports = _.filter(allReports, report => report.type === CONST.REPORT.TYPE.CHAT); + const iouReports = _.filter(allReports, report => report.type === CONST.REPORT.TYPE.IOU); + expect(_.size(chatReports)).toBe(1); + expect(_.size(iouReports)).toBe(1); + const chatReport = chatReports[0]; + chatReportID = chatReport.reportID; + const iouReport = iouReports[0]; + iouReportID = iouReport.reportID; + + // They should be linked together + expect(chatReport.participants).toEqual([CARLOS_EMAIL]); + expect(chatReport.iouReportID).toBe(iouReport.reportID); + expect(chatReport.hasOutstandingIOU).toBe(true); + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, + waitForCollectionCallback: true, + callback: (reportActionsForChatReport) => { + Onyx.disconnect(connectionID); + + // The chat report should have a CREATED action and IOU action + expect(_.size(reportActionsForChatReport)).toBe(2); + const createdActions = _.filter(reportActionsForChatReport, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); + const iouActions = _.filter(reportActionsForChatReport, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + expect(_.size(createdActions)).toBe(1); + expect(_.size(iouActions)).toBe(1); + createdAction = createdActions[0]; + iouAction = iouActions[0]; + + // The CREATED action should not be created after the IOU action + expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + + // The comment should be included in the IOU action + expect(iouAction.originalMessage.comment).toBe(comment); + + // The amount in the IOU action should be correct + expect(iouAction.originalMessage.amount).toBe(amount); + // Both actions should be pending + expect(createdAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(iouAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + allTransactions = _.filter(allTransactions, transaction => transaction !== null); + + // There should be one transaction + expect(_.size(allTransactions)).toBe(1); + const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); + transactionID = transaction.transactionID; + + // Its amount should match the amount of the request + expect(transaction.amount).toBe(amount); + + // It should be pending + expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + // The transactionID on the iou action should match the one from the transactions collection + expect(iouAction.originalMessage.IOUTransactionID).toBe(transactionID); + + resolve(); + }, + }); + })) + .then(() => { + fetch.fail(); + return fetch.resume(); + }) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, + waitForCollectionCallback: true, + callback: reportActionsForChatReport => { + Onyx.disconnect(connectionID); + expect(_.size(reportActionsForChatReport)).toBe(2); + iouAction = _.find(reportActionsForChatReport, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + expect(iouAction.pendingAction).toBeFalsy(); + const errorMessage = _.values(iouAction.errors)[0]; + expect(errorMessage).toBe(Localize.translateLocal('iou.error.genericCreateFailureMessage')); + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + waitForCollectionCallback: true, + callback: transaction => { + Onyx.disconnect(connectionID); + expect(transaction.pendingAction).toBeFalsy(); + expect(transaction.errors).toBeTruthy(); + expect(_.values(transaction.errors)[0]).toBe(Localize.translateLocal('iou.error.genericCreateFailureMessage')); + + // Cleanup + fetch.succeed(); + + resolve(); + }, + }); + })); }); }); }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index a933a72d7b3d..4644bef28ea9 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -158,8 +158,12 @@ function getGlobalFetchMock() { let shouldFail = false; const getResponse = () => shouldFail - ? {ok: false} - : { + ? { + ok: true, + json: () => Promise.resolve({ + jsonCode: 400, + }), + } : { ok: true, json: () => Promise.resolve({ jsonCode: 200 From 83b0bc7e83683b48ac58ae0b822fd7ff21327a07 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 11:24:35 -0400 Subject: [PATCH 18/85] Remove unused PusherHelper --- tests/actions/IOUTest.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 1556ed70b553..9a8b9b3605f4 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -15,7 +15,6 @@ const CARLOS_EMAIL = 'cmartins@expensifail.com'; describe('actions/IOU', () => { beforeAll(() => { - PusherHelper.setup(); Onyx.init({ keys: ONYXKEYS, }); @@ -26,8 +25,6 @@ describe('actions/IOU', () => { return Onyx.clear().then(waitForPromisesToResolve); }); - afterEach(PusherHelper.teardown); - describe('requestMoney', () => { it('creates new chat if needed', () => { const amount = 100; From afda245f87c0b79751dea7089ea039d301c64714 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 13:04:24 -0400 Subject: [PATCH 19/85] Update TODO link --- tests/actions/IOUTest.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 9a8b9b3605f4..e62ce67c4aea 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -180,7 +180,7 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged allReports = _.filter(allReports, report => report !== null); // The same chat report should be reused, and an IOU report should be created @@ -232,7 +232,7 @@ describe('actions/IOU', () => { callback: (allTransactions) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged allTransactions = _.filter(allTransactions, transaction => transaction !== null); // There should be one transaction @@ -346,7 +346,7 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged allReports = _.filter(allReports, report => report !== null); // No new reports should be created @@ -394,7 +394,7 @@ describe('actions/IOU', () => { callback: (allTransactions) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged allTransactions = _.filter(allTransactions, transaction => transaction !== null); // There should be two transactions @@ -434,7 +434,7 @@ describe('actions/IOU', () => { callback: (allTransactions) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged allTransactions = _.filter(allTransactions, transaction => transaction !== null); _.each(allTransactions, transaction => expect(transaction.pendingAction).toBeFalsy()); @@ -462,7 +462,7 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged allReports = _.filter(allReports, report => report !== null); // A chat report and an iou report should be created @@ -524,7 +524,7 @@ describe('actions/IOU', () => { callback: (allTransactions) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/App/pull/16531 is merged + // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged allTransactions = _.filter(allTransactions, transaction => transaction !== null); // There should be one transaction From 3ee3eb8ae3606e3e800ae1d9ff42a4208a08df56 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 15:04:27 -0400 Subject: [PATCH 20/85] Upgrade Onyx to 1.0.41 --- jest/setup.js | 2 +- package-lock.json | 18 +++++++----------- package.json | 2 +- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index 7578679ee111..f1cb9b9ee76b 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -11,7 +11,7 @@ jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); // We have to mock the SQLiteStorage provider because it uses the native module SQLiteStorage, which is not available in jest. // Mocking this file in __mocks__ does not work because jest doesn't support mocking files that are not directly used in the testing project -jest.mock('react-native-onyx/lib/storage/providers/SQLiteStorage', () => require('react-native-onyx/lib/storage/providers/__mocks__/SQLiteStorage')); +jest.mock('react-native-onyx/lib/storage', () => require('react-native-onyx/lib/storage/__mocks__')); // Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise jest.spyOn(console, 'debug').mockImplementation((...params) => { diff --git a/package-lock.json b/package-lock.json index e86eac518485..6e1ee11fef76 100644 --- a/package-lock.json +++ b/package-lock.json @@ -76,7 +76,7 @@ "react-native-key-command": "^1.0.0", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.39", + "react-native-onyx": "1.0.41", "react-native-pdf": "^6.6.2", "react-native-performance": "^4.0.0", "react-native-permissions": "^3.0.1", @@ -34744,9 +34744,9 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.39.tgz", - "integrity": "sha512-W6eH3rC16H3FaSrxEZJkYhBxZTaBJaqusiywqetSqXACVn+GIs00EkI7hlrG/v9ZwCWvs87f/pBPwyGkOQ13+w==", + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.41.tgz", + "integrity": "sha512-+TJlUIEWzy4yGnw4IJx9RvDYC77PL7bLGAejALzv3rXnvtz70dCP/kxb+Cn5dPto1+795c50TxGTW5X48xjpZw==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -34758,7 +34758,6 @@ "npm": "8.11.0" }, "peerDependencies": { - "@react-native-async-storage/async-storage": "^1.17.11", "expensify-common": ">=1", "localforage": "^1.10.0", "localforage-removeitems": "^1.4.0", @@ -34767,9 +34766,6 @@ "react-native-quick-sqlite": "^8.0.0-beta.2" }, "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - }, "localforage": { "optional": true }, @@ -64422,9 +64418,9 @@ } }, "react-native-onyx": { - "version": "1.0.39", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.39.tgz", - "integrity": "sha512-W6eH3rC16H3FaSrxEZJkYhBxZTaBJaqusiywqetSqXACVn+GIs00EkI7hlrG/v9ZwCWvs87f/pBPwyGkOQ13+w==", + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.41.tgz", + "integrity": "sha512-+TJlUIEWzy4yGnw4IJx9RvDYC77PL7bLGAejALzv3rXnvtz70dCP/kxb+Cn5dPto1+795c50TxGTW5X48xjpZw==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index 1c8fa922a614..0b8e260ae45d 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "react-native-key-command": "^1.0.0", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.39", + "react-native-onyx": "1.0.41", "react-native-pdf": "^6.6.2", "react-native-performance": "^4.0.0", "react-native-permissions": "^3.0.1", From 0a1a59bee8fe989b1a73967eade86c04bc9f754e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 15:05:35 -0400 Subject: [PATCH 21/85] Remove resolved TODOS --- tests/actions/IOUTest.js | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index e62ce67c4aea..bb2a4af5b40c 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -180,9 +180,6 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged - allReports = _.filter(allReports, report => report !== null); - // The same chat report should be reused, and an IOU report should be created expect(_.size(allReports)).toBe(2); expect(_.find(allReports, report => report.type === CONST.REPORT.TYPE.CHAT).reportID).toBe(chatReport.reportID); @@ -232,9 +229,6 @@ describe('actions/IOU', () => { callback: (allTransactions) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged - allTransactions = _.filter(allTransactions, transaction => transaction !== null); - // There should be one transaction expect(_.size(allTransactions)).toBe(1); const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); @@ -346,9 +340,6 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged - allReports = _.filter(allReports, report => report !== null); - // No new reports should be created expect(_.size(allReports)).toBe(2); expect(_.find(allReports, report => report.reportID === chatReportID)).toBeTruthy(); @@ -394,9 +385,6 @@ describe('actions/IOU', () => { callback: (allTransactions) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged - allTransactions = _.filter(allTransactions, transaction => transaction !== null); - // There should be two transactions expect(_.size(allTransactions)).toBe(2); @@ -434,9 +422,6 @@ describe('actions/IOU', () => { callback: (allTransactions) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged - allTransactions = _.filter(allTransactions, transaction => transaction !== null); - _.each(allTransactions, transaction => expect(transaction.pendingAction).toBeFalsy()); resolve(); }, @@ -462,9 +447,6 @@ describe('actions/IOU', () => { callback: (allReports) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged - allReports = _.filter(allReports, report => report !== null); - // A chat report and an iou report should be created const chatReports = _.filter(allReports, report => report.type === CONST.REPORT.TYPE.CHAT); const iouReports = _.filter(allReports, report => report.type === CONST.REPORT.TYPE.IOU); @@ -524,9 +506,6 @@ describe('actions/IOU', () => { callback: (allTransactions) => { Onyx.disconnect(connectionID); - // TODO: clean this up after https://github.com/Expensify/react-native-onyx/pull/245 is merged - allTransactions = _.filter(allTransactions, transaction => transaction !== null); - // There should be one transaction expect(_.size(allTransactions)).toBe(1); const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); From d7c84c6e68c28e18ec9f4590a6cb1103acc8ac39 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 15:21:42 -0400 Subject: [PATCH 22/85] Fix IOUUtilsTest --- tests/unit/IOUUtilsTest.js | 289 ++++++++++++++++++------------------- 1 file changed, 142 insertions(+), 147 deletions(-) diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 3fef8221f9ab..835cd747be98 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import CONST from '../../src/CONST'; import * as IOUUtils from '../../src/libs/IOUUtils'; import * as ReportUtils from '../../src/libs/ReportUtils'; +import * as NumberUtils from '../../src/libs/NumberUtils'; import ONYXKEYS from '../../src/ONYXKEYS'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; import currencyList from './currencyList.json'; @@ -11,7 +12,7 @@ let reportActions; const ownerEmail = 'owner@iou.com'; const managerEmail = 'manager@iou.com'; -function createIOUReportAction(type, amount, currency, {IOUTransactionID, isOnline} = {}) { +function createIOUReportAction(type, amount, currency, isOffline = false, IOUTransactionID = NumberUtils.rand64()) { const moneyRequestAction = ReportUtils.buildOptimisticIOUReportAction( type, amount, @@ -24,22 +25,14 @@ function createIOUReportAction(type, amount, currency, {IOUTransactionID, isOnli ); // Default is to create requests offline, if this is specified then we need to remove the pendingAction - moneyRequestAction.pendingAction = isOnline ? null : 'add'; + moneyRequestAction.pendingAction = isOffline ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null; reportActions.push(moneyRequestAction); return moneyRequestAction; } -function cancelMoneyRequest(moneyRequestAction, {isOnline} = {}) { - createIOUReportAction( - 'cancel', - moneyRequestAction.originalMessage.amount, - moneyRequestAction.originalMessage.currency, - { - IOUTransactionID: moneyRequestAction.originalMessage.IOUTransactionID, - isOnline, - }, - ); +function cancelMoneyRequest(moneyRequestAction, isOffline = false) { + createIOUReportAction('cancel', moneyRequestAction.originalMessage.amount, moneyRequestAction.originalMessage.currency, isOffline, moneyRequestAction.originalMessage.IOUTransactionID); } function initCurrencyList() { @@ -52,151 +45,153 @@ function initCurrencyList() { return waitForPromisesToResolve(); } -describe('isIOUReportPendingCurrencyConversion', () => { - beforeEach(() => { - reportActions = []; - const chatReportID = ReportUtils.generateReportID(); - const amount = 1000; - const currency = 'USD'; - - iouReport = ReportUtils.buildOptimisticIOUReport( - ownerEmail, - managerEmail, - amount, - chatReportID, - currency, - CONST.LOCALES.EN, - ); - - // The starting point of all tests is the IOUReport containing a single non-pending transaction in USD - // All requests in the tests are assumed to be offline, unless isOnline is specified - createIOUReportAction('create', amount, currency, {IOUTransactionID: '', isOnline: true}); - }); - - test('Requesting money offline in a different currency will show the pending conversion message', () => { - // Request money offline in AED - createIOUReportAction('create', 100, 'AED'); - - // We requested money offline in a different currency, we don't know the total of the iouReport until we're back online - expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(true); - }); +describe('IOUUtils', () => { + describe('isIOUReportPendingCurrencyConversion', () => { + beforeEach(() => { + reportActions = []; + const chatReportID = ReportUtils.generateReportID(); + const amount = 1000; + const currency = 'USD'; - test('IOUReport is not pending conversion when all requests made offline have been cancelled', () => { - // Create two requests offline - const moneyRequestA = createIOUReportAction('create', 1000, 'AED'); - const moneyRequestB = createIOUReportAction('create', 1000, 'AED'); + iouReport = ReportUtils.buildOptimisticIOUReport( + ownerEmail, + managerEmail, + amount, + chatReportID, + currency, + CONST.LOCALES.EN, + ); - // Cancel both requests - cancelMoneyRequest(moneyRequestA); - cancelMoneyRequest(moneyRequestB); + // The starting point of all tests is the IOUReport containing a single non-pending transaction in USD + // All requests in the tests are assumed to be offline, unless isOnline is specified + createIOUReportAction('create', amount, currency); + }); - // Both requests made offline have been cancelled, total won't update so no need to show a pending conversion message - expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(false); - }); + test('Requesting money offline in a different currency will show the pending conversion message', () => { + // Request money offline in AED + createIOUReportAction('create', 100, 'AED', true); - test('Cancelling a request made online shows the preview', () => { - // Request money online in AED - const moneyRequest = createIOUReportAction('create', 1000, 'AED', {isOnline: true}); + // We requested money offline in a different currency, we don't know the total of the iouReport until we're back online + expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(true); + }); - // Cancel it offline - cancelMoneyRequest(moneyRequest); + test('IOUReport is not pending conversion when all requests made offline have been cancelled', () => { + // Create two requests offline + const moneyRequestA = createIOUReportAction('create', 1000, 'AED', true); + const moneyRequestB = createIOUReportAction('create', 1000, 'AED', true); + + // Cancel both requests + cancelMoneyRequest(moneyRequestA, true); + cancelMoneyRequest(moneyRequestB, true); + + // Both requests made offline have been cancelled, total won't update so no need to show a pending conversion message + expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(false); + }); + + test('Cancelling a request made online shows the preview', () => { + // Request money online in AED + const moneyRequest = createIOUReportAction('create', 1000, 'AED'); + + // Cancel it offline + cancelMoneyRequest(moneyRequest, true); + + // We don't know what the total is because we need to subtract the converted amount of the offline request from the total + expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(true); + }); + + test('Cancelling a request made offline while there\'s a previous one made online will not show the pending conversion message', () => { + // Request money online in AED + createIOUReportAction('create', 1000, 'AED'); + + // Another request offline + const moneyRequestOffline = createIOUReportAction('create', 1000, 'AED', true); + + // Cancel the request made offline + cancelMoneyRequest(moneyRequestOffline, true); + + expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(false); + }); + + test('Cancelling a request made online while we have one made offline will show the pending conversion message', () => { + // Request money online in AED + const moneyRequestOnline = createIOUReportAction('create', 1000, 'AED'); + + // Request money again but offline + createIOUReportAction('create', 1000, 'AED', true); + + // Cancel the request made online + cancelMoneyRequest(moneyRequestOnline, true); + + // We don't know what the total is because we need to subtract the converted amount of the offline request from the total + expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(true); + }); + + test('Cancelling a request offline in the report\'s currency when we have requests in a different currency does not show the pending conversion message', () => { + // Request money in the report's currency (USD) + const onlineMoneyRequestInUSD = createIOUReportAction('create', 1000, 'USD'); + + // Request money online in a different currency + createIOUReportAction('create', 2000, 'AED'); + + // Cancel the USD request offline + cancelMoneyRequest(onlineMoneyRequestInUSD, true); + + expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(false); + }); + }); + + describe('getCurrencyDecimals', () => { + beforeAll(() => initCurrencyList()); + test('Currency decimals smaller than or equal 2', () => { + expect(IOUUtils.getCurrencyDecimals('JPY')).toBe(0); + expect(IOUUtils.getCurrencyDecimals('USD')).toBe(2); + }); + + test('Currency decimals larger than 2 should return 2', () => { + // Actual: 3 + expect(IOUUtils.getCurrencyDecimals('LYD')).toBe(2); + + // Actual: 4 + expect(IOUUtils.getCurrencyDecimals('UYW')).toBe(2); + }); + }); + + describe('getCurrencyUnit', () => { + beforeAll(() => initCurrencyList()); + test('Currency with decimals smaller than or equal 2', () => { + expect(IOUUtils.getCurrencyUnit('JPY')).toBe(1); + expect(IOUUtils.getCurrencyUnit('USD')).toBe(100); + }); - // We don't know what the total is because we need to subtract the converted amount of the offline request from the total - expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(true); + test('Currency with decimals larger than 2 should be floor to 2', () => { + expect(IOUUtils.getCurrencyUnit('LYD')).toBe(100); + }); }); - test('Cancelling a request made offline while there\'s a previous one made online will not show the pending conversion message', () => { - // Request money online in AED - createIOUReportAction('create', 1000, 'AED', {isOnline: true}); - - // Another request offline - const moneyRequestOffline = createIOUReportAction('create', 1000, 'AED'); + describe('calculateAmount', () => { + beforeAll(() => initCurrencyList()); + test('103 JPY split among 3 participants including the default user should be [35, 34, 34]', () => { + const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com']; + expect(IOUUtils.calculateAmount(participants, 103, 'JPY', true)).toBe(3500); + expect(IOUUtils.calculateAmount(participants, 103, 'JPY')).toBe(3400); + }); - // Cancel the request made offline - cancelMoneyRequest(moneyRequestOffline); - - expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(false); - }); + test('10 AFN split among 4 participants including the default user should be [1, 3, 3, 3]', () => { + const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com', 'suestorm@expensify.com']; + expect(IOUUtils.calculateAmount(participants, 10, 'AFN', true)).toBe(100); + expect(IOUUtils.calculateAmount(participants, 10, 'AFN')).toBe(300); + }); - test('Cancelling a request made online while we have one made offline will show the pending conversion message', () => { - // Request money online in AED - const moneyRequestOnline = createIOUReportAction('create', 1000, 'AED', {isOnline: true}); - - // Requet money again but offline - createIOUReportAction('create', 1000, 'AED'); - - // Cancel the request made online - cancelMoneyRequest(moneyRequestOnline); - - // We don't know what the total is because we need to subtract the converted amount of the offline request from the total - expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(true); - }); - - test('Cancelling a request offline in the report\'s currency when we have requests in a different currency does not show the pending conversion message', () => { - // Request money in the report's curreny (USD) - const onlineMoneyRequestInUSD = createIOUReportAction('create', 1000, 'USD', {isOnline: true}); - - // Request money online in a different currency - createIOUReportAction('create', 2000, 'AED', {isOnline: true}); - - // Cancel the USD request offline - cancelMoneyRequest(onlineMoneyRequestInUSD); - - expect(IOUUtils.isIOUReportPendingCurrencyConversion(reportActions, iouReport)).toBe(false); - }); -}); - -describe('getCurrencyDecimals', () => { - beforeAll(() => initCurrencyList()); - test('Currency decimals smaller than or equal 2', () => { - expect(IOUUtils.getCurrencyDecimals('JPY')).toBe(0); - expect(IOUUtils.getCurrencyDecimals('USD')).toBe(2); - }); - - test('Currency decimals larger than 2 should return 2', () => { - // Actual: 3 - expect(IOUUtils.getCurrencyDecimals('LYD')).toBe(2); - - // Actual: 4 - expect(IOUUtils.getCurrencyDecimals('UYW')).toBe(2); - }); -}); - -describe('getCurrencyUnit', () => { - beforeAll(() => initCurrencyList()); - test('Currency with decimals smaller than or equal 2', () => { - expect(IOUUtils.getCurrencyUnit('JPY')).toBe(1); - expect(IOUUtils.getCurrencyUnit('USD')).toBe(100); - }); - - test('Currency with decimals larger than 2 should be floor to 2', () => { - expect(IOUUtils.getCurrencyUnit('LYD')).toBe(100); - }); -}); - -describe('calculateAmount', () => { - beforeAll(() => initCurrencyList()); - test('103 JPY split among 3 participants including the default user should be [35, 34, 34]', () => { - const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 103, 'JPY', true)).toBe(3500); - expect(IOUUtils.calculateAmount(participants, 103, 'JPY')).toBe(3400); - }); - - test('10 AFN split among 4 participants including the default user should be [1, 3, 3, 3]', () => { - const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com', 'suestorm@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 10, 'AFN', true)).toBe(100); - expect(IOUUtils.calculateAmount(participants, 10, 'AFN')).toBe(300); - }); - - test('10 BHD split among 3 participants including the default user should be [334, 333, 333]', () => { - const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 10, 'BHD', true)).toBe(334); - expect(IOUUtils.calculateAmount(participants, 10, 'BHD')).toBe(333); - }); + test('10 BHD split among 3 participants including the default user should be [334, 333, 333]', () => { + const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com']; + expect(IOUUtils.calculateAmount(participants, 10, 'BHD', true)).toBe(334); + expect(IOUUtils.calculateAmount(participants, 10, 'BHD')).toBe(333); + }); - test('0.02 USD split among 4 participants including the default user should be [-1, 1, 1, 1]', () => { - const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com', 'suestorm@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 0.02, 'USD', true)).toBe(-1); - expect(IOUUtils.calculateAmount(participants, 0.02, 'USD')).toBe(1); + test('0.02 USD split among 4 participants including the default user should be [-1, 1, 1, 1]', () => { + const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com', 'suestorm@expensify.com']; + expect(IOUUtils.calculateAmount(participants, 0.02, 'USD', true)).toBe(-1); + expect(IOUUtils.calculateAmount(participants, 0.02, 'USD')).toBe(1); + }); }); }); From 99e89a2ecb651cc99d28f8964725040375ffb7e3 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 15:34:41 -0400 Subject: [PATCH 23/85] Clarify comment on global fetch mock --- tests/utils/TestHelper.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 4644bef28ea9..17411ea72b6f 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -135,9 +135,7 @@ function signOutTestUser() { } /** - * Use for situations where fetch() is required. - * - * It also has some additional methods: + * Use for situations where fetch() is required. This mock is stateful and has some additional methods to control its behavior: * * - pause() – stop resolving promises until you call resume() * - resume() - flush the queue of promises, and start resolving new promises immediately From 00fbb9b7c2bedf4a0c4b38706391d69c55d3747c Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 18:29:09 -0400 Subject: [PATCH 24/85] Fix IOU split transaction amounts --- src/libs/NumberUtils.js | 12 +++++++++++- src/libs/actions/IOU.js | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libs/NumberUtils.js b/src/libs/NumberUtils.js index ef2ac88aaff1..3d54ce30ff67 100644 --- a/src/libs/NumberUtils.js +++ b/src/libs/NumberUtils.js @@ -35,7 +35,17 @@ function rand64() { return left + middleString + rightString; } +/** + * Rounds a number down to the nearest hundred's place. + * + * @param {Number} num + * @returns {Number} + */ +function roundDownToTwoDecimalPlaces(num) { + return Math.floor(num * 100) / 100; +} + export { - // eslint-disable-next-line import/prefer-default-export rand64, + roundDownToTwoDecimalPlaces, }; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index b842d8483111..57c66604c0a7 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -14,6 +14,7 @@ import * as IOUUtils from '../IOUUtils'; import * as OptionsListUtils from '../OptionsListUtils'; import DateUtils from '../DateUtils'; import TransactionUtils from '../TransactionUtils'; +import * as NumberUtils from '../NumberUtils'; const chatReports = {}; const iouReports = {}; @@ -400,7 +401,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment oneOnOneChatReport.iouReportID = oneOnOneIOUReport.reportID; } - const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction(amountInCents, currency); + const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction(NumberUtils.roundDownToTwoDecimalPlaces(amountInCents / (participants.length + 1)), currency); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const oneOnOneCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); From acbedf86bada7c62ba4830c1add5cb2b9fffabf1 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 18:29:46 -0400 Subject: [PATCH 25/85] Add test for SplitBill --- tests/actions/IOUTest.js | 308 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 306 insertions(+), 2 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index bb2a4af5b40c..8f9f80f3a8f2 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; -import PusherHelper from '../utils/PusherHelper'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; import * as IOU from '../../src/libs/actions/IOU'; import * as TestHelper from '../utils/TestHelper'; @@ -10,8 +9,10 @@ import DateUtils from '../../src/libs/DateUtils'; import * as NumberUtils from '../../src/libs/NumberUtils'; import * as Localize from '../../src/libs/Localize'; -const RORY_EMAIL = 'rory@expensifail.com'; const CARLOS_EMAIL = 'cmartins@expensifail.com'; +const JULES_EMAIL = 'jules@expensifail.com'; +const RORY_EMAIL = 'rory@expensifail.com'; +const VIT_EMAIL = 'vit@expensifail.com'; describe('actions/IOU', () => { beforeAll(() => { @@ -562,4 +563,307 @@ describe('actions/IOU', () => { })); }); }); + + describe('split bill', () => { + it('creates and updates new chats and IOUs as needed', () => { + /* + * Given that: + * - Rory and Carlos have chatted before + * - Rory and Jules have chatted before and have an active IOU report + * - Rory and Vit have never chatted together before + * - There is no existing group chat with the four of them + */ + const amount = 400; + const amountInCents = amount * 100; + const comment = 'Yes, I am splitting a bill for $4 USD'; + let carlosChatReport = { + reportID: NumberUtils.rand64(), + type: CONST.REPORT.TYPE.CHAT, + hasOutstandingIOU: false, + participants: [CARLOS_EMAIL], + }; + let carlosCreatedAction = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: DateUtils.getDBTime(), + }; + let julesIOUReportID = NumberUtils.rand64(); + let julesChatReport = { + reportID: NumberUtils.rand64(), + type: CONST.REPORT.TYPE.CHAT, + hasOutstandingIOU: true, + iouReportID: julesIOUReportID, + participants: [JULES_EMAIL], + }; + let julesCreatedAction = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + created: DateUtils.getDBTime(), + }; + jest.advanceTimersByTime(200); + let julesExistingTransaction = { + transactionID: NumberUtils.rand64(), + amount: 1000, + comment: 'This is an existing transaction', + created: DateUtils.getDBTime(), + }; + let julesIOUReport = { + reportID: julesIOUReportID, + chatReportID: julesChatReport.reportID, + type: CONST.REPORT.TYPE.IOU, + ownerEmail: RORY_EMAIL, + managerEmail: JULES_EMAIL, + currency: CONST.CURRENCY.USD, + total: julesExistingTransaction.amount, + }; + let julesExistingIOUAction = { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.IOU, + actorEmail: RORY_EMAIL, + created: DateUtils.getDBTime(), + originalMessage: { + IOUReportID: julesIOUReportID, + IOUTransactionID: julesExistingTransaction.transactionID, + amount: julesExistingTransaction.amount, + currency: CONST.CURRENCY.USD, + type: CONST.IOU.REPORT_ACTION_TYPE.CREATE, + participants: [RORY_EMAIL, JULES_EMAIL], + }, + }; + + let carlosIOUReport; + let carlosIOUAction; + let carlosTransaction; + + let julesIOUAction; + let julesTransaction; + + let vitChatReport; + let vitIOUReport; + let vitCreatedAction; + let vitIOUAction; + let vitTransaction; + + let groupChat; + let groupCreatedAction; + let groupIOUAction; + + return Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, { + [`${ONYXKEYS.COLLECTION.REPORT}${carlosChatReport.reportID}`]: carlosChatReport, + [`${ONYXKEYS.COLLECTION.REPORT}${julesChatReport.reportID}`]: julesChatReport, + [`${ONYXKEYS.COLLECTION.REPORT}${julesIOUReport.reportID}`]: julesIOUReport, + }) + .then(() => Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${carlosChatReport.reportID}`]: { + [carlosCreatedAction.reportActionID]: carlosCreatedAction, + }, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${julesChatReport.reportID}`]: { + [julesCreatedAction.reportActionID]: julesCreatedAction, + [julesExistingIOUAction.reportActionID]: julesExistingIOUAction, + }, + })) + .then(() => Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction.transactionID}`, julesExistingTransaction)) + .then(() => { + // When we split a bill offline + fetch.pause(); + IOU.splitBill( + _.map([CARLOS_EMAIL, JULES_EMAIL, VIT_EMAIL], email => ({login: email})), + RORY_EMAIL, + amount, + comment, + CONST.CURRENCY.USD, + CONST.LOCALES.DEFAULT, + ); + return waitForPromisesToResolve(); + }) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + + // There should now be 7 reports + expect(_.size(allReports)).toBe(7); + + // 1. The chat report with Rory + Carlos + carlosChatReport = _.find(allReports, report => report.reportID === carlosChatReport.reportID); + expect(_.isEmpty(carlosChatReport)).toBe(false); + expect(carlosChatReport.pendingFields).toBeFalsy(); + + // 2. The IOU report with Rory + Carlos (new) + carlosIOUReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.IOU && report.managerEmail === CARLOS_EMAIL); + expect(_.isEmpty(carlosIOUReport)).toBe(false); + expect(carlosIOUReport.total).toBe(amountInCents / 4); + + // 3. The chat report with Rory + Jules + julesChatReport = _.find(allReports, report => report.reportID === julesChatReport.reportID); + expect(_.isEmpty(julesChatReport)).toBe(false); + expect(julesChatReport.pendingFields).toBeFalsy(); + + // 4. The IOU report with Rory + Jules + julesIOUReport = _.find(allReports, report => report.reportID === julesIOUReport.reportID); + expect(_.isEmpty(julesIOUReport)).toBe(false); + expect(julesChatReport.pendingFields).toBeFalsy(); + expect(julesIOUReport.total).toBe(julesExistingTransaction.amount + (amountInCents / 4)); + + // 5. The chat report with Rory + Vit (new) + vitChatReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.CHAT && _.isEqual(report.participants, [VIT_EMAIL])); + expect(_.isEmpty(vitChatReport)).toBe(false); + expect(vitChatReport.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + + // 6. The IOU report with Rory + Vit (new) + vitIOUReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.IOU && report.managerEmail === VIT_EMAIL); + expect(_.isEmpty(vitIOUReport)).toBe(false); + expect(vitIOUReport.total).toBe(amountInCents / 4); + + // 7. The group chat with everyone + groupChat = _.find(allReports, report => report.type === CONST.REPORT.TYPE.CHAT && _.isEqual(report.participants, [CARLOS_EMAIL, JULES_EMAIL, VIT_EMAIL])); + expect(_.isEmpty(groupChat)).toBe(false); + expect(groupChat.pendingFields).toStrictEqual({createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + + // The 1:1 chat reports and the IOU reports should be linked together + expect(carlosChatReport.hasOutstandingIOU).toBe(true); + expect(carlosChatReport.iouReportID).toBe(carlosIOUReport.reportID); + expect(carlosIOUReport.chatReportID).toBe(carlosChatReport.reportID); + + expect(julesChatReport.hasOutstandingIOU).toBe(true); + expect(julesChatReport.iouReportID).toBe(julesIOUReport.reportID); + expect(julesIOUReport.chatReportID).toBe(julesChatReport.reportID); + + expect(vitChatReport.hasOutstandingIOU).toBe(true); + expect(vitChatReport.iouReportID).toBe(vitIOUReport.reportID); + expect(vitIOUReport.chatReportID).toBe(vitChatReport.reportID) + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + + // There should be reportActions on all 4 chat reports + expect(_.size(allReportActions)).toBe(4); + + const carlosReportActions = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${carlosChatReport.reportID}`]; + const julesReportActions = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${julesChatReport.reportID}`]; + const vitReportActions = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${vitChatReport.reportID}`]; + const groupReportActions = allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${groupChat.reportID}`]; + + // Carlos DM should have two reportActions – the existing CREATED action and an pending IOU action + expect(_.size(carlosReportActions)).toBe(2); + expect(carlosReportActions[carlosCreatedAction.reportActionID]).toStrictEqual(carlosCreatedAction); + carlosIOUAction = _.find(carlosReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + expect(carlosIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(carlosIOUAction.originalMessage.amount).toBe(amountInCents / 4); + expect(carlosIOUAction.originalMessage.comment).toBe(comment); + expect(Date.parse(carlosCreatedAction.created)).toBeLessThanOrEqual(Date.parse(carlosIOUAction.created)); + + // Jules DM should have three reportActions, the existing CREATED action, the existing IOU action, and a new pending IOU action + expect(_.size(julesReportActions)).toBe(3); + expect(julesReportActions[julesCreatedAction.reportActionID]).toStrictEqual(julesCreatedAction); + julesIOUAction = _.find(julesReportActions, reportAction => ( + reportAction.reportActionID !== julesCreatedAction.reportActionID + && reportAction.reportActionID !== julesExistingIOUAction.reportActionID + )); + expect(julesIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(julesIOUAction.originalMessage.amount).toBe(amountInCents / 4); + expect(julesIOUAction.originalMessage.comment).toBe(comment); + expect(Date.parse(julesCreatedAction.created)).toBeLessThanOrEqual(Date.parse(julesIOUAction.created)); + + // Vit DM should have two reportActions – a pending CREATED action and a pending IOU action + expect(_.size(vitReportActions)).toBe(2); + vitCreatedAction = _.find(vitReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); + vitIOUAction = _.find(vitReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + expect(vitCreatedAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitIOUAction.originalMessage.amount).toBe(amountInCents / 4); + expect(vitIOUAction.originalMessage.comment).toBe(comment); + expect(Date.parse(vitCreatedAction.created)).toBeLessThanOrEqual(Date.parse(vitIOUAction.created)) + + // Group chat should have two reportActions – a pending CREATED action and a pending IOU action w/ type SPLIT + expect(_.size(groupReportActions)).toBe(2); + groupCreatedAction = _.find(groupReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED); + groupIOUAction = _.find(groupReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + expect(groupCreatedAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.SPLIT); + expect(Date.parse(groupCreatedAction.created)).toBeLessThanOrEqual(Date.parse(groupIOUAction.created)); + + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + + // There should be 4 transactions – one existing one with Jules and one for each of the three IOU reports + // TODO: I think this is wrong and there should only be 4 transactions + expect(_.size(allTransactions)).toBe(5); + expect(allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction.transactionID}`]).toBeTruthy(); + + carlosTransaction = _.find(allTransactions, transaction => transaction.transactionID === carlosIOUAction.originalMessage.IOUTransactionID); + julesTransaction = _.find(allTransactions, transaction => transaction.transactionID === julesIOUAction.originalMessage.IOUTransactionID); + vitTransaction = _.find(allTransactions, transaction => transaction.transactionID === vitIOUAction.originalMessage.IOUTransactionID); + + expect(carlosTransaction.amount).toBe(amountInCents / 4); + expect(julesTransaction.amount).toBe(amountInCents / 4); + expect(vitTransaction.amount).toBe(amountInCents / 4); + + expect(carlosTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(julesTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + + resolve(); + }, + }) + })) + .then(fetch.resume) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + _.each(allReports, report => { + if (report.pendingFields) { + _.each(report.pendingFields, pendingField => expect(pendingField).toBeFalsy()); + } + }); + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + _.each(allReportActions, reportAction => expect(reportAction.pendingAction).toBeFalsy()); + resolve(); + }, + }); + })) + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + _.each(allTransactions, transaction => expect(transaction.pendingAction).toBeFalsy()); + resolve(); + }, + }); + })); + }); + }); }); From 12a27a913c7946cde9db7eee5d9a7d9d1f92ea1f Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 18:30:04 -0400 Subject: [PATCH 26/85] Standardize on cents for transactions --- src/libs/actions/IOU.js | 4 ++-- tests/actions/IOUTest.js | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 57c66604c0a7..254a7e068882 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -77,7 +77,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com iouReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, debtorEmail, amount, chatReport.reportID, currency, preferredLocale); } - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency); const optimisticTransactionData = { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, @@ -762,7 +762,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType } const optimisticIOUReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, managerEmail, amount, chatReport.reportID, currency, preferredLocale, true); - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency, comment); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, comment); const optimisticTransactionData = { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 8f9f80f3a8f2..c79116ace6a8 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -29,6 +29,7 @@ describe('actions/IOU', () => { describe('requestMoney', () => { it('creates new chat if needed', () => { const amount = 100; + const amountInCents = amount * 100; const comment = 'Giv money plz'; let chatReportID; let iouReportID; @@ -110,7 +111,7 @@ describe('actions/IOU', () => { transactionID = transaction.transactionID; // Its amount should match the amount of the request - expect(transaction.amount).toBe(amount); + expect(transaction.amount).toBe(amountInCents); // It should be pending expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -150,6 +151,7 @@ describe('actions/IOU', () => { it('updates existing chat report if there is one', () => { const amount = 100; + const amountInCents = amount * 100; const comment = 'Giv money plz'; let chatReport = { reportID: 1234, @@ -236,7 +238,7 @@ describe('actions/IOU', () => { transactionID = transaction.transactionID; // Its amount should match the amount of the request - expect(transaction.amount).toBe(amount); + expect(transaction.amount).toBe(amountInCents); // It should be pending expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -276,6 +278,7 @@ describe('actions/IOU', () => { it('updates existing IOU report if there is one', () => { const amount = 100; + const amountInCents = amount * 100; const comment = 'Giv money plz'; const chatReportID = 1234; const iouReportID = 5678; @@ -391,7 +394,7 @@ describe('actions/IOU', () => { // The amount on the new transaction should be correct newTransaction = _.find(allTransactions, transaction => transaction.transactionID !== existingTransaction.transactionID); - expect(newTransaction.amount).toBe(amount); + expect(newTransaction.amount).toBe(amountInCents); // The new transaction should be pending expect(newTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -432,6 +435,7 @@ describe('actions/IOU', () => { it('correctly implements RedBrickRoad error handling', () => { const amount = 100; + const amountInCents = amount * 100; const comment = 'Giv money plz'; let chatReportID; let iouReportID; @@ -513,7 +517,7 @@ describe('actions/IOU', () => { transactionID = transaction.transactionID; // Its amount should match the amount of the request - expect(transaction.amount).toBe(amount); + expect(transaction.amount).toBe(amountInCents); // It should be pending expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); From f745bf6f7d15118bfe4a3be9ad1d0480a53c6230 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 24 Apr 2023 19:12:24 -0400 Subject: [PATCH 27/85] Add assertions for IOU type --- tests/actions/IOUTest.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index c79116ace6a8..85ac2d883477 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -90,6 +90,9 @@ describe('actions/IOU', () => { // The amount in the IOU action should be correct expect(iouAction.originalMessage.amount).toBe(amount); + // The IOU type should be correct + expect(iouAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + // Both actions should be pending expect(createdAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(iouAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -218,6 +221,9 @@ describe('actions/IOU', () => { // The amount in the IOU action should be correct expect(iouAction.originalMessage.amount).toBe(amount); + // The IOU action type should be correct + expect(iouAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + // The IOU action should be pending expect(iouAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -375,6 +381,9 @@ describe('actions/IOU', () => { // The amount in the IOU action should be correct expect(newIOUAction.originalMessage.amount).toBe(amount); + // The type of the IOU action should be correct + expect(newIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + // The IOU action should be pending expect(newIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -496,6 +505,9 @@ describe('actions/IOU', () => { // The amount in the IOU action should be correct expect(iouAction.originalMessage.amount).toBe(amount); + // The type should be correct + expect(iouAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); + // Both actions should be pending expect(createdAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(iouAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -765,6 +777,7 @@ describe('actions/IOU', () => { expect(carlosIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(carlosIOUAction.originalMessage.amount).toBe(amountInCents / 4); expect(carlosIOUAction.originalMessage.comment).toBe(comment); + expect(carlosIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); expect(Date.parse(carlosCreatedAction.created)).toBeLessThanOrEqual(Date.parse(carlosIOUAction.created)); // Jules DM should have three reportActions, the existing CREATED action, the existing IOU action, and a new pending IOU action @@ -777,6 +790,7 @@ describe('actions/IOU', () => { expect(julesIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(julesIOUAction.originalMessage.amount).toBe(amountInCents / 4); expect(julesIOUAction.originalMessage.comment).toBe(comment); + expect(julesIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); expect(Date.parse(julesCreatedAction.created)).toBeLessThanOrEqual(Date.parse(julesIOUAction.created)); // Vit DM should have two reportActions – a pending CREATED action and a pending IOU action @@ -787,6 +801,7 @@ describe('actions/IOU', () => { expect(vitIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(vitIOUAction.originalMessage.amount).toBe(amountInCents / 4); expect(vitIOUAction.originalMessage.comment).toBe(comment); + expect(vitIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); expect(Date.parse(vitCreatedAction.created)).toBeLessThanOrEqual(Date.parse(vitIOUAction.created)) // Group chat should have two reportActions – a pending CREATED action and a pending IOU action w/ type SPLIT @@ -810,7 +825,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); // There should be 4 transactions – one existing one with Jules and one for each of the three IOU reports - // TODO: I think this is wrong and there should only be 4 transactions + // TODO: I think this might be wrong and there should only be 4 transactions expect(_.size(allTransactions)).toBe(5); expect(allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction.transactionID}`]).toBeTruthy(); From 95112807944e7bdbdb9012708f0c68f499343770 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 25 Apr 2023 08:08:36 -0400 Subject: [PATCH 28/85] Add assertions for originalMessage.IOUReportID --- tests/actions/IOUTest.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 85ac2d883477..18a070659742 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -84,6 +84,9 @@ describe('actions/IOU', () => { // The CREATED action should not be created after the IOU action expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + // The IOUReportID should be correct + expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); + // The comment should be included in the IOU action expect(iouAction.originalMessage.comment).toBe(comment); @@ -215,6 +218,9 @@ describe('actions/IOU', () => { // The CREATED action should not be created after the IOU action expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + // The IOUReportID should be correct + expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); + // The comment should be included in the IOU action expect(iouAction.originalMessage.comment).toBe(comment); @@ -375,6 +381,9 @@ describe('actions/IOU', () => { expect(_.size(reportActionsForChatReport)).toBe(3); newIOUAction = _.find(reportActionsForChatReport, reportAction => reportAction.reportActionID !== createdAction.reportActionID && reportAction.reportActionID !== iouAction.reportActionID); + // The IOUReportID should be correct + expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); + // The comment should be included in the IOU action expect(newIOUAction.originalMessage.comment).toBe(comment); @@ -499,6 +508,9 @@ describe('actions/IOU', () => { // The CREATED action should not be created after the IOU action expect(Date.parse(createdAction.created)).toBeLessThanOrEqual(Date.parse(iouAction.created)); + // The IOUReportID should be correct + expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); + // The comment should be included in the IOU action expect(iouAction.originalMessage.comment).toBe(comment); @@ -775,6 +787,7 @@ describe('actions/IOU', () => { expect(carlosReportActions[carlosCreatedAction.reportActionID]).toStrictEqual(carlosCreatedAction); carlosIOUAction = _.find(carlosReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); expect(carlosIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(carlosIOUAction.originalMessage.IOUReportID).toBe(carlosIOUReport.reportID); expect(carlosIOUAction.originalMessage.amount).toBe(amountInCents / 4); expect(carlosIOUAction.originalMessage.comment).toBe(comment); expect(carlosIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); @@ -788,6 +801,7 @@ describe('actions/IOU', () => { && reportAction.reportActionID !== julesExistingIOUAction.reportActionID )); expect(julesIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(julesIOUAction.originalMessage.IOUReportID).toBe(julesIOUReport.reportID); expect(julesIOUAction.originalMessage.amount).toBe(amountInCents / 4); expect(julesIOUAction.originalMessage.comment).toBe(comment); expect(julesIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); @@ -799,6 +813,7 @@ describe('actions/IOU', () => { vitIOUAction = _.find(vitReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); expect(vitCreatedAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(vitIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(vitIOUAction.originalMessage.IOUReportID).toBe(vitIOUReport.reportID); expect(vitIOUAction.originalMessage.amount).toBe(amountInCents / 4); expect(vitIOUAction.originalMessage.comment).toBe(comment); expect(vitIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); @@ -810,6 +825,7 @@ describe('actions/IOU', () => { groupIOUAction = _.find(groupReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); expect(groupCreatedAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(groupIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupIOUAction.originalMessage).not.toHaveProperty('IOUReportID'); expect(groupIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.SPLIT); expect(Date.parse(groupCreatedAction.created)).toBeLessThanOrEqual(Date.parse(groupIOUAction.created)); From bc1cf76463b2d45e02666d4de4c12daf0c5fa611 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 25 Apr 2023 08:23:02 -0400 Subject: [PATCH 29/85] Add reportID to transaction --- src/CONST.js | 1 + src/libs/TransactionUtils.js | 4 +++- src/libs/actions/IOU.js | 16 +++++++++++----- tests/actions/IOUTest.js | 19 ++++++++++++++++++- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index 568aa09ebe6c..c701320f5e9d 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -390,6 +390,7 @@ const CONST = { DROP_NATIVE_ID: 'report-dropzone', ACTIVE_DROP_NATIVE_ID: 'report-dropzone', MAXIMUM_PARTICIPANTS: 8, + ID_DELETED: '-2', ACTIONS: { LIMIT: 50, TYPE: { diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index aaf6b9ca075c..0102d68f175f 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -7,10 +7,11 @@ import * as NumberUtils from './NumberUtils'; * * @param {Number} amount – in cents * @param {String} currency + * @param {String} reportID * @param {String} comment * @returns {Object} */ -function buildOptimisticTransaction(amount, currency, comment = '') { +function buildOptimisticTransaction(amount, currency, reportID, comment = '') { // transactionIDs are random, positive, 64-bit numbers. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) const transactionID = NumberUtils.rand64(); @@ -18,6 +19,7 @@ function buildOptimisticTransaction(amount, currency, comment = '') { transactionID, amount, comment, + reportID, created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 254a7e068882..5459ea37bfe3 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -77,7 +77,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com iouReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, debtorEmail, amount, chatReport.reportID, currency, preferredLocale); } - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, iouReport.reportID); const optimisticTransactionData = { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, @@ -267,7 +267,9 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment const groupChatReport = existingGroupChatReport || ReportUtils.buildOptimisticChatReport(participantLogins); const amountInCents = Math.round(amount * 100); - const groupTransaction = TransactionUtils.buildOptimisticTransaction(amountInCents, currency); + + // ReportID is -2 (aka "deleted") on the group transaction: https://github.com/Expensify/Auth/blob/3fa2698654cd4fbc30f9de38acfca3fbeb7842e4/auth/command/SplitTransaction.cpp#L24-L27 + const groupTransaction = TransactionUtils.buildOptimisticTransaction(amountInCents, currency, CONST.REPORT.ID_DELETED); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const groupCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); @@ -401,7 +403,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment oneOnOneChatReport.iouReportID = oneOnOneIOUReport.reportID; } - const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction(NumberUtils.roundDownToTwoDecimalPlaces(amountInCents / (participants.length + 1)), currency); + const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction( + NumberUtils.roundDownToTwoDecimalPlaces(amountInCents / (participants.length + 1)), + currency, + oneOnOneIOUReport.reportID + ); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const oneOnOneCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); @@ -762,7 +768,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType } const optimisticIOUReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, managerEmail, amount, chatReport.reportID, currency, preferredLocale, true); - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, comment); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, optimisticIOUReport.reportID, comment); const optimisticTransactionData = { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, @@ -902,7 +908,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType * @returns {Object} */ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMethodType) { - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(iouReport.total, iouReport.currency); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(iouReport.total, iouReport.currency, iouReport.reportID); const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.PAY, iouReport.total, diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 18a070659742..eb9a91814577 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -116,6 +116,9 @@ describe('actions/IOU', () => { const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); transactionID = transaction.transactionID; + // The transaction should be attached to the IOU report + expect(transaction.reportID).toBe(iouReportID); + // Its amount should match the amount of the request expect(transaction.amount).toBe(amountInCents); @@ -249,6 +252,9 @@ describe('actions/IOU', () => { const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); transactionID = transaction.transactionID; + // The transaction should be attached to the IOU report + expect(transaction.reportID).toBe(iouReportID); + // Its amount should match the amount of the request expect(transaction.amount).toBe(amountInCents); @@ -410,8 +416,12 @@ describe('actions/IOU', () => { // There should be two transactions expect(_.size(allTransactions)).toBe(2); - // The amount on the new transaction should be correct newTransaction = _.find(allTransactions, transaction => transaction.transactionID !== existingTransaction.transactionID); + + // The transaction should be attached to the IOU report + expect(newTransaction.reportID).toBe(iouReportID); + + // The amount on the new transaction should be correct expect(newTransaction.amount).toBe(amountInCents); // The new transaction should be pending @@ -540,6 +550,9 @@ describe('actions/IOU', () => { const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); transactionID = transaction.transactionID; + // The transaction should be attached to the IOU report + expect(transaction.reportID).toBe(iouReportID); + // Its amount should match the amount of the request expect(transaction.amount).toBe(amountInCents); @@ -849,6 +862,10 @@ describe('actions/IOU', () => { julesTransaction = _.find(allTransactions, transaction => transaction.transactionID === julesIOUAction.originalMessage.IOUTransactionID); vitTransaction = _.find(allTransactions, transaction => transaction.transactionID === vitIOUAction.originalMessage.IOUTransactionID); + expect(carlosTransaction.reportID).toBe(carlosIOUReport.reportID); + expect(julesTransaction.reportID).toBe(julesIOUReport.reportID); + expect(vitTransaction.reportID).toBe(vitIOUReport.reportID); + expect(carlosTransaction.amount).toBe(amountInCents / 4); expect(julesTransaction.amount).toBe(amountInCents / 4); expect(vitTransaction.amount).toBe(amountInCents / 4); From a264814594edb6fa037be6256a515557875390fb Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 25 Apr 2023 08:26:26 -0400 Subject: [PATCH 30/85] Update SplitBill test to account for group transaction --- tests/actions/IOUTest.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index eb9a91814577..9a951f1fa417 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -688,6 +688,7 @@ describe('actions/IOU', () => { let groupChat; let groupCreatedAction; let groupIOUAction; + let groupTransaction; return Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, { [`${ONYXKEYS.COLLECTION.REPORT}${carlosChatReport.reportID}`]: carlosChatReport, @@ -853,26 +854,33 @@ describe('actions/IOU', () => { callback: (allTransactions) => { Onyx.disconnect(connectionID); - // There should be 4 transactions – one existing one with Jules and one for each of the three IOU reports - // TODO: I think this might be wrong and there should only be 4 transactions + /* There should be 5 transactions + * – one existing one with Jules + * - one for each of the three IOU reports + * - one on the group chat w/ deleted report + */ expect(_.size(allTransactions)).toBe(5); expect(allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${julesExistingTransaction.transactionID}`]).toBeTruthy(); carlosTransaction = _.find(allTransactions, transaction => transaction.transactionID === carlosIOUAction.originalMessage.IOUTransactionID); julesTransaction = _.find(allTransactions, transaction => transaction.transactionID === julesIOUAction.originalMessage.IOUTransactionID); vitTransaction = _.find(allTransactions, transaction => transaction.transactionID === vitIOUAction.originalMessage.IOUTransactionID); + groupTransaction = _.find(allTransactions, transaction => transaction.reportID === CONST.REPORT.ID_DELETED); expect(carlosTransaction.reportID).toBe(carlosIOUReport.reportID); expect(julesTransaction.reportID).toBe(julesIOUReport.reportID); expect(vitTransaction.reportID).toBe(vitIOUReport.reportID); + expect(groupTransaction).toBeTruthy(); expect(carlosTransaction.amount).toBe(amountInCents / 4); expect(julesTransaction.amount).toBe(amountInCents / 4); expect(vitTransaction.amount).toBe(amountInCents / 4); + expect(groupTransaction.amount).toBe(amountInCents); expect(carlosTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(julesTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(vitTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); + expect(groupTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); resolve(); }, From ff72d4f27a887dccb544837343e2e6245cdb5226 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 25 Apr 2023 09:11:01 -0400 Subject: [PATCH 31/85] Add comment field to optimistic transactions --- src/libs/TransactionUtils.js | 17 ++++++++++++++--- src/libs/actions/IOU.js | 9 ++++++--- tests/actions/IOUTest.js | 25 +++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 0102d68f175f..8575e2e68cf8 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -8,18 +8,29 @@ import * as NumberUtils from './NumberUtils'; * @param {Number} amount – in cents * @param {String} currency * @param {String} reportID - * @param {String} comment + * @param {String} [comment] + * @param {String} [source] + * @param {String} [originalTransactionID] * @returns {Object} */ -function buildOptimisticTransaction(amount, currency, reportID, comment = '') { +function buildOptimisticTransaction(amount, currency, reportID, comment = '', source = '', originalTransactionID = '') { // transactionIDs are random, positive, 64-bit numbers. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) const transactionID = NumberUtils.rand64(); + + const commentJSON = {comment}; + if (source) { + commentJSON.source = source; + } + if (originalTransactionID) { + commentJSON.originalTransactionID = originalTransactionID; + } + return { transactionID, amount, - comment, reportID, + comment: commentJSON, created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 5459ea37bfe3..982bb6adb9e7 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -77,7 +77,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com iouReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, debtorEmail, amount, chatReport.reportID, currency, preferredLocale); } - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, iouReport.reportID); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, iouReport.reportID, comment); const optimisticTransactionData = { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, @@ -269,7 +269,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment const amountInCents = Math.round(amount * 100); // ReportID is -2 (aka "deleted") on the group transaction: https://github.com/Expensify/Auth/blob/3fa2698654cd4fbc30f9de38acfca3fbeb7842e4/auth/command/SplitTransaction.cpp#L24-L27 - const groupTransaction = TransactionUtils.buildOptimisticTransaction(amountInCents, currency, CONST.REPORT.ID_DELETED); + const groupTransaction = TransactionUtils.buildOptimisticTransaction(amountInCents, currency, CONST.REPORT.ID_DELETED, comment); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const groupCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); @@ -406,7 +406,10 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction( NumberUtils.roundDownToTwoDecimalPlaces(amountInCents / (participants.length + 1)), currency, - oneOnOneIOUReport.reportID + oneOnOneIOUReport.reportID, + comment, + CONST.IOU.MONEY_REQUEST_TYPE.SPLIT, + groupTransaction.transactionID, ); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 9a951f1fa417..60cec5f0ff35 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -122,6 +122,9 @@ describe('actions/IOU', () => { // Its amount should match the amount of the request expect(transaction.amount).toBe(amountInCents); + // The comment should be correct + expect(transaction.comment.comment).toBe(comment); + // It should be pending expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -258,6 +261,9 @@ describe('actions/IOU', () => { // Its amount should match the amount of the request expect(transaction.amount).toBe(amountInCents); + // The comment should be correct + expect(transaction.comment.comment).toBe(comment); + // It should be pending expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -424,6 +430,9 @@ describe('actions/IOU', () => { // The amount on the new transaction should be correct expect(newTransaction.amount).toBe(amountInCents); + // The comment should be correct + expect(newTransaction.comment.comment).toBe(comment); + // The new transaction should be pending expect(newTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -556,6 +565,9 @@ describe('actions/IOU', () => { // Its amount should match the amount of the request expect(transaction.amount).toBe(amountInCents); + // The comment should be correct + expect(transaction.comment.comment).toBe(comment); + // It should be pending expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -877,6 +889,19 @@ describe('actions/IOU', () => { expect(vitTransaction.amount).toBe(amountInCents / 4); expect(groupTransaction.amount).toBe(amountInCents); + expect(carlosTransaction.comment.comment).toBe(comment); + expect(julesTransaction.comment.comment).toBe(comment); + expect(vitTransaction.comment.comment).toBe(comment); + expect(groupTransaction.comment.comment).toBe(comment); + + expect(carlosTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); + expect(julesTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); + expect(vitTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); + + expect(carlosTransaction.comment.originalTransactionID).toBe(groupTransaction.transactionID); + expect(julesTransaction.comment.originalTransactionID).toBe(groupTransaction.transactionID); + expect(vitTransaction.comment.originalTransactionID).toBe(groupTransaction.transactionID); + expect(carlosTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(julesTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(vitTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); From f34a677d8263c630571353f1c822509b1fc75f24 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 25 Apr 2023 11:31:55 -0400 Subject: [PATCH 32/85] Add string formatting util --- src/libs/StringUtils.js | 33 +++++++++++++++++++++++++++++++++ tests/unit/StringUtilsTest.js | 14 ++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/libs/StringUtils.js create mode 100644 tests/unit/StringUtilsTest.js diff --git a/src/libs/StringUtils.js b/src/libs/StringUtils.js new file mode 100644 index 000000000000..8cadcbbbaa24 --- /dev/null +++ b/src/libs/StringUtils.js @@ -0,0 +1,33 @@ +import _ from 'underscore'; + +/** + * Converts an array of strings to a spoken list, like: + * + * ['rory', 'vit', 'jules'] => 'rory, vit, and jules' + * + * @param {Array} arr + * @returns {String} + */ +function arrayToSpokenList(arr) { + if (_.isEmpty(arr)) { + return ''; + } + + if (arr.length === 1) { + return arr[0]; + } + + if (arr.length === 2) { + return `${arr[0]} and ${arr[1]}`; + } + + let result = arr[0]; + for (let i = 1; i < arr.length - 1; i++) { + result += `, ${arr[i]}`; + } + return `${result}, and ${arr[arr.length - 1]}`; +} + +export default { + arrayToSpokenList, +}; diff --git a/tests/unit/StringUtilsTest.js b/tests/unit/StringUtilsTest.js new file mode 100644 index 000000000000..8df2048bf5b1 --- /dev/null +++ b/tests/unit/StringUtilsTest.js @@ -0,0 +1,14 @@ +import StringUtils from '../../src/libs/StringUtils'; + +describe('StringUtils', () => { + describe('arrayToSpokenList', () => { + test.each([ + [[], ''], + [['rory'], 'rory'], + [['rory', 'vit'], 'rory and vit'], + [['rory', 'vit', 'jules'], 'rory, vit, and jules'], + ])(`arrayToSpokenList(%s)`, (input, expectedOutput) => { + expect(StringUtils.arrayToSpokenList(input)).toBe(expectedOutput); + }); + }); +}); From bf1586f1d0e550de85515d1ff56b23cd1777c164 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 25 Apr 2023 11:45:32 -0400 Subject: [PATCH 33/85] Add merchant to transactions --- src/libs/TransactionUtils.js | 3 ++- src/libs/actions/IOU.js | 11 ++++++++++- tests/actions/IOUTest.js | 25 +++++++++++-------------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 8575e2e68cf8..d4d625ccea4d 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -13,7 +13,7 @@ import * as NumberUtils from './NumberUtils'; * @param {String} [originalTransactionID] * @returns {Object} */ -function buildOptimisticTransaction(amount, currency, reportID, comment = '', source = '', originalTransactionID = '') { +function buildOptimisticTransaction(amount, currency, reportID, comment = '', source = '', originalTransactionID = '', merchant = CONST.REPORT.TYPE.IOU) { // transactionIDs are random, positive, 64-bit numbers. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) const transactionID = NumberUtils.rand64(); @@ -31,6 +31,7 @@ function buildOptimisticTransaction(amount, currency, reportID, comment = '', so amount, reportID, comment: commentJSON, + merchant, created: DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 982bb6adb9e7..747ef68ef453 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -15,6 +15,7 @@ import * as OptionsListUtils from '../OptionsListUtils'; import DateUtils from '../DateUtils'; import TransactionUtils from '../TransactionUtils'; import * as NumberUtils from '../NumberUtils'; +import StringUtils from '../StringUtils'; const chatReports = {}; const iouReports = {}; @@ -269,7 +270,15 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment const amountInCents = Math.round(amount * 100); // ReportID is -2 (aka "deleted") on the group transaction: https://github.com/Expensify/Auth/blob/3fa2698654cd4fbc30f9de38acfca3fbeb7842e4/auth/command/SplitTransaction.cpp#L24-L27 - const groupTransaction = TransactionUtils.buildOptimisticTransaction(amountInCents, currency, CONST.REPORT.ID_DELETED, comment); + const groupTransaction = TransactionUtils.buildOptimisticTransaction( + amountInCents, + currency, + CONST.REPORT.ID_DELETED, + comment, + '', + '', + `Split Bill with ${StringUtils.arrayToSpokenList([currentUserLogin, ..._.map(participants, participant => participant.login)])} [${DateUtils.getDBTime().slice(0, 10)}]`, + ); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat const groupCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 60cec5f0ff35..df3a5c1c5f3b 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -131,6 +131,8 @@ describe('actions/IOU', () => { // The transactionID on the iou action should match the one from the transactions collection expect(iouAction.originalMessage.IOUTransactionID).toBe(transactionID); + expect(transaction.merchant).toBe(CONST.REPORT.TYPE.IOU); + resolve(); }, }); @@ -264,6 +266,8 @@ describe('actions/IOU', () => { // The comment should be correct expect(transaction.comment.comment).toBe(comment); + expect(transaction.merchant).toBe(CONST.REPORT.TYPE.IOU); + // It should be pending expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -424,16 +428,10 @@ describe('actions/IOU', () => { newTransaction = _.find(allTransactions, transaction => transaction.transactionID !== existingTransaction.transactionID); - // The transaction should be attached to the IOU report expect(newTransaction.reportID).toBe(iouReportID); - - // The amount on the new transaction should be correct expect(newTransaction.amount).toBe(amountInCents); - - // The comment should be correct expect(newTransaction.comment.comment).toBe(comment); - - // The new transaction should be pending + expect(newTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); expect(newTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); // The transactionID on the iou action should match the one from the transactions collection @@ -559,16 +557,10 @@ describe('actions/IOU', () => { const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); transactionID = transaction.transactionID; - // The transaction should be attached to the IOU report expect(transaction.reportID).toBe(iouReportID); - - // Its amount should match the amount of the request expect(transaction.amount).toBe(amountInCents); - - // The comment should be correct expect(transaction.comment.comment).toBe(comment); - - // It should be pending + expect(transaction.merchant).toBe(CONST.REPORT.TYPE.IOU); expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); // The transactionID on the iou action should match the one from the transactions collection @@ -894,6 +886,11 @@ describe('actions/IOU', () => { expect(vitTransaction.comment.comment).toBe(comment); expect(groupTransaction.comment.comment).toBe(comment); + expect(carlosTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); + expect(julesTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); + expect(vitTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); + expect(groupTransaction.merchant).toBe(`Split Bill with ${RORY_EMAIL}, ${CARLOS_EMAIL}, ${JULES_EMAIL}, and ${VIT_EMAIL} [${DateUtils.getDBTime().slice(0, 10)}]`); + expect(carlosTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); expect(julesTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); expect(vitTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); From 6bd14d70bb58c1fcb8ba67aefb5947e91c6f223e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 May 2023 14:06:18 -0700 Subject: [PATCH 34/85] iouTransactionID -> transactionID --- src/libs/ReportUtils.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 4bb0ef650767..7468439779d4 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1083,14 +1083,14 @@ function getIOUReportActionMessage(type, total, participants, comment, currency, * @param {String} currency * @param {String} comment - User comment for the IOU. * @param {Array} participants - An array with participants details. - * @param {String} iouTransactionID - Only required if the IOUReportAction type is oneOf(cancel, decline). Generates a randomID as default. + * @param {String} transactionID - Only required if the IOUReportAction type is oneOf(cancel, decline). Generates a randomID as default. * * @param {String} [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, payPal, Expensify). * @param {String} [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. * @param {Boolean} [isSettlingUp] - Whether we are settling up an IOU. * @returns {Object} */ -function buildOptimisticIOUReportAction(type, amount, currency, comment, participants, iouTransactionID, paymentType = '', iouReportID = '', isSettlingUp = false) { +function buildOptimisticIOUReportAction(type, amount, currency, comment, participants, transactionID, paymentType = '', iouReportID = '', isSettlingUp = false) { const IOUReportID = iouReportID || generateReportID(); const parser = new ExpensiMark(); const commentText = getParsedComment(comment); @@ -1100,7 +1100,7 @@ function buildOptimisticIOUReportAction(type, amount, currency, comment, partici amount, comment: textForNewComment, currency, - IOUTransactionID: iouTransactionID, + IOUTransactionID: transactionID, IOUReportID, type, }; From c13d0181b1cfa82a97d81fc663f6d860e47db1c0 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 May 2023 14:21:35 -0700 Subject: [PATCH 35/85] Update comment --- src/libs/TransactionUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index d4d625ccea4d..5796f9ef5d85 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -14,7 +14,7 @@ import * as NumberUtils from './NumberUtils'; * @returns {Object} */ function buildOptimisticTransaction(amount, currency, reportID, comment = '', source = '', originalTransactionID = '', merchant = CONST.REPORT.TYPE.IOU) { - // transactionIDs are random, positive, 64-bit numbers. + // transactionIDs are random, positive, 64-bit numberic strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) const transactionID = NumberUtils.rand64(); From d2669e6c8b5f7f591af501262f40d820d41bb807 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 May 2023 14:43:43 -0700 Subject: [PATCH 36/85] Fix lint --- src/libs/TransactionUtils.js | 1 + tests/actions/IOUTest.js | 51 +++++++++++++++++++---------------- tests/unit/StringUtilsTest.js | 2 +- tests/utils/TestHelper.js | 6 ++--- 4 files changed, 33 insertions(+), 27 deletions(-) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 5796f9ef5d85..5b266a99bdf0 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -11,6 +11,7 @@ import * as NumberUtils from './NumberUtils'; * @param {String} [comment] * @param {String} [source] * @param {String} [originalTransactionID] + * @param {String} [merchant] * @returns {Object} */ function buildOptimisticTransaction(amount, currency, reportID, comment = '', source = '', originalTransactionID = '', merchant = CONST.REPORT.TYPE.IOU) { diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index df3a5c1c5f3b..eec46e7ecb9d 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -113,7 +113,7 @@ describe('actions/IOU', () => { // There should be one transaction expect(_.size(allTransactions)).toBe(1); - const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); + const transaction = _.find(allTransactions, t => !_.isEmpty(t)); transactionID = transaction.transactionID; // The transaction should be attached to the IOU report @@ -142,7 +142,7 @@ describe('actions/IOU', () => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, waitForCollectionCallback: true, - callback: reportActionsForChatReport => { + callback: (reportActionsForChatReport) => { Onyx.disconnect(connectionID); expect(_.size(reportActionsForChatReport)).toBe(2); _.each(reportActionsForChatReport, reportAction => expect(reportAction.pendingAction).toBeFalsy()); @@ -154,7 +154,7 @@ describe('actions/IOU', () => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, waitForCollectionCallback: true, - callback: transaction => { + callback: (transaction) => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); resolve(); @@ -254,7 +254,7 @@ describe('actions/IOU', () => { // There should be one transaction expect(_.size(allTransactions)).toBe(1); - const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); + const transaction = _.find(allTransactions, t => !_.isEmpty(t)); transactionID = transaction.transactionID; // The transaction should be attached to the IOU report @@ -275,15 +275,15 @@ describe('actions/IOU', () => { expect(iouAction.originalMessage.IOUTransactionID).toBe(transactionID); resolve(); - } - }) + }, + }); })) .then(fetch.resume) .then(() => new Promise((resolve) => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, waitForCollectionCallback: true, - callback: reportActionsForChatReport => { + callback: (reportActionsForChatReport) => { Onyx.disconnect(connectionID); expect(_.size(reportActionsForChatReport)).toBe(2); _.each(reportActionsForChatReport, reportAction => expect(reportAction.pendingAction).toBeFalsy()); @@ -295,7 +295,7 @@ describe('actions/IOU', () => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, waitForCollectionCallback: true, - callback: transaction => { + callback: (transaction) => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); resolve(); @@ -395,7 +395,10 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(_.size(reportActionsForChatReport)).toBe(3); - newIOUAction = _.find(reportActionsForChatReport, reportAction => reportAction.reportActionID !== createdAction.reportActionID && reportAction.reportActionID !== iouAction.reportActionID); + newIOUAction = _.find(reportActionsForChatReport, reportAction => ( + reportAction.reportActionID !== createdAction.reportActionID + && reportAction.reportActionID !== iouAction.reportActionID + )); // The IOUReportID should be correct expect(iouAction.originalMessage.IOUReportID).toBe(iouReportID); @@ -446,7 +449,7 @@ describe('actions/IOU', () => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, waitForCollectionCallback: true, - callback: reportActionsForChatReport => { + callback: (reportActionsForChatReport) => { Onyx.disconnect(connectionID); expect(_.size(reportActionsForChatReport)).toBe(3); _.each(reportActionsForChatReport, reportAction => expect(reportAction.pendingAction).toBeFalsy()); @@ -554,7 +557,7 @@ describe('actions/IOU', () => { // There should be one transaction expect(_.size(allTransactions)).toBe(1); - const transaction = _.find(allTransactions, transaction => !_.isEmpty(transaction)); + const transaction = _.find(allTransactions, t => !_.isEmpty(t)); transactionID = transaction.transactionID; expect(transaction.reportID).toBe(iouReportID); @@ -627,12 +630,12 @@ describe('actions/IOU', () => { hasOutstandingIOU: false, participants: [CARLOS_EMAIL], }; - let carlosCreatedAction = { + const carlosCreatedAction = { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, created: DateUtils.getDBTime(), }; - let julesIOUReportID = NumberUtils.rand64(); + const julesIOUReportID = NumberUtils.rand64(); let julesChatReport = { reportID: NumberUtils.rand64(), type: CONST.REPORT.TYPE.CHAT, @@ -640,13 +643,13 @@ describe('actions/IOU', () => { iouReportID: julesIOUReportID, participants: [JULES_EMAIL], }; - let julesCreatedAction = { + const julesCreatedAction = { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, created: DateUtils.getDBTime(), }; jest.advanceTimersByTime(200); - let julesExistingTransaction = { + const julesExistingTransaction = { transactionID: NumberUtils.rand64(), amount: 1000, comment: 'This is an existing transaction', @@ -661,7 +664,7 @@ describe('actions/IOU', () => { currency: CONST.CURRENCY.USD, total: julesExistingTransaction.amount, }; - let julesExistingIOUAction = { + const julesExistingIOUAction = { reportActionID: NumberUtils.rand64(), actionName: CONST.REPORT.ACTIONS.TYPE.IOU, actorEmail: RORY_EMAIL, @@ -779,7 +782,7 @@ describe('actions/IOU', () => { expect(vitChatReport.hasOutstandingIOU).toBe(true); expect(vitChatReport.iouReportID).toBe(vitIOUReport.reportID); - expect(vitIOUReport.chatReportID).toBe(vitChatReport.reportID) + expect(vitIOUReport.chatReportID).toBe(vitChatReport.reportID); resolve(); }, @@ -835,7 +838,7 @@ describe('actions/IOU', () => { expect(vitIOUAction.originalMessage.amount).toBe(amountInCents / 4); expect(vitIOUAction.originalMessage.comment).toBe(comment); expect(vitIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); - expect(Date.parse(vitCreatedAction.created)).toBeLessThanOrEqual(Date.parse(vitIOUAction.created)) + expect(Date.parse(vitCreatedAction.created)).toBeLessThanOrEqual(Date.parse(vitIOUAction.created)); // Group chat should have two reportActions – a pending CREATED action and a pending IOU action w/ type SPLIT expect(_.size(groupReportActions)).toBe(2); @@ -889,7 +892,8 @@ describe('actions/IOU', () => { expect(carlosTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); expect(julesTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); expect(vitTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); - expect(groupTransaction.merchant).toBe(`Split Bill with ${RORY_EMAIL}, ${CARLOS_EMAIL}, ${JULES_EMAIL}, and ${VIT_EMAIL} [${DateUtils.getDBTime().slice(0, 10)}]`); + expect(groupTransaction.merchant) + .toBe(`Split Bill with ${RORY_EMAIL}, ${CARLOS_EMAIL}, ${JULES_EMAIL}, and ${VIT_EMAIL} [${DateUtils.getDBTime().slice(0, 10)}]`); expect(carlosTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); expect(julesTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); @@ -906,7 +910,7 @@ describe('actions/IOU', () => { resolve(); }, - }) + }); })) .then(fetch.resume) .then(() => new Promise((resolve) => { @@ -915,10 +919,11 @@ describe('actions/IOU', () => { waitForCollectionCallback: true, callback: (allReports) => { Onyx.disconnect(connectionID); - _.each(allReports, report => { - if (report.pendingFields) { - _.each(report.pendingFields, pendingField => expect(pendingField).toBeFalsy()); + _.each(allReports, (report) => { + if (!report.pendingFields) { + return; } + _.each(report.pendingFields, pendingField => expect(pendingField).toBeFalsy()); }); resolve(); }, diff --git a/tests/unit/StringUtilsTest.js b/tests/unit/StringUtilsTest.js index 8df2048bf5b1..6d09f4bd3e86 100644 --- a/tests/unit/StringUtilsTest.js +++ b/tests/unit/StringUtilsTest.js @@ -7,7 +7,7 @@ describe('StringUtils', () => { [['rory'], 'rory'], [['rory', 'vit'], 'rory and vit'], [['rory', 'vit', 'jules'], 'rory, vit, and jules'], - ])(`arrayToSpokenList(%s)`, (input, expectedOutput) => { + ])('arrayToSpokenList(%s)', (input, expectedOutput) => { expect(StringUtils.arrayToSpokenList(input)).toBe(expectedOutput); }); }); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 17411ea72b6f..972bf2c8af2b 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -151,11 +151,11 @@ function signOutTestUser() { * @returns {Function} */ function getGlobalFetchMock() { - let queue = []; + const queue = []; let isPaused = false; let shouldFail = false; - const getResponse = () => shouldFail + const getResponse = () => (shouldFail ? { ok: true, json: () => Promise.resolve({ @@ -166,7 +166,7 @@ function getGlobalFetchMock() { json: () => Promise.resolve({ jsonCode: 200 }), - }; + }); const mockFetch = jest.fn() .mockImplementation(() => { From c81c126d391b0635a55126a4a5a2cae9eb3b5d67 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 May 2023 15:15:23 -0700 Subject: [PATCH 37/85] Fix missed lint items --- tests/actions/IOUTest.js | 4 ++-- tests/utils/TestHelper.js | 8 ++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index eec46e7ecb9d..99c0dfeadbd8 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -581,7 +581,7 @@ describe('actions/IOU', () => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, waitForCollectionCallback: true, - callback: reportActionsForChatReport => { + callback: (reportActionsForChatReport) => { Onyx.disconnect(connectionID); expect(_.size(reportActionsForChatReport)).toBe(2); iouAction = _.find(reportActionsForChatReport, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); @@ -596,7 +596,7 @@ describe('actions/IOU', () => { const connectionID = Onyx.connect({ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, waitForCollectionCallback: true, - callback: transaction => { + callback: (transaction) => { Onyx.disconnect(connectionID); expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); diff --git a/tests/utils/TestHelper.js b/tests/utils/TestHelper.js index 972bf2c8af2b..4286354c5851 100644 --- a/tests/utils/TestHelper.js +++ b/tests/utils/TestHelper.js @@ -158,14 +158,10 @@ function getGlobalFetchMock() { const getResponse = () => (shouldFail ? { ok: true, - json: () => Promise.resolve({ - jsonCode: 400, - }), + json: () => Promise.resolve({jsonCode: 400}), } : { ok: true, - json: () => Promise.resolve({ - jsonCode: 200 - }), + json: () => Promise.resolve({jsonCode: 200}), }); const mockFetch = jest.fn() From b9defaa692f857fa93e9c80d8dc7c5218969cb77 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 May 2023 15:22:19 -0700 Subject: [PATCH 38/85] Remove unnecessary NumberUtils function --- src/libs/NumberUtils.js | 12 +----------- src/libs/actions/IOU.js | 3 +-- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/src/libs/NumberUtils.js b/src/libs/NumberUtils.js index 3d54ce30ff67..ef2ac88aaff1 100644 --- a/src/libs/NumberUtils.js +++ b/src/libs/NumberUtils.js @@ -35,17 +35,7 @@ function rand64() { return left + middleString + rightString; } -/** - * Rounds a number down to the nearest hundred's place. - * - * @param {Number} num - * @returns {Number} - */ -function roundDownToTwoDecimalPlaces(num) { - return Math.floor(num * 100) / 100; -} - export { + // eslint-disable-next-line import/prefer-default-export rand64, - roundDownToTwoDecimalPlaces, }; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 8d69718580f0..dcabf504275a 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -14,7 +14,6 @@ import * as IOUUtils from '../IOUUtils'; import * as OptionsListUtils from '../OptionsListUtils'; import DateUtils from '../DateUtils'; import TransactionUtils from '../TransactionUtils'; -import * as NumberUtils from '../NumberUtils'; import StringUtils from '../StringUtils'; const chatReports = {}; @@ -413,7 +412,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment } const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction( - NumberUtils.roundDownToTwoDecimalPlaces(amountInCents / (participants.length + 1)), + splitAmount, currency, oneOnOneIOUReport.reportID, comment, From d1dc16d2cf73e574a4ed0faaa67bf009263ac1c9 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:04:57 -0700 Subject: [PATCH 39/85] Rename ID_DELETED to SPLIT_REPORTID --- src/CONST.js | 2 +- src/libs/actions/IOU.js | 2 +- tests/actions/IOUTest.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index d22addedeb2f..72b52a36062b 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -391,7 +391,7 @@ const CONST = { DROP_NATIVE_ID: 'report-dropzone', ACTIVE_DROP_NATIVE_ID: 'report-dropzone', MAXIMUM_PARTICIPANTS: 8, - ID_DELETED: '-2', + SPLIT_REPORTID: '-2', ACTIONS: { LIMIT: 50, TYPE: { diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index dcabf504275a..a067621c925e 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -272,7 +272,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment const groupTransaction = TransactionUtils.buildOptimisticTransaction( amountInCents, currency, - CONST.REPORT.ID_DELETED, + CONST.REPORT.SPLIT_REPORTID, comment, '', '', diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 99c0dfeadbd8..225113b63f7f 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -872,7 +872,7 @@ describe('actions/IOU', () => { carlosTransaction = _.find(allTransactions, transaction => transaction.transactionID === carlosIOUAction.originalMessage.IOUTransactionID); julesTransaction = _.find(allTransactions, transaction => transaction.transactionID === julesIOUAction.originalMessage.IOUTransactionID); vitTransaction = _.find(allTransactions, transaction => transaction.transactionID === vitIOUAction.originalMessage.IOUTransactionID); - groupTransaction = _.find(allTransactions, transaction => transaction.reportID === CONST.REPORT.ID_DELETED); + groupTransaction = _.find(allTransactions, transaction => transaction.reportID === CONST.REPORT.SPLIT_REPORTID); expect(carlosTransaction.reportID).toBe(carlosIOUReport.reportID); expect(julesTransaction.reportID).toBe(julesIOUReport.reportID); From 89a57c27a49b562ae1512d4acb5926c5c205918c Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:05:50 -0700 Subject: [PATCH 40/85] Fix JSDoc comment in buildOptimisticIOUReportAction --- src/libs/ReportUtils.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 7468439779d4..84b854ed8059 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1083,8 +1083,7 @@ function getIOUReportActionMessage(type, total, participants, comment, currency, * @param {String} currency * @param {String} comment - User comment for the IOU. * @param {Array} participants - An array with participants details. - * @param {String} transactionID - Only required if the IOUReportAction type is oneOf(cancel, decline). Generates a randomID as default. - * + * @param {String} transactionID * @param {String} [paymentType] - Only required if the IOUReportAction type is 'pay'. Can be oneOf(elsewhere, payPal, Expensify). * @param {String} [iouReportID] - Only required if the IOUReportActions type is oneOf(decline, cancel, pay). Generates a randomID as default. * @param {Boolean} [isSettlingUp] - Whether we are settling up an IOU. From 4f346e2a0492130d5d000a3487440da628337260 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:10:26 -0700 Subject: [PATCH 41/85] Use StringUtils instead of one-off code --- src/libs/ReportUtils.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 84b854ed8059..dec265b47f71 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -21,6 +21,7 @@ import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; import * as LocalePhoneNumber from './LocalePhoneNumber'; +import StringUtils from './StringUtils'; let sessionEmail; Onyx.connect({ @@ -1026,9 +1027,7 @@ function buildOptimisticIOUReport(ownerEmail, userEmail, total, chatReportID, cu function getIOUReportActionMessage(type, total, participants, comment, currency, paymentType = '', isSettlingUp = false) { const amount = NumberFormatUtils.format(preferredLocale, total / 100, {style: 'currency', currency}); const displayNames = _.map(participants, participant => getDisplayNameForParticipant(participant.login, true)); - const who = displayNames.length < 3 - ? displayNames.join(' and ') - : `${displayNames.slice(0, -1).join(', ')}, and ${_.last(displayNames)}`; + const who = StringUtils.arrayToSpokenList(displayNames); let paymentMethodMessage; switch (paymentType) { case CONST.IOU.PAYMENT_TYPE.EXPENSIFY: From 55d3994cf6aa31a9a2c8c4786cc0ed1658045f65 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:11:21 -0700 Subject: [PATCH 42/85] Fix typo --- src/libs/TransactionUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 5b266a99bdf0..a0a2d4099e3a 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -15,7 +15,7 @@ import * as NumberUtils from './NumberUtils'; * @returns {Object} */ function buildOptimisticTransaction(amount, currency, reportID, comment = '', source = '', originalTransactionID = '', merchant = CONST.REPORT.TYPE.IOU) { - // transactionIDs are random, positive, 64-bit numberic strings. + // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) const transactionID = NumberUtils.rand64(); From 114482dfa9e8fcea9d302bbb253be539669ae17c Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:18:01 -0700 Subject: [PATCH 43/85] Clarify jest comment --- jest/setup.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jest/setup.js b/jest/setup.js index f1cb9b9ee76b..407059251579 100644 --- a/jest/setup.js +++ b/jest/setup.js @@ -9,8 +9,9 @@ reanimatedJestUtils.setUpTests(); // https://reactnavigation.org/docs/testing/#mocking-native-modules jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper'); -// We have to mock the SQLiteStorage provider because it uses the native module SQLiteStorage, which is not available in jest. -// Mocking this file in __mocks__ does not work because jest doesn't support mocking files that are not directly used in the testing project +// Mock react-native-onyx storage layer because the SQLite storage layer doesn't work in jest. +// Mocking this file in __mocks__ does not work because jest doesn't support mocking files that are not directly used in the testing project, +// and we only want to mock the storage layer, not the whole Onyx module. jest.mock('react-native-onyx/lib/storage', () => require('react-native-onyx/lib/storage/__mocks__')); // Turn off the console logs for timing events. They are not relevant for unit tests and create a lot of noise From 2bedfa676b666c03c870d78c21c16f1dc4d39bf8 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:21:06 -0700 Subject: [PATCH 44/85] pendingAction not pendingFields --- src/libs/actions/IOU.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index a067621c925e..50fe83c5c06f 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -365,7 +365,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${groupTransaction.transactionID}`, value: { - pendingFields: null, + pendingAction: null, errors: { [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), }, @@ -515,7 +515,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${oneOnOneTransaction.transactionID}`, value: { - pendingFields: null, + pendingAction: null, errors: { [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), }, From d8db06854b83dba992992f34971d921ce287ab8f Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:25:13 -0700 Subject: [PATCH 45/85] Fix comments in tests --- tests/actions/IOUTest.js | 1 - tests/unit/IOUUtilsTest.js | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 225113b63f7f..b9435b6a7c11 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -463,7 +463,6 @@ describe('actions/IOU', () => { waitForCollectionCallback: true, callback: (allTransactions) => { Onyx.disconnect(connectionID); - _.each(allTransactions, transaction => expect(transaction.pendingAction).toBeFalsy()); resolve(); }, diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 835cd747be98..12676a3efb03 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -24,7 +24,7 @@ function createIOUReportAction(type, amount, currency, isOffline = false, IOUTra iouReport.reportID, ); - // Default is to create requests offline, if this is specified then we need to remove the pendingAction + // Default is to create requests online, if `isOffline` is specified then we need to remove the pendingAction moneyRequestAction.pendingAction = isOffline ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null; reportActions.push(moneyRequestAction); @@ -62,8 +62,8 @@ describe('IOUUtils', () => { CONST.LOCALES.EN, ); - // The starting point of all tests is the IOUReport containing a single non-pending transaction in USD - // All requests in the tests are assumed to be offline, unless isOnline is specified + // The starting point of all tests is the IOUReport containing a single pending transaction in USD + // All requests in the tests are assumed to be online, unless isOffline is specified createIOUReportAction('create', amount, currency); }); From adb91e1366ed4d96abdbb4eae70860245be1ad50 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:28:07 -0700 Subject: [PATCH 46/85] Fix amount / comment discrepancy --- tests/actions/IOUTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index b9435b6a7c11..3bd3489e0aa5 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -620,7 +620,7 @@ describe('actions/IOU', () => { * - Rory and Vit have never chatted together before * - There is no existing group chat with the four of them */ - const amount = 400; + const amount = 4; const amountInCents = amount * 100; const comment = 'Yes, I am splitting a bill for $4 USD'; let carlosChatReport = { From 0b5f90f4d2329003b2a67be828d66004b4ca3373 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:29:36 -0700 Subject: [PATCH 47/85] Move cleanup to another promise block --- tests/actions/IOUTest.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 3bd3489e0aa5..bec4269eb9ac 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -600,14 +600,13 @@ describe('actions/IOU', () => { expect(transaction.pendingAction).toBeFalsy(); expect(transaction.errors).toBeTruthy(); expect(_.values(transaction.errors)[0]).toBe(Localize.translateLocal('iou.error.genericCreateFailureMessage')); - - // Cleanup - fetch.succeed(); - resolve(); }, }); - })); + })) + + // Cleanup + .then(fetch.succeed); }); }); From b8181f118df65d138a965b0d9133424ad9496ce4 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 12:52:57 -0700 Subject: [PATCH 48/85] Fix lint --- src/libs/ReportUtils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 2946844c48e5..e5d24d19b558 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -21,7 +21,6 @@ import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; import * as LocalePhoneNumber from './LocalePhoneNumber'; -import StringUtils from './StringUtils'; let sessionEmail; Onyx.connect({ From a06911f6dcc50b82126e75457b6453d1031804ce Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 14:34:46 -0700 Subject: [PATCH 49/85] Simplify initialization of IOUUtilsTest --- tests/unit/IOUUtilsTest.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 12676a3efb03..c6822a7b2118 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -25,7 +25,9 @@ function createIOUReportAction(type, amount, currency, isOffline = false, IOUTra ); // Default is to create requests online, if `isOffline` is specified then we need to remove the pendingAction - moneyRequestAction.pendingAction = isOffline ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null; + if (!isOffline) { + moneyRequestAction.pendingAction = null; + } reportActions.push(moneyRequestAction); return moneyRequestAction; @@ -62,7 +64,7 @@ describe('IOUUtils', () => { CONST.LOCALES.EN, ); - // The starting point of all tests is the IOUReport containing a single pending transaction in USD + // The starting point of all tests is the IOUReport containing a single non-pending transaction in USD // All requests in the tests are assumed to be online, unless isOffline is specified createIOUReportAction('create', amount, currency); }); From 3cff94b9d1888bd639c8ee9cf2c4a79c10fd5d6e Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 16:37:02 -0700 Subject: [PATCH 50/85] Correct comment --- tests/unit/IOUUtilsTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index c6822a7b2118..96125fc86863 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -24,7 +24,7 @@ function createIOUReportAction(type, amount, currency, isOffline = false, IOUTra iouReport.reportID, ); - // Default is to create requests online, if `isOffline` is specified then we need to remove the pendingAction + // Default is to create requests online, if `isOffline` is not specified then we need to remove the pendingAction if (!isOffline) { moneyRequestAction.pendingAction = null; } From 5de881a0f53d344c4efae70f1fb2965fca2bcbcb Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 17:05:02 -0700 Subject: [PATCH 51/85] Internationalize string formatter --- src/libs/StringUtils.js | 33 ++++++++++++------------- tests/unit/StringUtilsTest.js | 45 +++++++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/libs/StringUtils.js b/src/libs/StringUtils.js index 8cadcbbbaa24..55b94378700c 100644 --- a/src/libs/StringUtils.js +++ b/src/libs/StringUtils.js @@ -1,4 +1,17 @@ -import _ from 'underscore'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../ONYXKEYS'; +import CONST from '../CONST'; + +let formatter = new Intl.ListFormat(CONST.LOCALES.DEFAULT, {style: 'long', type: 'conjunction'}); +Onyx.connect({ + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + callback: (locale) => { + if (!locale) { + return; + } + formatter = new Intl.ListFormat(locale, {style: 'long', type: 'conjunction'}); + }, +}); /** * Converts an array of strings to a spoken list, like: @@ -9,23 +22,7 @@ import _ from 'underscore'; * @returns {String} */ function arrayToSpokenList(arr) { - if (_.isEmpty(arr)) { - return ''; - } - - if (arr.length === 1) { - return arr[0]; - } - - if (arr.length === 2) { - return `${arr[0]} and ${arr[1]}`; - } - - let result = arr[0]; - for (let i = 1; i < arr.length - 1; i++) { - result += `, ${arr[i]}`; - } - return `${result}, and ${arr[arr.length - 1]}`; + return formatter.format(arr); } export default { diff --git a/tests/unit/StringUtilsTest.js b/tests/unit/StringUtilsTest.js index 6d09f4bd3e86..877bea8f7bef 100644 --- a/tests/unit/StringUtilsTest.js +++ b/tests/unit/StringUtilsTest.js @@ -1,14 +1,49 @@ +import Onyx from 'react-native-onyx'; +import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import CONST from '../../src/CONST'; +import ONYXKEYS from '../../src/ONYXKEYS'; import StringUtils from '../../src/libs/StringUtils'; describe('StringUtils', () => { + beforeAll(() => { + Onyx.init({ + keys: {NVP_PREFERRED_LOCALE: ONYXKEYS.NVP_PREFERRED_LOCALE}, + initialKeyStates: {[ONYXKEYS.NVP_PREFERRED_LOCALE]: CONST.LOCALES.DEFAULT}, + }); + return waitForPromisesToResolve(); + }); + + afterEach(() => Onyx.clear()); + describe('arrayToSpokenList', () => { test.each([ - [[], ''], - [['rory'], 'rory'], - [['rory', 'vit'], 'rory and vit'], - [['rory', 'vit', 'jules'], 'rory, vit, and jules'], - ])('arrayToSpokenList(%s)', (input, expectedOutput) => { + [[], { + [CONST.LOCALES.DEFAULT]: '', + [CONST.LOCALES.ES]: '', + }], + [['rory'], { + [CONST.LOCALES.DEFAULT]: 'rory', + [CONST.LOCALES.ES]: 'rory', + }], + [['rory', 'vit'], { + [CONST.LOCALES.DEFAULT]: 'rory and vit', + [CONST.LOCALES.ES]: 'rory y vit', + }], + [['rory', 'vit', 'jules'], { + [CONST.LOCALES.DEFAULT]: 'rory, vit, and jules', + [CONST.LOCALES.ES]: 'rory, vit y jules', + }], + [['rory', 'vit', 'ionatan'], { + [CONST.LOCALES.DEFAULT]: 'rory, vit, and ionatan', + [CONST.LOCALES.ES]: 'rory, vit e ionatan', + }], + ])('arrayToSpokenList(%s)', (input, { + [CONST.LOCALES.DEFAULT]: expectedOutput, + [CONST.LOCALES.ES]: expectedOutputES, + }) => { expect(StringUtils.arrayToSpokenList(input)).toBe(expectedOutput); + return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES) + .then(() => expect(StringUtils.arrayToSpokenList(input)).toBe(expectedOutputES)); }); }); }); From 382e802323862b945b111410b6b4553a99b08169 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 18:10:06 -0700 Subject: [PATCH 52/85] Localize split bill merchant --- src/languages/en.js | 1 + src/languages/es.js | 1 + src/libs/Localize/index.js | 25 +++++++++------- src/libs/StringUtils.js | 30 ------------------- src/libs/actions/IOU.js | 4 +-- tests/actions/IOUTest.js | 2 +- .../{StringUtilsTest.js => LocalizeTests.js} | 10 +++---- 7 files changed, 25 insertions(+), 48 deletions(-) delete mode 100644 src/libs/StringUtils.js rename tests/unit/{StringUtilsTest.js => LocalizeTests.js} (83%) diff --git a/src/languages/en.js b/src/languages/en.js index fbb220273cc7..80e3451b01b8 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -133,6 +133,7 @@ export default { websiteExample: 'e.g. https://www.expensify.com', zipCodeExampleFormat: ({zipSampleFormat}) => (zipSampleFormat ? `e.g. ${zipSampleFormat}` : ''), description: 'Description', + with: 'with', }, attachmentPicker: { cameraPermissionRequired: 'Camera access', diff --git a/src/languages/es.js b/src/languages/es.js index 9713c780381b..e4382b7e9e2f 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -132,6 +132,7 @@ export default { websiteExample: 'p. ej. https://www.expensify.com', zipCodeExampleFormat: ({zipSampleFormat}) => (zipSampleFormat ? `p. ej. ${zipSampleFormat}` : ''), description: 'Descripción', + with: 'con', }, attachmentPicker: { cameraPermissionRequired: 'Permiso para acceder a la cámara', diff --git a/src/libs/Localize/index.js b/src/libs/Localize/index.js index 24eb2e060064..90beb3066d28 100644 --- a/src/libs/Localize/index.js +++ b/src/libs/Localize/index.js @@ -12,6 +12,19 @@ import BaseLocaleListener from './LocaleListener/BaseLocaleListener'; // Listener when an update in Onyx happens so we use the updated locale when translating/localizing items. LocaleListener.connect(); +const CONJUNCTION_LIST_FORMATS_FOR_LOCALES = _.reduce(CONST.LOCALES, (memo, locale) => { + // This is not a supported locale, so we'll use ES_ES instead + if (locale === CONST.LOCALES.ES_ES_ONFIDO) { + // eslint-disable-next-line no-param-reassign + memo[locale] = new Intl.ListFormat(CONST.LOCALES.ES_ES, {style: 'long', type: 'conjunction'}); + return memo; + } + + // eslint-disable-next-line no-param-reassign + memo[locale] = new Intl.ListFormat(locale, {style: 'long', type: 'conjunction'}); + return memo; +}, {}); + /** * Return translated string for given locale and phrase * @@ -76,16 +89,8 @@ function translateLocal(phrase, variables) { * @return {String} */ function arrayToString(anArray) { - const and = translateLocal('common.and'); - let aString = ''; - if (_.size(anArray) === 1) { - aString = anArray[0]; - } else if (_.size(anArray) === 2) { - aString = anArray.join(` ${and} `); - } else if (_.size(anArray) > 2) { - aString = `${anArray.slice(0, -1).join(', ')} ${and} ${anArray.slice(-1)}`; - } - return aString; + const listFormat = CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()]; + return listFormat.format(anArray); } /** diff --git a/src/libs/StringUtils.js b/src/libs/StringUtils.js deleted file mode 100644 index 55b94378700c..000000000000 --- a/src/libs/StringUtils.js +++ /dev/null @@ -1,30 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '../ONYXKEYS'; -import CONST from '../CONST'; - -let formatter = new Intl.ListFormat(CONST.LOCALES.DEFAULT, {style: 'long', type: 'conjunction'}); -Onyx.connect({ - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: (locale) => { - if (!locale) { - return; - } - formatter = new Intl.ListFormat(locale, {style: 'long', type: 'conjunction'}); - }, -}); - -/** - * Converts an array of strings to a spoken list, like: - * - * ['rory', 'vit', 'jules'] => 'rory, vit, and jules' - * - * @param {Array} arr - * @returns {String} - */ -function arrayToSpokenList(arr) { - return formatter.format(arr); -} - -export default { - arrayToSpokenList, -}; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 50fe83c5c06f..244561632f4e 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -14,7 +14,6 @@ import * as IOUUtils from '../IOUUtils'; import * as OptionsListUtils from '../OptionsListUtils'; import DateUtils from '../DateUtils'; import TransactionUtils from '../TransactionUtils'; -import StringUtils from '../StringUtils'; const chatReports = {}; const iouReports = {}; @@ -269,6 +268,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment const amountInCents = Math.round(amount * 100); // ReportID is -2 (aka "deleted") on the group transaction: https://github.com/Expensify/Auth/blob/3fa2698654cd4fbc30f9de38acfca3fbeb7842e4/auth/command/SplitTransaction.cpp#L24-L27 + const formattedParticipants = Localize.arrayToString([currentUserLogin, ..._.map(participants, participant => participant.login)]); const groupTransaction = TransactionUtils.buildOptimisticTransaction( amountInCents, currency, @@ -276,7 +276,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment comment, '', '', - `Split Bill with ${StringUtils.arrayToSpokenList([currentUserLogin, ..._.map(participants, participant => participant.login)])} [${DateUtils.getDBTime().slice(0, 10)}]`, + `${Localize.translateLocal('iou.splitBill')} ${Localize.translateLocal('common.with')} ${formattedParticipants} [${DateUtils.getDBTime().slice(0, 10)}]`, ); // Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index bec4269eb9ac..b15fc839f239 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -891,7 +891,7 @@ describe('actions/IOU', () => { expect(julesTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); expect(vitTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); expect(groupTransaction.merchant) - .toBe(`Split Bill with ${RORY_EMAIL}, ${CARLOS_EMAIL}, ${JULES_EMAIL}, and ${VIT_EMAIL} [${DateUtils.getDBTime().slice(0, 10)}]`); + .toBe(`Split bill with ${RORY_EMAIL}, ${CARLOS_EMAIL}, ${JULES_EMAIL}, and ${VIT_EMAIL} [${DateUtils.getDBTime().slice(0, 10)}]`); expect(carlosTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); expect(julesTransaction.comment.source).toBe(CONST.IOU.MONEY_REQUEST_TYPE.SPLIT); diff --git a/tests/unit/StringUtilsTest.js b/tests/unit/LocalizeTests.js similarity index 83% rename from tests/unit/StringUtilsTest.js rename to tests/unit/LocalizeTests.js index 877bea8f7bef..cbe158ffb913 100644 --- a/tests/unit/StringUtilsTest.js +++ b/tests/unit/LocalizeTests.js @@ -2,9 +2,9 @@ import Onyx from 'react-native-onyx'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; -import StringUtils from '../../src/libs/StringUtils'; +import * as Localize from '../../src/libs/Localize'; -describe('StringUtils', () => { +describe('localize', () => { beforeAll(() => { Onyx.init({ keys: {NVP_PREFERRED_LOCALE: ONYXKEYS.NVP_PREFERRED_LOCALE}, @@ -15,7 +15,7 @@ describe('StringUtils', () => { afterEach(() => Onyx.clear()); - describe('arrayToSpokenList', () => { + describe('arrayToString', () => { test.each([ [[], { [CONST.LOCALES.DEFAULT]: '', @@ -41,9 +41,9 @@ describe('StringUtils', () => { [CONST.LOCALES.DEFAULT]: expectedOutput, [CONST.LOCALES.ES]: expectedOutputES, }) => { - expect(StringUtils.arrayToSpokenList(input)).toBe(expectedOutput); + expect(Localize.arrayToString(input)).toBe(expectedOutput); return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES) - .then(() => expect(StringUtils.arrayToSpokenList(input)).toBe(expectedOutputES)); + .then(() => expect(Localize.arrayToString(input)).toBe(expectedOutputES)); }); }); }); From 50105d9820de5036ef39ea7003dfbef182d26b29 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 2 May 2023 18:12:53 -0700 Subject: [PATCH 53/85] Use SET for optimistic transactions --- src/libs/actions/IOU.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 244561632f4e..7451a6bde6f5 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -78,7 +78,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, iouReport.reportID, comment); const optimisticTransactionData = { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, value: optimisticTransaction, }; @@ -781,7 +781,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, optimisticIOUReport.reportID, comment); const optimisticTransactionData = { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, value: optimisticTransaction, }; @@ -966,7 +966,7 @@ function getPayMoneyRequestParams(chatReport, iouReport, recipient, paymentMetho }, }, { - onyxMethod: Onyx.METHOD.MERGE, + onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, value: optimisticTransaction, }, From d2d9ce36856cbcc772686812f752beb4b6123e49 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 10:31:10 -0700 Subject: [PATCH 54/85] Change CurrencySymbolUtils to just CurrencyUtils --- src/components/TextInputWithCurrencySymbol.js | 9 +-- ...urrencySymbolUtils.js => CurrencyUtils.js} | 13 ++-- src/pages/iou/IOUCurrencySelection.js | 4 +- src/pages/iou/steps/MoneyRequestAmountPage.js | 1 - tests/unit/CurrencySymbolUtilsTest.js | 39 ------------ tests/unit/CurrencyUtilsTest.js | 60 +++++++++++++++++++ 6 files changed, 72 insertions(+), 54 deletions(-) rename src/libs/{CurrencySymbolUtils.js => CurrencyUtils.js} (65%) delete mode 100644 tests/unit/CurrencySymbolUtilsTest.js create mode 100644 tests/unit/CurrencyUtilsTest.js diff --git a/src/components/TextInputWithCurrencySymbol.js b/src/components/TextInputWithCurrencySymbol.js index 1c50b62e5e21..122fa9f0dcbc 100644 --- a/src/components/TextInputWithCurrencySymbol.js +++ b/src/components/TextInputWithCurrencySymbol.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import AmountTextInput from './AmountTextInput'; import CurrencySymbolButton from './CurrencySymbolButton'; -import * as CurrencySymbolUtils from '../libs/CurrencySymbolUtils'; +import * as CurrencyUtils from '../libs/CurrencyUtils'; const propTypes = { /** A ref to forward to amount text input */ @@ -23,9 +23,6 @@ const propTypes = { /** Placeholder value for amount text input */ placeholder: PropTypes.string.isRequired, - /** Preferred locale of the user */ - preferredLocale: PropTypes.string.isRequired, - /** Currency code of user's selected currency */ selectedCurrencyCode: PropTypes.string.isRequired, @@ -48,8 +45,8 @@ const defaultProps = { }; function TextInputWithCurrencySymbol(props) { - const currencySymbol = CurrencySymbolUtils.getLocalizedCurrencySymbol(props.preferredLocale, props.selectedCurrencyCode); - const isCurrencySymbolLTR = CurrencySymbolUtils.isCurrencySymbolLTR(props.preferredLocale, props.selectedCurrencyCode); + const currencySymbol = CurrencyUtils.getLocalizedCurrencySymbol(props.selectedCurrencyCode); + const isCurrencySymbolLTR = CurrencyUtils.isCurrencySymbolLTR(props.selectedCurrencyCode); const currencySymbolButton = ( { const isSelectedCurrency = currencyCode === this.props.iou.selectedCurrencyCode; return { - text: `${currencyCode} - ${CurrencySymbolUtils.getLocalizedCurrencySymbol(this.props.preferredLocale, currencyCode)}`, + text: `${currencyCode} - ${CurrencyUtils.getLocalizedCurrencySymbol(currencyCode)}`, currencyCode, keyForList: currencyCode, customIcon: isSelectedCurrency ? greenCheckmark : undefined, diff --git a/src/pages/iou/steps/MoneyRequestAmountPage.js b/src/pages/iou/steps/MoneyRequestAmountPage.js index 7fb169fa3d84..e80c2962201d 100755 --- a/src/pages/iou/steps/MoneyRequestAmountPage.js +++ b/src/pages/iou/steps/MoneyRequestAmountPage.js @@ -321,7 +321,6 @@ class MoneyRequestAmountPage extends React.Component { onChangeAmount={this.updateAmount} onCurrencyButtonPress={this.navigateToCurrencySelectionPage} placeholder={this.props.numberFormat(0)} - preferredLocale={this.props.preferredLocale} ref={el => this.textInput = el} selectedCurrencyCode={this.props.iou.selectedCurrencyCode} selection={this.state.selection} diff --git a/tests/unit/CurrencySymbolUtilsTest.js b/tests/unit/CurrencySymbolUtilsTest.js deleted file mode 100644 index ef18e2026de3..000000000000 --- a/tests/unit/CurrencySymbolUtilsTest.js +++ /dev/null @@ -1,39 +0,0 @@ -import _ from 'underscore'; -import * as CurrencySymbolUtils from '../../src/libs/CurrencySymbolUtils'; -import CONST from '../../src/CONST'; - -// This file can get outdated. In that case, you can follow these steps to update it: -// - open your browser console and navigate to the Network tab -// - refresh the App -// - click on the OpenApp request and in the preview tab locate the key `currencyList` -// - copy the value and format it to valid json using some external tool -// - update currencyList.json -import currencyList from './currencyList.json'; - -const currencyCodeList = _.keys(currencyList); -const AVAILABLE_LOCALES = [CONST.LOCALES.EN, CONST.LOCALES.ES]; - -// Contains item [isLeft, locale, currencyCode] -const symbolPositions = [ - [true, CONST.LOCALES.EN, 'USD'], - [false, CONST.LOCALES.ES, 'USD'], -]; - -describe('CurrencySymbolUtils', () => { - describe('getLocalizedCurrencySymbol', () => { - test.each(AVAILABLE_LOCALES)('Returns non empty string for all currencyCode with preferredLocale %s', (prefrredLocale) => { - _.forEach(currencyCodeList, (currencyCode) => { - const localizedSymbol = CurrencySymbolUtils.getLocalizedCurrencySymbol(prefrredLocale, currencyCode); - - expect(localizedSymbol).toBeTruthy(); - }); - }); - }); - - describe('isCurrencySymbolLTR', () => { - test.each(symbolPositions)('Returns %s for preferredLocale %s and currencyCode %s', (isLeft, locale, currencyCode) => { - const isSymbolLeft = CurrencySymbolUtils.isCurrencySymbolLTR(locale, currencyCode); - expect(isSymbolLeft).toBe(isLeft); - }); - }); -}); diff --git a/tests/unit/CurrencyUtilsTest.js b/tests/unit/CurrencyUtilsTest.js new file mode 100644 index 000000000000..96a39c3ce198 --- /dev/null +++ b/tests/unit/CurrencyUtilsTest.js @@ -0,0 +1,60 @@ +import _ from 'underscore'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../src/ONYXKEYS'; +import CONST from '../../src/CONST'; +import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; +import * as CurrencyUtils from '../../src/libs/CurrencyUtils'; +import LocaleListener from '../../src/libs/Localize/LocaleListener'; + +// This file can get outdated. In that case, you can follow these steps to update it: +// - open your browser console and navigate to the Network tab +// - refresh the App +// - click on the OpenApp request and in the preview tab locate the key `currencyList` +// - copy the value and format it to valid json using some external tool +// - update currencyList.json +import currencyList from './currencyList.json'; + +const currencyCodeList = _.keys(currencyList); +const AVAILABLE_LOCALES = [CONST.LOCALES.EN, CONST.LOCALES.ES]; + +// Contains item [isLeft, locale, currencyCode] +const symbolPositions = [ + [true, CONST.LOCALES.EN, 'USD'], + [false, CONST.LOCALES.ES, 'USD'], +]; + +describe('CurrencyUtils', () => { + beforeAll(() => { + Onyx.init({ + keys: {NVP_PREFERRED_LOCALE: ONYXKEYS.NVP_PREFERRED_LOCALE}, + initialKeyStates: {[ONYXKEYS.NVP_PREFERRED_LOCALE]: CONST.LOCALES.DEFAULT}, + }); + LocaleListener.connect(); + return waitForPromisesToResolve(); + }); + + afterEach(() => Onyx.clear()); + + describe('getLocalizedCurrencySymbol', () => { + test.each(AVAILABLE_LOCALES)('Returns non empty string for all currencyCode with preferredLocale %s', prefrredLocale => ( + Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, prefrredLocale) + .then(() => { + _.forEach(currencyCodeList, (currencyCode) => { + const localizedSymbol = CurrencyUtils.getLocalizedCurrencySymbol(currencyCode); + + expect(localizedSymbol).toBeTruthy(); + }); + }) + )); + }); + + describe('isCurrencySymbolLTR', () => { + test.each(symbolPositions)('Returns %s for preferredLocale %s and currencyCode %s', (isLeft, locale, currencyCode) => ( + Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, locale) + .then(() => { + const isSymbolLeft = CurrencyUtils.isCurrencySymbolLTR(currencyCode); + expect(isSymbolLeft).toBe(isLeft); + }) + )); + }); +}); From 6ca0f609b97cc2be2211019ba871b3a7728da890 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 10:40:32 -0700 Subject: [PATCH 55/85] Move some IOUUtils to CurrencyUtils --- src/libs/CurrencyUtils.js | 42 +++++++++++++++++++++++++++++++ src/libs/IOUUtils.js | 44 ++------------------------------- tests/unit/CurrencyUtilsTest.js | 36 +++++++++++++++++++++++++-- tests/unit/IOUUtilsTest.js | 28 --------------------- 4 files changed, 78 insertions(+), 72 deletions(-) diff --git a/src/libs/CurrencyUtils.js b/src/libs/CurrencyUtils.js index aaf174a2a93f..c7504c69fe75 100644 --- a/src/libs/CurrencyUtils.js +++ b/src/libs/CurrencyUtils.js @@ -1,7 +1,47 @@ import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../ONYXKEYS'; +import CONST from '../CONST'; import BaseLocaleListener from './Localize/LocaleListener/BaseLocaleListener'; import * as NumberFormatUtils from './NumberFormatUtils'; +let currencyList = {}; +Onyx.connect({ + key: ONYXKEYS.CURRENCY_LIST, + callback: (val) => { + if (_.isEmpty(val)) { + return; + } + + currencyList = val; + }, +}); + +/** + * Returns the number of digits after the decimal separator for a specific currency. + * For currencies that have decimal places > 2, floor to 2 instead: + * https://github.com/Expensify/App/issues/15878#issuecomment-1496291464 + * + * @param {String} currency - IOU currency + * @returns {Number} + */ +function getCurrencyDecimals(currency = CONST.CURRENCY.USD) { + const decimals = lodashGet(currencyList, [currency, 'decimals']); + return _.isUndefined(decimals) ? 2 : Math.min(decimals, 2); +} + +/** + * Returns the currency's minor unit quantity + * e.g. Cent in USD + * + * @param {String} currency - IOU currency + * @returns {Number} + */ +function getCurrencyUnit(currency = CONST.CURRENCY.USD) { + return 10 ** getCurrencyDecimals(currency); +} + /** * Get localized currency symbol for currency(ISO 4217) Code * @@ -33,6 +73,8 @@ function isCurrencySymbolLTR(currencyCode) { } export { + getCurrencyDecimals, + getCurrencyUnit, getLocalizedCurrencySymbol, isCurrencySymbolLTR, }; diff --git a/src/libs/IOUUtils.js b/src/libs/IOUUtils.js index 1151f9c77225..d60a00e7393f 100644 --- a/src/libs/IOUUtils.js +++ b/src/libs/IOUUtils.js @@ -1,44 +1,6 @@ import _ from 'underscore'; -import Onyx from 'react-native-onyx'; -import lodashGet from 'lodash/get'; import CONST from '../CONST'; -import ONYXKEYS from '../ONYXKEYS'; - -let currencyList = {}; -Onyx.connect({ - key: ONYXKEYS.CURRENCY_LIST, - callback: (val) => { - if (_.isEmpty(val)) { - return; - } - - currencyList = val; - }, -}); - -/** - * Returns the number of digits after the decimal separator for a specific currency. - * For currencies that have decimal places > 2, floor to 2 instead: - * https://github.com/Expensify/App/issues/15878#issuecomment-1496291464 - * - * @param {String} currency - IOU currency - * @returns {Number} - */ -function getCurrencyDecimals(currency = CONST.CURRENCY.USD) { - const decimals = lodashGet(currencyList, [currency, 'decimals']); - return _.isUndefined(decimals) ? 2 : Math.min(decimals, 2); -} - -/** - * Returns the currency's minor unit quantity - * e.g. Cent in USD - * - * @param {String} currency - IOU currency - * @returns {Number} - */ -function getCurrencyUnit(currency = CONST.CURRENCY.USD) { - return 10 ** getCurrencyDecimals(currency); -} +import * as CurrencyUtils from './CurrencyUtils'; /** * Calculates the amount per user given a list of participants @@ -55,7 +17,7 @@ function calculateAmount(participants, total, currency, isDefaultUser = false) { // numbers cannot be represented with perfect accuracy. // Currencies that do not have minor units (i.e. no decimal place) are also supported. // https://github.com/Expensify/App/issues/15878 - const currencyUnit = getCurrencyUnit(currency); + const currencyUnit = CurrencyUtils.getCurrencyUnit(currency); const iouAmount = Math.round(parseFloat(total * currencyUnit)); const totalParticipants = participants.length + 1; @@ -183,6 +145,4 @@ export { updateIOUOwnerAndTotal, getIOUReportActions, isIOUReportPendingCurrencyConversion, - getCurrencyUnit, - getCurrencyDecimals, }; diff --git a/tests/unit/CurrencyUtilsTest.js b/tests/unit/CurrencyUtilsTest.js index 96a39c3ce198..1e791c30f82d 100644 --- a/tests/unit/CurrencyUtilsTest.js +++ b/tests/unit/CurrencyUtilsTest.js @@ -26,8 +26,14 @@ const symbolPositions = [ describe('CurrencyUtils', () => { beforeAll(() => { Onyx.init({ - keys: {NVP_PREFERRED_LOCALE: ONYXKEYS.NVP_PREFERRED_LOCALE}, - initialKeyStates: {[ONYXKEYS.NVP_PREFERRED_LOCALE]: CONST.LOCALES.DEFAULT}, + keys: { + NVP_PREFERRED_LOCALE: ONYXKEYS.NVP_PREFERRED_LOCALE, + CURRENCY_LIST: ONYXKEYS.CURRENCY_LIST, + }, + initialKeyStates: { + [ONYXKEYS.NVP_PREFERRED_LOCALE]: CONST.LOCALES.DEFAULT, + [ONYXKEYS.CURRENCY_LIST]: currencyList, + }, }); LocaleListener.connect(); return waitForPromisesToResolve(); @@ -57,4 +63,30 @@ describe('CurrencyUtils', () => { }) )); }); + + describe('getCurrencyDecimals', () => { + test('Currency decimals smaller than or equal 2', () => { + expect(CurrencyUtils.getCurrencyDecimals('JPY')).toBe(0); + expect(CurrencyUtils.getCurrencyDecimals('USD')).toBe(2); + }); + + test('Currency decimals larger than 2 should return 2', () => { + // Actual: 3 + expect(CurrencyUtils.getCurrencyDecimals('LYD')).toBe(2); + + // Actual: 4 + expect(CurrencyUtils.getCurrencyDecimals('UYW')).toBe(2); + }); + }); + + describe('getCurrencyUnit', () => { + test('Currency with decimals smaller than or equal 2', () => { + expect(CurrencyUtils.getCurrencyUnit('JPY')).toBe(1); + expect(CurrencyUtils.getCurrencyUnit('USD')).toBe(100); + }); + + test('Currency with decimals larger than 2 should be floor to 2', () => { + expect(CurrencyUtils.getCurrencyUnit('LYD')).toBe(100); + }); + }); }); diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 96125fc86863..5f8762463223 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -142,34 +142,6 @@ describe('IOUUtils', () => { }); }); - describe('getCurrencyDecimals', () => { - beforeAll(() => initCurrencyList()); - test('Currency decimals smaller than or equal 2', () => { - expect(IOUUtils.getCurrencyDecimals('JPY')).toBe(0); - expect(IOUUtils.getCurrencyDecimals('USD')).toBe(2); - }); - - test('Currency decimals larger than 2 should return 2', () => { - // Actual: 3 - expect(IOUUtils.getCurrencyDecimals('LYD')).toBe(2); - - // Actual: 4 - expect(IOUUtils.getCurrencyDecimals('UYW')).toBe(2); - }); - }); - - describe('getCurrencyUnit', () => { - beforeAll(() => initCurrencyList()); - test('Currency with decimals smaller than or equal 2', () => { - expect(IOUUtils.getCurrencyUnit('JPY')).toBe(1); - expect(IOUUtils.getCurrencyUnit('USD')).toBe(100); - }); - - test('Currency with decimals larger than 2 should be floor to 2', () => { - expect(IOUUtils.getCurrencyUnit('LYD')).toBe(100); - }); - }); - describe('calculateAmount', () => { beforeAll(() => initCurrencyList()); test('103 JPY split among 3 participants including the default user should be [35, 34, 34]', () => { From 9256a73c440ace8871714c2bc05e12294a3bc445 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 11:02:30 -0700 Subject: [PATCH 56/85] Create function to convert amount to smallest unit --- src/libs/CurrencyUtils.js | 18 ++++++++++++++++++ tests/unit/CurrencyUtilsTest.js | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/libs/CurrencyUtils.js b/src/libs/CurrencyUtils.js index c7504c69fe75..2ee60751ed22 100644 --- a/src/libs/CurrencyUtils.js +++ b/src/libs/CurrencyUtils.js @@ -72,9 +72,27 @@ function isCurrencySymbolLTR(currencyCode) { return parts[0].type === 'currency'; } +/** + * Takes an amount as a floating point number and converts it to an integer amount. + * For example, given [25, USD], will return 2500. + * Given [25.50, USD] will return 2550. + * Given [2500, JPY], will return 2500. + * + * @note we do not currently support any currencies with more than two decimal places. Sorry Tunisia :( + * + * @param {String} currency + * @param {Number} amountAsFloat + * @returns {Number} + */ +function convertToSmallestUnit(currency, amountAsFloat) { + const currencyUnit = getCurrencyUnit(currency); + return Math.trunc(amountAsFloat * currencyUnit); +} + export { getCurrencyDecimals, getCurrencyUnit, getLocalizedCurrencySymbol, isCurrencySymbolLTR, + convertToSmallestUnit, }; diff --git a/tests/unit/CurrencyUtilsTest.js b/tests/unit/CurrencyUtilsTest.js index 1e791c30f82d..107f692d8e9a 100644 --- a/tests/unit/CurrencyUtilsTest.js +++ b/tests/unit/CurrencyUtilsTest.js @@ -89,4 +89,17 @@ describe('CurrencyUtils', () => { expect(CurrencyUtils.getCurrencyUnit('LYD')).toBe(100); }); }); + + describe('convertToSmallestUnit', () => { + test.each([ + [[CONST.CURRENCY.USD, 25], 2500], + [[CONST.CURRENCY.USD, 25.5], 2550], + [[CONST.CURRENCY.USD, 25.50], 2550], + [['JPY', 25], 25], + [['JPY', 2500], 2500], + [['JPY', 25.5], 25], + ])('Correctly converts %s to amount in smallest units', ([currency, amount], expectedResult) => { + expect(CurrencyUtils.convertToSmallestUnit(currency, amount)).toBe(expectedResult); + }); + }); }); From 422e56dbf06078efe2e019da08fb02b06bc64cce Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 11:31:43 -0700 Subject: [PATCH 57/85] Create convertToDisplayString function --- src/libs/CurrencyUtils.js | 17 ++++++++++++ tests/unit/CurrencyUtilsTest.js | 49 +++++++++++++++++++++++---------- 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/src/libs/CurrencyUtils.js b/src/libs/CurrencyUtils.js index 2ee60751ed22..56cba966cfc9 100644 --- a/src/libs/CurrencyUtils.js +++ b/src/libs/CurrencyUtils.js @@ -89,10 +89,27 @@ function convertToSmallestUnit(currency, amountAsFloat) { return Math.trunc(amountAsFloat * currencyUnit); } +/** + * Given an amount in the smallest units of a currency, convert it to a string for display in the UI. + * + * @param {String} currency + * @param {Number} amountInSmallestUnit – should be an integer. Anything after a decimal place will be dropped. + * @returns {String} + */ +function convertToDisplayString(currency, amountInSmallestUnit) { + const currencyUnit = getCurrencyUnit(currency); + const convertedAmount = Math.trunc(amountInSmallestUnit) / currencyUnit; + return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, { + style: 'currency', + currency, + }); +} + export { getCurrencyDecimals, getCurrencyUnit, getLocalizedCurrencySymbol, isCurrencySymbolLTR, convertToSmallestUnit, + convertToDisplayString, }; diff --git a/tests/unit/CurrencyUtilsTest.js b/tests/unit/CurrencyUtilsTest.js index 107f692d8e9a..2f2fc15cc8c3 100644 --- a/tests/unit/CurrencyUtilsTest.js +++ b/tests/unit/CurrencyUtilsTest.js @@ -17,12 +17,6 @@ import currencyList from './currencyList.json'; const currencyCodeList = _.keys(currencyList); const AVAILABLE_LOCALES = [CONST.LOCALES.EN, CONST.LOCALES.ES]; -// Contains item [isLeft, locale, currencyCode] -const symbolPositions = [ - [true, CONST.LOCALES.EN, 'USD'], - [false, CONST.LOCALES.ES, 'USD'], -]; - describe('CurrencyUtils', () => { beforeAll(() => { Onyx.init({ @@ -55,7 +49,10 @@ describe('CurrencyUtils', () => { }); describe('isCurrencySymbolLTR', () => { - test.each(symbolPositions)('Returns %s for preferredLocale %s and currencyCode %s', (isLeft, locale, currencyCode) => ( + test.each([ + [true, CONST.LOCALES.EN, 'USD'], + [false, CONST.LOCALES.ES, 'USD'], + ])('Returns %s for preferredLocale %s and currencyCode %s', (isLeft, locale, currencyCode) => ( Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, locale) .then(() => { const isSymbolLeft = CurrencyUtils.isCurrencySymbolLTR(currencyCode); @@ -92,14 +89,38 @@ describe('CurrencyUtils', () => { describe('convertToSmallestUnit', () => { test.each([ - [[CONST.CURRENCY.USD, 25], 2500], - [[CONST.CURRENCY.USD, 25.5], 2550], - [[CONST.CURRENCY.USD, 25.50], 2550], - [['JPY', 25], 25], - [['JPY', 2500], 2500], - [['JPY', 25.5], 25], - ])('Correctly converts %s to amount in smallest units', ([currency, amount], expectedResult) => { + [CONST.CURRENCY.USD, 25, 2500], + [CONST.CURRENCY.USD, 25.5, 2550], + [CONST.CURRENCY.USD, 25.50, 2550], + ['JPY', 25, 25], + ['JPY', 2500, 2500], + ['JPY', 25.5, 25], + ])('Correctly converts %s to amount in smallest units', (currency, amount, expectedResult) => { expect(CurrencyUtils.convertToSmallestUnit(currency, amount)).toBe(expectedResult); }); }); + + describe('convertToDisplayString', () => { + test.each([ + [CONST.CURRENCY.USD, 25, '$0.25'], + [CONST.CURRENCY.USD, 2500, '$25.00'], + [CONST.CURRENCY.USD, 150, '$1.50'], + [CONST.CURRENCY.USD, 250000, '$2,500.00'], + ['JPY', 25, '¥25'], + ['JPY', 2500, '¥2,500'], + ['JPY', 25.5, '¥25'], + ])('Correctly displays %s', (currency, amount, expectedResult) => { + expect(CurrencyUtils.convertToDisplayString(currency, amount)).toBe(expectedResult); + }); + + test.each([ + ['EUR', 25, '0,25\xa0€'], + ['EUR', 2500, '25,00\xa0€'], + ['EUR', 250000, '2500,00\xa0€'], + ['EUR', 250000000, '2.500.000,00\xa0€'], + ])('Correctly displays %s in ES locale', (currency, amount, expectedResult) => ( + Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES) + .then(() => expect(CurrencyUtils.convertToDisplayString(currency, amount)).toBe(expectedResult)) + )); + }); }); From 4e13073505205805c24cf52efb9449870fe8c400 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 11:44:13 -0700 Subject: [PATCH 58/85] Always use CurrencyUtils for the amount in the requestMoney flow --- src/libs/ReportUtils.js | 13 ++++--------- src/libs/actions/IOU.js | 15 ++++----------- src/pages/iou/MoneyRequestModal.js | 5 ++++- tests/unit/IOUUtilsTest.js | 9 +-------- 4 files changed, 13 insertions(+), 29 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index e5d24d19b558..c709ade2fca4 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -21,6 +21,7 @@ import * as defaultAvatars from '../components/Icon/DefaultAvatars'; import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; import * as LocalePhoneNumber from './LocalePhoneNumber'; +import * as CurrencyUtils from './CurrencyUtils'; let sessionEmail; Onyx.connect({ @@ -1007,21 +1008,15 @@ function buildOptimisticAddCommentReportAction(text, file) { * * @param {String} ownerEmail - Email of the person generating the IOU. * @param {String} userEmail - Email of the other person participating in the IOU. - * @param {Number} total - IOU amount in cents. + * @param {Number} total - IOU amount in the smallest unit of the currency. * @param {String} chatReportID - Report ID of the chat where the IOU is. * @param {String} currency - IOU currency. - * @param {String} locale - Locale where the IOU is created * @param {Boolean} isSendingMoney - If we send money the IOU should be created as settled * * @returns {Object} */ -function buildOptimisticIOUReport(ownerEmail, userEmail, total, chatReportID, currency, locale, isSendingMoney = false) { - const formattedTotal = NumberFormatUtils.format(locale, - total, { - style: 'currency', - currency, - }); - +function buildOptimisticIOUReport(ownerEmail, userEmail, total, chatReportID, currency, isSendingMoney = false) { + const formattedTotal = CurrencyUtils.convertToDisplayString(currency, total); return { // If we're sending money, hasOutstandingIOU should be false hasOutstandingIOU: !isSendingMoney, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7451a6bde6f5..1d0f2bd49679 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -47,7 +47,7 @@ Onyx.connect({ * Request money from another user * * @param {Object} report - * @param {Number} amount + * @param {Number} amount - always in the smallest unit of the currency * @param {String} currency * @param {String} recipientEmail * @param {Object} participant @@ -73,7 +73,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com currency, ); } else { - iouReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, debtorEmail, amount, chatReport.reportID, currency, preferredLocale); + iouReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, debtorEmail, amount, chatReport.reportID, currency); } const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, iouReport.reportID, comment); @@ -399,14 +399,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment ); oneOnOneChatReport.hasOutstandingIOU = oneOnOneIOUReport.total !== 0; } else { - oneOnOneIOUReport = ReportUtils.buildOptimisticIOUReport( - currentUserEmail, - email, - splitAmount, - oneOnOneChatReport.reportID, - currency, - locale, - ); + oneOnOneIOUReport = ReportUtils.buildOptimisticIOUReport(currentUserEmail, email, splitAmount, oneOnOneChatReport.reportID, currency); oneOnOneChatReport.hasOutstandingIOU = true; oneOnOneChatReport.iouReportID = oneOnOneIOUReport.reportID; } @@ -777,7 +770,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType chatReport = ReportUtils.buildOptimisticChatReport([recipientEmail]); isNewChat = true; } - const optimisticIOUReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, managerEmail, amount, chatReport.reportID, currency, preferredLocale, true); + const optimisticIOUReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, managerEmail, amount, chatReport.reportID, currency, true); const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, optimisticIOUReport.reportID, comment); const optimisticTransactionData = { diff --git a/src/pages/iou/MoneyRequestModal.js b/src/pages/iou/MoneyRequestModal.js index fb727646a61b..4e69989e6aa7 100644 --- a/src/pages/iou/MoneyRequestModal.js +++ b/src/pages/iou/MoneyRequestModal.js @@ -29,6 +29,7 @@ import reportPropTypes from '../reportPropTypes'; import * as ReportUtils from '../../libs/ReportUtils'; import * as ReportScrollManager from '../../libs/ReportScrollManager'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; +import * as CurrencyUtils from '../../libs/CurrencyUtils'; /** * A modal used for requesting money, splitting bills or sending money. @@ -265,6 +266,7 @@ const MoneyRequestModal = (props) => { * @param {String} paymentMethodType */ const sendMoney = useCallback((paymentMethodType) => { + // TODO: convert correctly const amountInDollars = Math.round(amount * 100); const currency = props.iou.selectedCurrencyCode; const trimmedComment = props.iou.comment.trim(); @@ -330,6 +332,7 @@ const MoneyRequestModal = (props) => { // If the request is created from the global create menu, we also navigate the user to the group report if (props.hasMultipleParticipants) { + // TODO: convert amount IOU.splitBillAndOpenReport( selectedParticipants, props.currentUserPersonalDetails.login, @@ -347,7 +350,7 @@ const MoneyRequestModal = (props) => { } IOU.requestMoney( props.report, - Math.round(amount * 100), + CurrencyUtils.convertToSmallestUnit(props.iou.selectedCurrencyCode, Number.parseFloat(amount)), props.iou.selectedCurrencyCode, props.currentUserPersonalDetails.login, selectedParticipants[0], diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 5f8762463223..223344efd5d4 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -55,14 +55,7 @@ describe('IOUUtils', () => { const amount = 1000; const currency = 'USD'; - iouReport = ReportUtils.buildOptimisticIOUReport( - ownerEmail, - managerEmail, - amount, - chatReportID, - currency, - CONST.LOCALES.EN, - ); + iouReport = ReportUtils.buildOptimisticIOUReport(ownerEmail, managerEmail, amount, chatReportID, currency); // The starting point of all tests is the IOUReport containing a single non-pending transaction in USD // All requests in the tests are assumed to be online, unless isOffline is specified From 9f14ad338388f7f2551889452be27f4cc16aece1 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 11:49:22 -0700 Subject: [PATCH 59/85] don't convert transaction amount --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 1d0f2bd49679..e7a2b6f2f511 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -76,7 +76,7 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com iouReport = ReportUtils.buildOptimisticIOUReport(recipientEmail, debtorEmail, amount, chatReport.reportID, currency); } - const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, iouReport.reportID, comment); + const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency, iouReport.reportID, comment); const optimisticTransactionData = { onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, From 53791cfe6c732e55f2236e1077b70de294a06c8d Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 12:14:30 -0700 Subject: [PATCH 60/85] Always use smallest currency units for SplitBill flows --- .../MoneyRequestConfirmationList.js | 4 +- src/libs/IOUUtils.js | 27 +++-------- src/libs/actions/IOU.js | 45 ++++++------------- src/pages/iou/MoneyRequestModal.js | 16 ++----- tests/actions/IOUTest.js | 9 +--- tests/unit/IOUUtilsTest.js | 17 ++++--- 6 files changed, 36 insertions(+), 82 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 55d7c3755147..ee7e24a0068e 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -136,7 +136,7 @@ class MoneyRequestConfirmationList extends Component { * @returns {Array} */ getParticipantsWithAmount(participants) { - const iouAmount = IOUUtils.calculateAmount(participants, this.props.iouAmount, this.props.iou.selectedCurrencyCode); + const iouAmount = IOUUtils.calculateAmount(participants, this.props.iouAmount); return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participants, @@ -172,7 +172,7 @@ class MoneyRequestConfirmationList extends Component { const formattedUnselectedParticipants = this.getParticipantsWithoutAmount(unselectedParticipants); const formattedParticipants = _.union(formattedSelectedParticipants, formattedUnselectedParticipants); - const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants, this.props.iouAmount, this.props.iou.selectedCurrencyCode, true); + const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants, this.props.iouAmount, true); const formattedMyPersonalDetails = OptionsListUtils.getIOUConfirmationOptionsFromMyPersonalDetail( this.props.currentUserPersonalDetails, this.props.numberFormat(myIOUAmount / 100, { diff --git a/src/libs/IOUUtils.js b/src/libs/IOUUtils.js index d60a00e7393f..141d3a1544a1 100644 --- a/src/libs/IOUUtils.js +++ b/src/libs/IOUUtils.js @@ -1,37 +1,24 @@ import _ from 'underscore'; import CONST from '../CONST'; -import * as CurrencyUtils from './CurrencyUtils'; /** * Calculates the amount per user given a list of participants + * * @param {Array} participants - List of logins for the participants in the chat. It should not include the current user's login. - * @param {Number} total - IOU total amount - * @param {String} currency - IOU currency + * @param {Number} total - IOU total amount in the smallest units of the currency * @param {Boolean} isDefaultUser - Whether we are calculating the amount for the current user * @returns {Number} */ -function calculateAmount(participants, total, currency, isDefaultUser = false) { - // Convert to cents before working with iouAmount to avoid - // javascript subtraction with decimal problem -- when dealing with decimals, - // because they are encoded as IEEE 754 floating point numbers, some of the decimal - // numbers cannot be represented with perfect accuracy. - // Currencies that do not have minor units (i.e. no decimal place) are also supported. - // https://github.com/Expensify/App/issues/15878 - const currencyUnit = CurrencyUtils.getCurrencyUnit(currency); - const iouAmount = Math.round(parseFloat(total * currencyUnit)); - +function calculateAmount(participants, total, isDefaultUser = false) { const totalParticipants = participants.length + 1; - const amountPerPerson = Math.round(iouAmount / totalParticipants); - + const amountPerPerson = Math.round(total / totalParticipants); let finalAmount = amountPerPerson; - if (isDefaultUser) { const sumAmount = amountPerPerson * totalParticipants; - const difference = iouAmount - sumAmount; - finalAmount = iouAmount !== sumAmount ? (amountPerPerson + difference) : amountPerPerson; + const difference = total - sumAmount; + finalAmount = total !== sumAmount ? (amountPerPerson + difference) : amountPerPerson; } - - return (finalAmount * 100) / currencyUnit; + return finalAmount; } /** diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index e7a2b6f2f511..caea5952ca06 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -31,18 +31,6 @@ Onyx.connect({ }, }); -let preferredLocale = CONST.LOCALES.DEFAULT; -Onyx.connect({ - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: (val) => { - if (!val) { - return; - } - - preferredLocale = val; - }, -}); - /** * Request money from another user * @@ -249,15 +237,14 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com * ] * @param {Array} participants * @param {String} currentUserLogin - * @param {Number} amount + * @param {Number} amount - always in the smallest unit of the currency * @param {String} comment * @param {String} currency - * @param {String} locale * @param {String} existingGroupChatReportID * * @return {Object} */ -function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment, currency, locale, existingGroupChatReportID = '') { +function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment, currency, existingGroupChatReportID = '') { const currentUserEmail = OptionsListUtils.addSMSDomainIfPhoneNumber(currentUserLogin); const participantLogins = _.map(participants, participant => OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login).toLowerCase()); const existingGroupChatReport = existingGroupChatReportID @@ -265,12 +252,10 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment : ReportUtils.getChatByParticipants(participantLogins); const groupChatReport = existingGroupChatReport || ReportUtils.buildOptimisticChatReport(participantLogins); - const amountInCents = Math.round(amount * 100); - // ReportID is -2 (aka "deleted") on the group transaction: https://github.com/Expensify/Auth/blob/3fa2698654cd4fbc30f9de38acfca3fbeb7842e4/auth/command/SplitTransaction.cpp#L24-L27 const formattedParticipants = Localize.arrayToString([currentUserLogin, ..._.map(participants, participant => participant.login)]); const groupTransaction = TransactionUtils.buildOptimisticTransaction( - amountInCents, + amount, currency, CONST.REPORT.SPLIT_REPORTID, comment, @@ -283,7 +268,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment const groupCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmail); const groupIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.SPLIT, - amountInCents, + amount, currency, comment, participants, @@ -374,8 +359,8 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment ]; // Loop through participants creating individual chats, iouReports and reportActionIDs as needed - const splitAmount = IOUUtils.calculateAmount(participants, amount, currency, false); - const splits = [{email: currentUserEmail, amount: IOUUtils.calculateAmount(participants, amount, currency, true)}]; + const splitAmount = IOUUtils.calculateAmount(participants, amount, false); + const splits = [{email: currentUserEmail, amount: IOUUtils.calculateAmount(participants, amount, true)}]; const hasMultipleParticipants = participants.length > 1; _.each(participants, (participant) => { @@ -568,19 +553,18 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment /** * @param {Array} participants * @param {String} currentUserLogin - * @param {Number} amount + * @param {Number} amount - always in smallest currency unit * @param {String} comment * @param {String} currency - * @param {String} locale * @param {String} existingGroupChatReportID */ -function splitBill(participants, currentUserLogin, amount, comment, currency, locale, existingGroupChatReportID = '') { - const {groupData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, amount, comment, currency, locale, existingGroupChatReportID); +function splitBill(participants, currentUserLogin, amount, comment, currency, existingGroupChatReportID = '') { + const {groupData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, amount, comment, currency, existingGroupChatReportID); const parsedComment = ReportUtils.getParsedComment(comment); API.write('SplitBill', { reportID: groupData.chatReportID, - amount: Math.round(amount * 100), + amount, splits: JSON.stringify(splits), currency, comment: parsedComment, @@ -595,18 +579,17 @@ function splitBill(participants, currentUserLogin, amount, comment, currency, lo /** * @param {Array} participants * @param {String} currentUserLogin - * @param {Number} amount + * @param {Number} amount - always in smallest currency unit * @param {String} comment * @param {String} currency - * @param {String} locale */ -function splitBillAndOpenReport(participants, currentUserLogin, amount, comment, currency, locale) { - const {groupData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, amount, comment, currency, locale); +function splitBillAndOpenReport(participants, currentUserLogin, amount, comment, currency) { + const {groupData, splits, onyxData} = createSplitsAndOnyxData(participants, currentUserLogin, amount, comment, currency); const parsedComment = ReportUtils.getParsedComment(comment); API.write('SplitBillAndOpenReport', { reportID: groupData.chatReportID, - amount: Math.round(amount * 100), + amount, splits: JSON.stringify(splits), currency, comment: parsedComment, diff --git a/src/pages/iou/MoneyRequestModal.js b/src/pages/iou/MoneyRequestModal.js index 4e69989e6aa7..6e95ff117bde 100644 --- a/src/pages/iou/MoneyRequestModal.js +++ b/src/pages/iou/MoneyRequestModal.js @@ -314,29 +314,21 @@ const MoneyRequestModal = (props) => { const createTransaction = useCallback((selectedParticipants) => { const reportID = lodashGet(props.route, 'params.reportID', ''); const trimmedComment = props.iou.comment.trim(); + const amountInSmallestCurrencyUnit = CurrencyUtils.convertToSmallestUnit(props.iou.selectedCurrencyCode, Number.parseFloat(amount)); // IOUs created from a group report will have a reportID param in the route. // Since the user is already viewing the report, we don't need to navigate them to the report if (props.hasMultipleParticipants && CONST.REGEX.NUMBER.test(reportID)) { - IOU.splitBill( - selectedParticipants, - props.currentUserPersonalDetails.login, - amount, - trimmedComment, - props.iou.selectedCurrencyCode, - props.preferredLocale, - reportID, - ); + IOU.splitBill(selectedParticipants, props.currentUserPersonalDetails.login, amountInSmallestCurrencyUnit, trimmedComment, props.iou.selectedCurrencyCode, reportID); return; } // If the request is created from the global create menu, we also navigate the user to the group report if (props.hasMultipleParticipants) { - // TODO: convert amount IOU.splitBillAndOpenReport( selectedParticipants, props.currentUserPersonalDetails.login, - amount, + amountInSmallestCurrencyUnit, trimmedComment, props.iou.selectedCurrencyCode, props.preferredLocale, @@ -350,7 +342,7 @@ const MoneyRequestModal = (props) => { } IOU.requestMoney( props.report, - CurrencyUtils.convertToSmallestUnit(props.iou.selectedCurrencyCode, Number.parseFloat(amount)), + amountInSmallestCurrencyUnit, props.iou.selectedCurrencyCode, props.currentUserPersonalDetails.login, selectedParticipants[0], diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index b15fc839f239..f864f8b084c0 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -713,14 +713,7 @@ describe('actions/IOU', () => { .then(() => { // When we split a bill offline fetch.pause(); - IOU.splitBill( - _.map([CARLOS_EMAIL, JULES_EMAIL, VIT_EMAIL], email => ({login: email})), - RORY_EMAIL, - amount, - comment, - CONST.CURRENCY.USD, - CONST.LOCALES.DEFAULT, - ); + IOU.splitBill(_.map([CARLOS_EMAIL, JULES_EMAIL, VIT_EMAIL], email => ({login: email})), RORY_EMAIL, amount, comment, CONST.CURRENCY.USD); return waitForPromisesToResolve(); }) .then(() => new Promise((resolve) => { diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 223344efd5d4..2387709ea624 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -1,5 +1,4 @@ import Onyx from 'react-native-onyx'; -import CONST from '../../src/CONST'; import * as IOUUtils from '../../src/libs/IOUUtils'; import * as ReportUtils from '../../src/libs/ReportUtils'; import * as NumberUtils from '../../src/libs/NumberUtils'; @@ -139,26 +138,26 @@ describe('IOUUtils', () => { beforeAll(() => initCurrencyList()); test('103 JPY split among 3 participants including the default user should be [35, 34, 34]', () => { const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 103, 'JPY', true)).toBe(3500); - expect(IOUUtils.calculateAmount(participants, 103, 'JPY')).toBe(3400); + expect(IOUUtils.calculateAmount(participants, 103, true)).toBe(3500); + expect(IOUUtils.calculateAmount(participants, 103)).toBe(3400); }); test('10 AFN split among 4 participants including the default user should be [1, 3, 3, 3]', () => { const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com', 'suestorm@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 10, 'AFN', true)).toBe(100); - expect(IOUUtils.calculateAmount(participants, 10, 'AFN')).toBe(300); + expect(IOUUtils.calculateAmount(participants, 10, true)).toBe(100); + expect(IOUUtils.calculateAmount(participants, 10)).toBe(300); }); test('10 BHD split among 3 participants including the default user should be [334, 333, 333]', () => { const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 10, 'BHD', true)).toBe(334); - expect(IOUUtils.calculateAmount(participants, 10, 'BHD')).toBe(333); + expect(IOUUtils.calculateAmount(participants, 10, true)).toBe(334); + expect(IOUUtils.calculateAmount(participants, 10)).toBe(333); }); test('0.02 USD split among 4 participants including the default user should be [-1, 1, 1, 1]', () => { const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com', 'suestorm@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 0.02, 'USD', true)).toBe(-1); - expect(IOUUtils.calculateAmount(participants, 0.02, 'USD')).toBe(1); + expect(IOUUtils.calculateAmount(participants, 0.02, true)).toBe(-1); + expect(IOUUtils.calculateAmount(participants, 0.02)).toBe(1); }); }); }); From 164dd2b939aade49a85c15d33799a30adcd018f4 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 12:44:24 -0700 Subject: [PATCH 61/85] Add convertToWholeUnits function --- src/libs/CurrencyUtils.js | 24 +++++++++++++++++++++--- tests/unit/CurrencyUtilsTest.js | 12 ++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/libs/CurrencyUtils.js b/src/libs/CurrencyUtils.js index 56cba966cfc9..97b778659305 100644 --- a/src/libs/CurrencyUtils.js +++ b/src/libs/CurrencyUtils.js @@ -74,9 +74,9 @@ function isCurrencySymbolLTR(currencyCode) { /** * Takes an amount as a floating point number and converts it to an integer amount. - * For example, given [25, USD], will return 2500. - * Given [25.50, USD] will return 2550. - * Given [2500, JPY], will return 2500. + * For example, given [25, USD], return 2500. + * Given [25.50, USD] return 2550. + * Given [2500, JPY], return 2500. * * @note we do not currently support any currencies with more than two decimal places. Sorry Tunisia :( * @@ -89,6 +89,23 @@ function convertToSmallestUnit(currency, amountAsFloat) { return Math.trunc(amountAsFloat * currencyUnit); } +/** + * Takes an amount as an interget and converts it to a floating point amount. + * For example, give [25, USD], return 0.25 + * Given [2550, USD], return 25.50 + * Given [2500, JPY], return 2500 + * + * @note we do not currency support any currencies with more than two decimal places. + * + * @param {String} currency + * @param {Number} amountAsInt + * @returns {Number} + */ +function convertToWholeUnit(currency, amountAsInt) { + const currencyUnit = getCurrencyUnit(currency); + return Math.trunc(amountAsInt) / currencyUnit; +} + /** * Given an amount in the smallest units of a currency, convert it to a string for display in the UI. * @@ -111,5 +128,6 @@ export { getLocalizedCurrencySymbol, isCurrencySymbolLTR, convertToSmallestUnit, + convertToWholeUnit, convertToDisplayString, }; diff --git a/tests/unit/CurrencyUtilsTest.js b/tests/unit/CurrencyUtilsTest.js index 2f2fc15cc8c3..f3ef02a6e731 100644 --- a/tests/unit/CurrencyUtilsTest.js +++ b/tests/unit/CurrencyUtilsTest.js @@ -100,6 +100,18 @@ describe('CurrencyUtils', () => { }); }); + describe('convertToWholeUnit', () => { + test.each([ + [CONST.CURRENCY.USD, 2500, 25], + [CONST.CURRENCY.USD, 2550, 25.5], + ['JPY', 25, 25], + ['JPY', 2500, 2500], + ['JPY', 25.5, 25], + ])('Correctly converts %s to amount in whole units', (currency, amount, expectedResult) => { + expect(CurrencyUtils.convertToWholeUnit(currency, amount)).toBe(expectedResult); + }); + }); + describe('convertToDisplayString', () => { test.each([ [CONST.CURRENCY.USD, 25, '$0.25'], From a968d311376f780510266c893f70bbdf1c86cc84 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 12:59:01 -0700 Subject: [PATCH 62/85] Standardize currency handling across the money request flow --- .../MoneyRequestConfirmationList.js | 30 ++++++------------- src/pages/iou/MoneyRequestModal.js | 23 +++++++------- src/pages/iou/steps/MoneyRequestAmountPage.js | 9 +++--- .../iou/steps/MoneyRequestConfirmPage.js | 2 +- 4 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index ee7e24a0068e..7a91f36feb64 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -19,6 +19,7 @@ import * as IOUUtils from '../libs/IOUUtils'; import MenuItemWithTopDescription from './MenuItemWithTopDescription'; import Navigation from '../libs/Navigation/Navigation'; import optionPropTypes from './optionPropTypes'; +import * as CurrencyUtils from '../libs/CurrencyUtils'; const propTypes = { /** Callback to inform parent modal of success */ @@ -31,7 +32,7 @@ const propTypes = { hasMultipleParticipants: PropTypes.bool.isRequired, /** IOU amount */ - iouAmount: PropTypes.string.isRequired, + iouAmount: PropTypes.number.isRequired, /** IOU type */ iouType: PropTypes.string, @@ -104,12 +105,10 @@ class MoneyRequestConfirmationList extends Component { */ getSplitOrRequestOptions() { return [{ - text: this.props.translate(this.props.hasMultipleParticipants ? 'iou.split' : 'iou.request', { - amount: this.props.numberFormat( - this.props.iouAmount, - {style: 'currency', currency: this.props.iou.selectedCurrencyCode}, - ), - }), + text: this.props.translate( + this.props.hasMultipleParticipants ? 'iou.split' : 'iou.request', + {amount: CurrencyUtils.convertToDisplayString(this.props.iou.selectedCurrencyCode, this.props.iouAmount)}, + ), value: this.props.hasMultipleParticipants ? CONST.IOU.MONEY_REQUEST_TYPE.SPLIT : CONST.IOU.MONEY_REQUEST_TYPE.REQUEST, }]; } @@ -137,13 +136,9 @@ class MoneyRequestConfirmationList extends Component { */ getParticipantsWithAmount(participants) { const iouAmount = IOUUtils.calculateAmount(participants, this.props.iouAmount); - return OptionsListUtils.getIOUConfirmationOptionsFromParticipants( participants, - this.props.numberFormat(iouAmount / 100, { - style: 'currency', - currency: this.props.iou.selectedCurrencyCode, - }), + CurrencyUtils.convertToDisplayString(this.props.iou.selectedCurrencyCode, iouAmount), ); } @@ -175,10 +170,7 @@ class MoneyRequestConfirmationList extends Component { const myIOUAmount = IOUUtils.calculateAmount(selectedParticipants, this.props.iouAmount, true); const formattedMyPersonalDetails = OptionsListUtils.getIOUConfirmationOptionsFromMyPersonalDetail( this.props.currentUserPersonalDetails, - this.props.numberFormat(myIOUAmount / 100, { - style: 'currency', - currency: this.props.iou.selectedCurrencyCode, - }), + CurrencyUtils.convertToDisplayString(this.props.iou.selectedCurrencyCode, myIOUAmount), ); sections.push({ @@ -270,10 +262,7 @@ class MoneyRequestConfirmationList extends Component { const shouldDisableButton = selectedParticipants.length === 0; const recipient = this.state.participants[0]; const canModifyParticipants = this.props.canModifyParticipants && this.props.hasMultipleParticipants; - const formattedAmount = this.props.numberFormat(this.props.iouAmount, { - style: 'currency', - currency: this.props.iou.selectedCurrencyCode, - }); + const formattedAmount = CurrencyUtils.convertToDisplayString(this.props.iou.selectedCurrencyCode, this.props.iouAmount); return ( { ? OptionsListUtils.getPolicyExpenseReportOptions(props.report) : OptionsListUtils.getParticipantsOptions(props.report, props.personalDetails), ); - const [amount, setAmount] = useState(''); + const [amount, setAmount] = useState(0); useEffect(() => { PersonalDetails.openMoneyRequestModalPage(); @@ -266,8 +266,6 @@ const MoneyRequestModal = (props) => { * @param {String} paymentMethodType */ const sendMoney = useCallback((paymentMethodType) => { - // TODO: convert correctly - const amountInDollars = Math.round(amount * 100); const currency = props.iou.selectedCurrencyCode; const trimmedComment = props.iou.comment.trim(); const participant = selectedOptions[0]; @@ -275,7 +273,7 @@ const MoneyRequestModal = (props) => { if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.ELSEWHERE) { IOU.sendMoneyElsewhere( props.report, - amountInDollars, + amount, currency, trimmedComment, props.currentUserPersonalDetails.login, @@ -287,7 +285,7 @@ const MoneyRequestModal = (props) => { if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.PAYPAL_ME) { IOU.sendMoneyViaPaypal( props.report, - amountInDollars, + amount, currency, trimmedComment, props.currentUserPersonalDetails.login, @@ -299,7 +297,7 @@ const MoneyRequestModal = (props) => { if (paymentMethodType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { IOU.sendMoneyWithWallet( props.report, - amountInDollars, + amount, currency, trimmedComment, props.currentUserPersonalDetails.login, @@ -314,12 +312,11 @@ const MoneyRequestModal = (props) => { const createTransaction = useCallback((selectedParticipants) => { const reportID = lodashGet(props.route, 'params.reportID', ''); const trimmedComment = props.iou.comment.trim(); - const amountInSmallestCurrencyUnit = CurrencyUtils.convertToSmallestUnit(props.iou.selectedCurrencyCode, Number.parseFloat(amount)); // IOUs created from a group report will have a reportID param in the route. // Since the user is already viewing the report, we don't need to navigate them to the report if (props.hasMultipleParticipants && CONST.REGEX.NUMBER.test(reportID)) { - IOU.splitBill(selectedParticipants, props.currentUserPersonalDetails.login, amountInSmallestCurrencyUnit, trimmedComment, props.iou.selectedCurrencyCode, reportID); + IOU.splitBill(selectedParticipants, props.currentUserPersonalDetails.login, amount, trimmedComment, props.iou.selectedCurrencyCode, reportID); return; } @@ -328,10 +325,9 @@ const MoneyRequestModal = (props) => { IOU.splitBillAndOpenReport( selectedParticipants, props.currentUserPersonalDetails.login, - amountInSmallestCurrencyUnit, + amount, trimmedComment, props.iou.selectedCurrencyCode, - props.preferredLocale, ); return; } @@ -342,7 +338,7 @@ const MoneyRequestModal = (props) => { } IOU.requestMoney( props.report, - amountInSmallestCurrencyUnit, + amount, props.iou.selectedCurrencyCode, props.currentUserPersonalDetails.login, selectedParticipants[0], @@ -381,12 +377,13 @@ const MoneyRequestModal = (props) => { {modalHeader} { - setAmount(value); + const amountInSmallestCurrencyUnits = CurrencyUtils.convertToSmallestUnit(props.iou.selectedCurrencyCode, Number.parseFloat(value)); + setAmount(amountInSmallestCurrencyUnits); navigateToNextStep(); }} reportID={reportID} hasMultipleParticipants={props.hasMultipleParticipants} - selectedAmount={amount} + selectedAmount={CurrencyUtils.convertToWholeUnit(props.iou.selectedCurrencyCode, amount)} navigation={props.navigation} iouType={props.iouType} buttonText={amountButtonText} diff --git a/src/pages/iou/steps/MoneyRequestAmountPage.js b/src/pages/iou/steps/MoneyRequestAmountPage.js index e80c2962201d..783f5d59a803 100755 --- a/src/pages/iou/steps/MoneyRequestAmountPage.js +++ b/src/pages/iou/steps/MoneyRequestAmountPage.js @@ -30,7 +30,7 @@ const propTypes = { onStepComplete: PropTypes.func.isRequired, /** Previously selected amount to show if the user comes back to this screen */ - selectedAmount: PropTypes.string.isRequired, + selectedAmount: PropTypes.number.isRequired, /** Text to display on the button that "saves" the amount */ buttonText: PropTypes.string.isRequired, @@ -66,12 +66,13 @@ class MoneyRequestAmountPage extends React.Component { this.numPadContainerViewID = 'numPadContainerView'; this.numPadViewID = 'numPadView'; + const selectedAmountAsString = props.selectedAmount ? props.selectedAmount.toString() : ''; this.state = { - amount: props.selectedAmount, + amount: selectedAmountAsString, shouldUpdateSelection: true, selection: { - start: props.selectedAmount.length, - end: props.selectedAmount.length, + start: selectedAmountAsString.length, + end: selectedAmountAsString.length, }, }; } diff --git a/src/pages/iou/steps/MoneyRequestConfirmPage.js b/src/pages/iou/steps/MoneyRequestConfirmPage.js index 673c229ad9c9..5aa8898d8843 100644 --- a/src/pages/iou/steps/MoneyRequestConfirmPage.js +++ b/src/pages/iou/steps/MoneyRequestConfirmPage.js @@ -15,7 +15,7 @@ const propTypes = { hasMultipleParticipants: PropTypes.bool.isRequired, /** IOU amount */ - iouAmount: PropTypes.string.isRequired, + iouAmount: PropTypes.number.isRequired, /** Selected participants from MoneyRequestModal with login */ participants: PropTypes.arrayOf(optionPropTypes).isRequired, From 296e4581f3b1b2a62edbd6b49c219e550ceaefcc Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 13:11:50 -0700 Subject: [PATCH 63/85] Use CurrencyUtils.convertToDisplayString everywhere --- src/components/CurrentWalletBalance.js | 7 +++---- src/components/ReportActionItem/IOUPreview.js | 7 +++---- src/pages/settings/InitialSettingsPage.js | 7 +++---- .../settings/Payments/TransferBalancePage.js | 20 ++++++++----------- src/pages/workspace/WorkspacesListPage.js | 7 +++---- 5 files changed, 20 insertions(+), 28 deletions(-) diff --git a/src/components/CurrentWalletBalance.js b/src/components/CurrentWalletBalance.js index 008cfb295338..e4af68fc10c8 100644 --- a/src/components/CurrentWalletBalance.js +++ b/src/components/CurrentWalletBalance.js @@ -1,11 +1,13 @@ import React from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; +import CONST from '../CONST'; import styles from '../styles/styles'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import compose from '../libs/compose'; import ONYXKEYS from '../ONYXKEYS'; import Text from './Text'; +import * as CurrencyUtils from '../libs/CurrencyUtils'; const propTypes = { /** The user's wallet account */ @@ -31,10 +33,7 @@ const defaultProps = { }; const CurrentWalletBalance = (props) => { - const formattedBalance = props.numberFormat( - props.userWallet.currentBalance / 100, // Divide by 100 because balance is in cents - {style: 'currency', currency: 'USD'}, - ); + const formattedBalance = CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, props.userWallet.currentBalance); return ( { name: ownerEmail, }; const cachedTotal = props.iouReport.total && props.iouReport.currency - ? props.numberFormat( - Math.abs(props.iouReport.total) / 100, - {style: 'currency', currency: props.iouReport.currency}, - ) : ''; + ? CurrencyUtils.convertToDisplayString(props.iouReport.currency, props.iouReport.total) + : ''; const avatarTooltip = [Str.removeSMSDomain(managerEmail), Str.removeSMSDomain(ownerEmail)]; const showContextMenu = (event) => { diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 774da65b3010..89ec6c5df584 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -38,6 +38,7 @@ import * as UserUtils from '../../libs/UserUtils'; import policyMemberPropType from '../policyMemberPropType'; import * as ReportActionContextMenu from '../home/report/ContextMenu/ReportActionContextMenu'; import {CONTEXT_MENU_TYPES} from '../home/report/ContextMenu/ContextMenuActions'; +import * as CurrencyUtils from '../../libs/CurrencyUtils'; const propTypes = { /* Onyx Props */ @@ -146,10 +147,8 @@ class InitialSettingsPage extends React.Component { */ getWalletBalance(isPaymentItem) { return (isPaymentItem && Permissions.canUseWallet(this.props.betas)) - ? this.props.numberFormat( - this.props.userWallet.currentBalance / 100, // Divide by 100 because balance is in cents - {style: 'currency', currency: 'USD'}, - ) : undefined; + ? CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, this.props.userWallet.currentBalance) + : undefined; } /** diff --git a/src/pages/settings/Payments/TransferBalancePage.js b/src/pages/settings/Payments/TransferBalancePage.js index d9bad8d84f77..50fd93a31d07 100644 --- a/src/pages/settings/Payments/TransferBalancePage.js +++ b/src/pages/settings/Payments/TransferBalancePage.js @@ -25,6 +25,7 @@ import ROUTES from '../../../ROUTES'; import FormAlertWithSubmitButton from '../../../components/FormAlertWithSubmitButton'; import {withNetwork} from '../../../components/OnyxProvider'; import ConfirmationPage from '../../../components/ConfirmationPage'; +import * as CurrencyUtils from '../../../libs/CurrencyUtils'; const propTypes = { /** User's wallet information */ @@ -71,9 +72,9 @@ class TransferBalancePage extends React.Component { title: this.props.translate('transferAmountPage.instant'), description: this.props.translate('transferAmountPage.instantSummary', { rate: this.props.numberFormat(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.RATE), - minAmount: this.props.numberFormat( - CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.MINIMUM_FEE / 100, - {style: 'currency', currency: 'USD'}, + minAmount: CurrencyUtils.convertToDisplayString( + CONST.CURRENCY.USD, + CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.MINIMUM_FEE ), }), icon: Expensicons.Bolt, @@ -187,7 +188,7 @@ class TransferBalancePage extends React.Component { onCloseButtonPress={() => Navigation.dismissModal(true)} /> - + @@ -242,10 +243,7 @@ class TransferBalancePage extends React.Component { - {this.props.numberFormat( - calculatedFee / 100, - {style: 'currency', currency: 'USD'}, - )} + {CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, calculatedFee)} @@ -255,10 +253,8 @@ class TransferBalancePage extends React.Component { 'transferAmountPage.transfer', { amount: isTransferable - ? this.props.numberFormat( - transferAmount / 100, - {style: 'currency', currency: 'USD'}, - ) : '', + ? CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, transferAmount) + : '', }, )} isLoading={this.props.walletTransfer.loading} diff --git a/src/pages/workspace/WorkspacesListPage.js b/src/pages/workspace/WorkspacesListPage.js index f1dbbbb7f1d3..932057fc4694 100755 --- a/src/pages/workspace/WorkspacesListPage.js +++ b/src/pages/workspace/WorkspacesListPage.js @@ -26,6 +26,7 @@ import BlockingView from '../../components/BlockingViews/BlockingView'; import {withNetwork} from '../../components/OnyxProvider'; import * as ReimbursementAccountProps from '../ReimbursementAccount/reimbursementAccountPropTypes'; import * as ReportUtils from '../../libs/ReportUtils'; +import * as CurrencyUtils from '../../libs/CurrencyUtils'; const propTypes = { /* Onyx Props */ @@ -110,10 +111,8 @@ class WorkspacesListPage extends Component { */ getWalletBalance(isPaymentItem) { return (isPaymentItem && Permissions.canUseWallet(this.props.betas)) - ? this.props.numberFormat( - this.props.userWallet.currentBalance / 100, // Divide by 100 because balance is in cents - {style: 'currency', currency: 'USD'}, - ) : undefined; + ? CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, this.props.userWallet.currentBalance) + : undefined; } /** From 1aba2a399c84cb7d0835bf4787acf5f1f6b29757 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 13:14:05 -0700 Subject: [PATCH 64/85] Refactor CurrencyUtils.convertToDisplayString with USD as default currency --- src/components/CurrentWalletBalance.js | 2 +- src/components/MoneyRequestConfirmationList.js | 8 ++++---- src/components/ReportActionItem/IOUPreview.js | 2 +- src/libs/CurrencyUtils.js | 4 ++-- src/libs/ReportUtils.js | 2 +- src/pages/settings/InitialSettingsPage.js | 2 +- src/pages/settings/Payments/TransferBalancePage.js | 9 +++------ src/pages/workspace/WorkspacesListPage.js | 2 +- tests/unit/CurrencyUtilsTest.js | 4 ++-- 9 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/components/CurrentWalletBalance.js b/src/components/CurrentWalletBalance.js index e4af68fc10c8..904ba503715d 100644 --- a/src/components/CurrentWalletBalance.js +++ b/src/components/CurrentWalletBalance.js @@ -33,7 +33,7 @@ const defaultProps = { }; const CurrentWalletBalance = (props) => { - const formattedBalance = CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, props.userWallet.currentBalance); + const formattedBalance = CurrencyUtils.convertToDisplayString(props.userWallet.currentBalance); return ( { name: ownerEmail, }; const cachedTotal = props.iouReport.total && props.iouReport.currency - ? CurrencyUtils.convertToDisplayString(props.iouReport.currency, props.iouReport.total) + ? CurrencyUtils.convertToDisplayString(props.iouReport.total, props.iouReport.currency) : ''; const avatarTooltip = [Str.removeSMSDomain(managerEmail), Str.removeSMSDomain(ownerEmail)]; diff --git a/src/libs/CurrencyUtils.js b/src/libs/CurrencyUtils.js index 97b778659305..aca93e205bef 100644 --- a/src/libs/CurrencyUtils.js +++ b/src/libs/CurrencyUtils.js @@ -109,11 +109,11 @@ function convertToWholeUnit(currency, amountAsInt) { /** * Given an amount in the smallest units of a currency, convert it to a string for display in the UI. * - * @param {String} currency * @param {Number} amountInSmallestUnit – should be an integer. Anything after a decimal place will be dropped. + * @param {String} currency * @returns {String} */ -function convertToDisplayString(currency, amountInSmallestUnit) { +function convertToDisplayString(amountInSmallestUnit, currency = CONST.CURRENCY.USD) { const currencyUnit = getCurrencyUnit(currency); const convertedAmount = Math.trunc(amountInSmallestUnit) / currencyUnit; return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, { diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index c709ade2fca4..9e10b9ccc6e1 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1016,7 +1016,7 @@ function buildOptimisticAddCommentReportAction(text, file) { * @returns {Object} */ function buildOptimisticIOUReport(ownerEmail, userEmail, total, chatReportID, currency, isSendingMoney = false) { - const formattedTotal = CurrencyUtils.convertToDisplayString(currency, total); + const formattedTotal = CurrencyUtils.convertToDisplayString(total, currency); return { // If we're sending money, hasOutstandingIOU should be false hasOutstandingIOU: !isSendingMoney, diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 89ec6c5df584..4c2c303dd961 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -147,7 +147,7 @@ class InitialSettingsPage extends React.Component { */ getWalletBalance(isPaymentItem) { return (isPaymentItem && Permissions.canUseWallet(this.props.betas)) - ? CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, this.props.userWallet.currentBalance) + ? CurrencyUtils.convertToDisplayString(this.props.userWallet.currentBalance) : undefined; } diff --git a/src/pages/settings/Payments/TransferBalancePage.js b/src/pages/settings/Payments/TransferBalancePage.js index 50fd93a31d07..9dc7eaa4302d 100644 --- a/src/pages/settings/Payments/TransferBalancePage.js +++ b/src/pages/settings/Payments/TransferBalancePage.js @@ -72,10 +72,7 @@ class TransferBalancePage extends React.Component { title: this.props.translate('transferAmountPage.instant'), description: this.props.translate('transferAmountPage.instantSummary', { rate: this.props.numberFormat(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.RATE), - minAmount: CurrencyUtils.convertToDisplayString( - CONST.CURRENCY.USD, - CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.MINIMUM_FEE - ), + minAmount: CurrencyUtils.convertToDisplayString(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.MINIMUM_FEE), }), icon: Expensicons.Bolt, type: CONST.PAYMENT_METHODS.DEBIT_CARD, @@ -243,7 +240,7 @@ class TransferBalancePage extends React.Component { - {CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, calculatedFee)} + {CurrencyUtils.convertToDisplayString(calculatedFee)} @@ -253,7 +250,7 @@ class TransferBalancePage extends React.Component { 'transferAmountPage.transfer', { amount: isTransferable - ? CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, transferAmount) + ? CurrencyUtils.convertToDisplayString(transferAmount) : '', }, )} diff --git a/src/pages/workspace/WorkspacesListPage.js b/src/pages/workspace/WorkspacesListPage.js index 932057fc4694..2bed6d9a61be 100755 --- a/src/pages/workspace/WorkspacesListPage.js +++ b/src/pages/workspace/WorkspacesListPage.js @@ -111,7 +111,7 @@ class WorkspacesListPage extends Component { */ getWalletBalance(isPaymentItem) { return (isPaymentItem && Permissions.canUseWallet(this.props.betas)) - ? CurrencyUtils.convertToDisplayString(CONST.CURRENCY.USD, this.props.userWallet.currentBalance) + ? CurrencyUtils.convertToDisplayString(this.props.userWallet.currentBalance) : undefined; } diff --git a/tests/unit/CurrencyUtilsTest.js b/tests/unit/CurrencyUtilsTest.js index f3ef02a6e731..c47445ce4353 100644 --- a/tests/unit/CurrencyUtilsTest.js +++ b/tests/unit/CurrencyUtilsTest.js @@ -122,7 +122,7 @@ describe('CurrencyUtils', () => { ['JPY', 2500, '¥2,500'], ['JPY', 25.5, '¥25'], ])('Correctly displays %s', (currency, amount, expectedResult) => { - expect(CurrencyUtils.convertToDisplayString(currency, amount)).toBe(expectedResult); + expect(CurrencyUtils.convertToDisplayString(amount, currency)).toBe(expectedResult); }); test.each([ @@ -132,7 +132,7 @@ describe('CurrencyUtils', () => { ['EUR', 250000000, '2.500.000,00\xa0€'], ])('Correctly displays %s in ES locale', (currency, amount, expectedResult) => ( Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES) - .then(() => expect(CurrencyUtils.convertToDisplayString(currency, amount)).toBe(expectedResult)) + .then(() => expect(CurrencyUtils.convertToDisplayString(amount, currency)).toBe(expectedResult)) )); }); }); From 6dc9879334a5a997ec09a63b37e6e28d25478333 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 13:16:48 -0700 Subject: [PATCH 65/85] Fix lint --- src/components/CurrentWalletBalance.js | 1 - src/pages/iou/MoneyRequestModal.js | 2 +- src/pages/settings/Payments/TransferBalancePage.js | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/CurrentWalletBalance.js b/src/components/CurrentWalletBalance.js index 904ba503715d..72f05d138939 100644 --- a/src/components/CurrentWalletBalance.js +++ b/src/components/CurrentWalletBalance.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import {withOnyx} from 'react-native-onyx'; -import CONST from '../CONST'; import styles from '../styles/styles'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import compose from '../libs/compose'; diff --git a/src/pages/iou/MoneyRequestModal.js b/src/pages/iou/MoneyRequestModal.js index 893f758e877d..33cb083dcf78 100644 --- a/src/pages/iou/MoneyRequestModal.js +++ b/src/pages/iou/MoneyRequestModal.js @@ -344,7 +344,7 @@ const MoneyRequestModal = (props) => { selectedParticipants[0], trimmedComment, ); - }, [amount, props.iou.comment, props.currentUserPersonalDetails.login, props.hasMultipleParticipants, props.iou.selectedCurrencyCode, props.preferredLocale, props.report, props.route]); + }, [amount, props.iou.comment, props.currentUserPersonalDetails.login, props.hasMultipleParticipants, props.iou.selectedCurrencyCode, props.report, props.route]); const currentStep = steps[currentStepIndex]; const moneyRequestStepIndex = _.indexOf(steps, Steps.MoneyRequestConfirm); diff --git a/src/pages/settings/Payments/TransferBalancePage.js b/src/pages/settings/Payments/TransferBalancePage.js index 9dc7eaa4302d..18d3f23afe9f 100644 --- a/src/pages/settings/Payments/TransferBalancePage.js +++ b/src/pages/settings/Payments/TransferBalancePage.js @@ -185,7 +185,7 @@ class TransferBalancePage extends React.Component { onCloseButtonPress={() => Navigation.dismissModal(true)} /> - + From dd11c8fe5a77a7f31d2d133d8c342dfcb057ce67 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 13:36:43 -0700 Subject: [PATCH 66/85] Create getLinkedTransactionID function --- src/libs/ReportActionsUtils.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 2e9ea66f7fd4..2ad1e2b7401d 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -272,6 +272,21 @@ function getLatestReportActionFromOnyxData(onyxData) { return _.last(sortedReportActions); } +/** + * Find the transaction associated with this reportAction, if one exists. + * + * @param {String} reportID + * @param {String} reportActionID + * @returns {String|null} + */ +function getLinkedTransactionID(reportID, reportActionID) { + const reportAction = lodashGet(allReportActions, [reportID, reportActionID]); + if (!reportAction || reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) { + return null; + } + return reportAction.originalMessage.IOUTransactionID; +} + export { getSortedReportActions, getLastVisibleAction, @@ -284,4 +299,5 @@ export { getSortedReportActionsForDisplay, getLastClosedReportAction, getLatestReportActionFromOnyxData, + getLinkedTransactionID,g }; From 89ee78594a9acc8d42f8404e6242fe1c0a9806ab Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 13:37:29 -0700 Subject: [PATCH 67/85] Fix typo --- src/libs/ReportActionsUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 2ad1e2b7401d..b12fab7b6d99 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -299,5 +299,5 @@ export { getSortedReportActionsForDisplay, getLastClosedReportAction, getLatestReportActionFromOnyxData, - getLinkedTransactionID,g + getLinkedTransactionID, }; From 9900bb0b7ebe9f713482f1a4a389e00bbf8c2e8a Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 13:41:33 -0700 Subject: [PATCH 68/85] Fix IOU tests --- tests/actions/IOUTest.js | 45 ++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index f864f8b084c0..b0ea170507c4 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -28,8 +28,7 @@ describe('actions/IOU', () => { describe('requestMoney', () => { it('creates new chat if needed', () => { - const amount = 100; - const amountInCents = amount * 100; + const amount = 10000; const comment = 'Giv money plz'; let chatReportID; let iouReportID; @@ -120,7 +119,7 @@ describe('actions/IOU', () => { expect(transaction.reportID).toBe(iouReportID); // Its amount should match the amount of the request - expect(transaction.amount).toBe(amountInCents); + expect(transaction.amount).toBe(amount); // The comment should be correct expect(transaction.comment.comment).toBe(comment); @@ -164,8 +163,7 @@ describe('actions/IOU', () => { }); it('updates existing chat report if there is one', () => { - const amount = 100; - const amountInCents = amount * 100; + const amount = 10000; const comment = 'Giv money plz'; let chatReport = { reportID: 1234, @@ -261,7 +259,7 @@ describe('actions/IOU', () => { expect(transaction.reportID).toBe(iouReportID); // Its amount should match the amount of the request - expect(transaction.amount).toBe(amountInCents); + expect(transaction.amount).toBe(amount); // The comment should be correct expect(transaction.comment.comment).toBe(comment); @@ -305,8 +303,7 @@ describe('actions/IOU', () => { }); it('updates existing IOU report if there is one', () => { - const amount = 100; - const amountInCents = amount * 100; + const amount = 10000; const comment = 'Giv money plz'; const chatReportID = 1234; const iouReportID = 5678; @@ -381,7 +378,7 @@ describe('actions/IOU', () => { iouReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.IOU); // The total on the iou report should be updated - expect(iouReport.total).toBe(1100); + expect(iouReport.total).toBe(11000); resolve(); }, @@ -432,7 +429,7 @@ describe('actions/IOU', () => { newTransaction = _.find(allTransactions, transaction => transaction.transactionID !== existingTransaction.transactionID); expect(newTransaction.reportID).toBe(iouReportID); - expect(newTransaction.amount).toBe(amountInCents); + expect(newTransaction.amount).toBe(amount); expect(newTransaction.comment.comment).toBe(comment); expect(newTransaction.merchant).toBe(CONST.REPORT.TYPE.IOU); expect(newTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -471,8 +468,7 @@ describe('actions/IOU', () => { }); it('correctly implements RedBrickRoad error handling', () => { - const amount = 100; - const amountInCents = amount * 100; + const amount = 10000; const comment = 'Giv money plz'; let chatReportID; let iouReportID; @@ -560,7 +556,7 @@ describe('actions/IOU', () => { transactionID = transaction.transactionID; expect(transaction.reportID).toBe(iouReportID); - expect(transaction.amount).toBe(amountInCents); + expect(transaction.amount).toBe(amount); expect(transaction.comment.comment).toBe(comment); expect(transaction.merchant).toBe(CONST.REPORT.TYPE.IOU); expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -619,8 +615,7 @@ describe('actions/IOU', () => { * - Rory and Vit have never chatted together before * - There is no existing group chat with the four of them */ - const amount = 4; - const amountInCents = amount * 100; + const amount = 400; const comment = 'Yes, I am splitting a bill for $4 USD'; let carlosChatReport = { reportID: NumberUtils.rand64(), @@ -734,7 +729,7 @@ describe('actions/IOU', () => { // 2. The IOU report with Rory + Carlos (new) carlosIOUReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.IOU && report.managerEmail === CARLOS_EMAIL); expect(_.isEmpty(carlosIOUReport)).toBe(false); - expect(carlosIOUReport.total).toBe(amountInCents / 4); + expect(carlosIOUReport.total).toBe(amount / 4); // 3. The chat report with Rory + Jules julesChatReport = _.find(allReports, report => report.reportID === julesChatReport.reportID); @@ -745,7 +740,7 @@ describe('actions/IOU', () => { julesIOUReport = _.find(allReports, report => report.reportID === julesIOUReport.reportID); expect(_.isEmpty(julesIOUReport)).toBe(false); expect(julesChatReport.pendingFields).toBeFalsy(); - expect(julesIOUReport.total).toBe(julesExistingTransaction.amount + (amountInCents / 4)); + expect(julesIOUReport.total).toBe(julesExistingTransaction.amount + (amount / 4)); // 5. The chat report with Rory + Vit (new) vitChatReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.CHAT && _.isEqual(report.participants, [VIT_EMAIL])); @@ -755,7 +750,7 @@ describe('actions/IOU', () => { // 6. The IOU report with Rory + Vit (new) vitIOUReport = _.find(allReports, report => report.type === CONST.REPORT.TYPE.IOU && report.managerEmail === VIT_EMAIL); expect(_.isEmpty(vitIOUReport)).toBe(false); - expect(vitIOUReport.total).toBe(amountInCents / 4); + expect(vitIOUReport.total).toBe(amount / 4); // 7. The group chat with everyone groupChat = _.find(allReports, report => report.type === CONST.REPORT.TYPE.CHAT && _.isEqual(report.participants, [CARLOS_EMAIL, JULES_EMAIL, VIT_EMAIL])); @@ -800,7 +795,7 @@ describe('actions/IOU', () => { carlosIOUAction = _.find(carlosReportActions, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); expect(carlosIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(carlosIOUAction.originalMessage.IOUReportID).toBe(carlosIOUReport.reportID); - expect(carlosIOUAction.originalMessage.amount).toBe(amountInCents / 4); + expect(carlosIOUAction.originalMessage.amount).toBe(amount / 4); expect(carlosIOUAction.originalMessage.comment).toBe(comment); expect(carlosIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); expect(Date.parse(carlosCreatedAction.created)).toBeLessThanOrEqual(Date.parse(carlosIOUAction.created)); @@ -814,7 +809,7 @@ describe('actions/IOU', () => { )); expect(julesIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(julesIOUAction.originalMessage.IOUReportID).toBe(julesIOUReport.reportID); - expect(julesIOUAction.originalMessage.amount).toBe(amountInCents / 4); + expect(julesIOUAction.originalMessage.amount).toBe(amount / 4); expect(julesIOUAction.originalMessage.comment).toBe(comment); expect(julesIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); expect(Date.parse(julesCreatedAction.created)).toBeLessThanOrEqual(Date.parse(julesIOUAction.created)); @@ -826,7 +821,7 @@ describe('actions/IOU', () => { expect(vitCreatedAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(vitIOUAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(vitIOUAction.originalMessage.IOUReportID).toBe(vitIOUReport.reportID); - expect(vitIOUAction.originalMessage.amount).toBe(amountInCents / 4); + expect(vitIOUAction.originalMessage.amount).toBe(amount / 4); expect(vitIOUAction.originalMessage.comment).toBe(comment); expect(vitIOUAction.originalMessage.type).toBe(CONST.IOU.REPORT_ACTION_TYPE.CREATE); expect(Date.parse(vitCreatedAction.created)).toBeLessThanOrEqual(Date.parse(vitIOUAction.created)); @@ -870,10 +865,10 @@ describe('actions/IOU', () => { expect(vitTransaction.reportID).toBe(vitIOUReport.reportID); expect(groupTransaction).toBeTruthy(); - expect(carlosTransaction.amount).toBe(amountInCents / 4); - expect(julesTransaction.amount).toBe(amountInCents / 4); - expect(vitTransaction.amount).toBe(amountInCents / 4); - expect(groupTransaction.amount).toBe(amountInCents); + expect(carlosTransaction.amount).toBe(amount / 4); + expect(julesTransaction.amount).toBe(amount / 4); + expect(vitTransaction.amount).toBe(amount / 4); + expect(groupTransaction.amount).toBe(amount); expect(carlosTransaction.comment.comment).toBe(comment); expect(julesTransaction.comment.comment).toBe(comment); From 609797b7b7e33b81aaace5450e233a0cc3938c0f Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 13:50:15 -0700 Subject: [PATCH 69/85] Fix IOUUtilsTest --- tests/unit/IOUUtilsTest.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 2387709ea624..68ce5339bc5e 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -136,28 +136,23 @@ describe('IOUUtils', () => { describe('calculateAmount', () => { beforeAll(() => initCurrencyList()); + test('103 JPY split among 3 participants including the default user should be [35, 34, 34]', () => { const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 103, true)).toBe(3500); - expect(IOUUtils.calculateAmount(participants, 103)).toBe(3400); + expect(IOUUtils.calculateAmount(participants, 103, true)).toBe(35); + expect(IOUUtils.calculateAmount(participants, 103)).toBe(34); }); test('10 AFN split among 4 participants including the default user should be [1, 3, 3, 3]', () => { const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com', 'suestorm@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 10, true)).toBe(100); - expect(IOUUtils.calculateAmount(participants, 10)).toBe(300); - }); - - test('10 BHD split among 3 participants including the default user should be [334, 333, 333]', () => { - const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 10, true)).toBe(334); - expect(IOUUtils.calculateAmount(participants, 10)).toBe(333); + expect(IOUUtils.calculateAmount(participants, 10, true)).toBe(1); + expect(IOUUtils.calculateAmount(participants, 10)).toBe(3); }); test('0.02 USD split among 4 participants including the default user should be [-1, 1, 1, 1]', () => { const participants = ['tonystark@expensify.com', 'reedrichards@expensify.com', 'suestorm@expensify.com']; - expect(IOUUtils.calculateAmount(participants, 0.02, true)).toBe(-1); - expect(IOUUtils.calculateAmount(participants, 0.02)).toBe(1); + expect(IOUUtils.calculateAmount(participants, 2, true)).toBe(-1); + expect(IOUUtils.calculateAmount(participants, 2)).toBe(1); }); }); }); From 80339aae265516b18908e7b2751a75711323f44a Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 14:10:23 -0700 Subject: [PATCH 70/85] Fix RBR for RequestMoney flow --- src/components/MoneyRequestConfirmationList.js | 2 +- src/libs/actions/IOU.js | 3 --- src/libs/actions/ReportActions.js | 5 +++++ src/styles/styles.js | 1 - tests/actions/IOUTest.js | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 20d0326c9b1b..c73c31e25e6c 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -303,7 +303,7 @@ class MoneyRequestConfirmationList extends Component { description={this.props.translate('iou.amount')} interactive={false} // This is so the menu item's background doesn't change color on hover onPress={() => this.props.navigateToStep(0)} - style={styles.moneyRequestMenuItem} + style={[styles.moneyRequestMenuItem, styles.mb5]} titleStyle={styles.moneyRequestConfirmationAmount} disabled={this.state.didConfirm} /> diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index caea5952ca06..d7e5af1f8d12 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -81,7 +81,6 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`, value: { - pendingAction: null, errors: { [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), }, @@ -154,7 +153,6 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com value: { [optimisticReportAction.reportActionID]: { ...optimisticReportAction, - pendingAction: null, errors: { [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), }, @@ -185,7 +183,6 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com // Then add an optimistic created action optimisticReportActionsData.value[optimisticCreatedAction.reportActionID] = optimisticCreatedAction; reportActionsSuccessData.value[optimisticCreatedAction.reportActionID] = {pendingAction: null}; - reportActionsFailureData.value[optimisticCreatedAction.reportActionID] = {pendingAction: null}; } const optimisticData = [ diff --git a/src/libs/actions/ReportActions.js b/src/libs/actions/ReportActions.js index 6205fa847ed2..59d59b9d463d 100644 --- a/src/libs/actions/ReportActions.js +++ b/src/libs/actions/ReportActions.js @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; +import * as ReportActionUtils from '../ReportActionsUtils'; /** * @param {String} reportID @@ -9,6 +10,10 @@ function deleteOptimisticReportAction(reportID, reportActionID) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { [reportActionID]: null, }); + const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(reportID, reportActionID); + if (linkedTransactionID) { + Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); + } } /** diff --git a/src/styles/styles.js b/src/styles/styles.js index 25b1be636292..e5a6e478cfd5 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -2310,7 +2310,6 @@ const styles = { borderRadius: 0, justifyContent: 'space-between', width: '100%', - marginBottom: 20, }, iouPreviewBox: { diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index b0ea170507c4..735448aff342 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -580,7 +580,7 @@ describe('actions/IOU', () => { Onyx.disconnect(connectionID); expect(_.size(reportActionsForChatReport)).toBe(2); iouAction = _.find(reportActionsForChatReport, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); - expect(iouAction.pendingAction).toBeFalsy(); + expect(iouAction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); const errorMessage = _.values(iouAction.errors)[0]; expect(errorMessage).toBe(Localize.translateLocal('iou.error.genericCreateFailureMessage')); resolve(); @@ -593,7 +593,7 @@ describe('actions/IOU', () => { waitForCollectionCallback: true, callback: (transaction) => { Onyx.disconnect(connectionID); - expect(transaction.pendingAction).toBeFalsy(); + expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); expect(transaction.errors).toBeTruthy(); expect(_.values(transaction.errors)[0]).toBe(Localize.translateLocal('iou.error.genericCreateFailureMessage')); resolve(); From 22c4c6537578c91b0288a886522d3a35e6d0c4aa Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 15:50:47 -0700 Subject: [PATCH 71/85] Standardize dismissal of reportAction errors and add test coverage --- src/components/ReportTransaction.js | 11 +---- src/libs/actions/ReportActions.js | 51 ++++++++++------------- src/pages/home/report/ReportActionItem.js | 8 +--- tests/actions/IOUTest.js | 34 +++++++++++++++ 4 files changed, 57 insertions(+), 47 deletions(-) diff --git a/src/components/ReportTransaction.js b/src/components/ReportTransaction.js index be53b11c742e..b88c2f759965 100644 --- a/src/components/ReportTransaction.js +++ b/src/components/ReportTransaction.js @@ -56,16 +56,7 @@ class ReportTransaction extends Component { render() { return ( { - if (this.props.action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { - ReportActions.clearSendMoneyErrors(this.props.chatReportID, this.props.action.reportActionID); - } - if (this.props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { - ReportActions.deleteOptimisticReportAction(this.props.chatReportID, this.props.action.reportActionID); - } else { - ReportActions.clearReportActionErrors(this.props.chatReportID, this.props.action.reportActionID); - } - }} + onClose={() => ReportActions.clearReportActionErrors(this.props.chatReportID, this.props.action)} pendingAction={this.props.action.pendingAction} errors={this.props.action.errors} errorRowStyles={[styles.ml10, styles.mr2]} diff --git a/src/libs/actions/ReportActions.js b/src/libs/actions/ReportActions.js index 59d59b9d463d..e7e6d63c220d 100644 --- a/src/libs/actions/ReportActions.js +++ b/src/libs/actions/ReportActions.js @@ -1,48 +1,39 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; +import CONST from '../../CONST'; import * as ReportActionUtils from '../ReportActionsUtils'; /** * @param {String} reportID - * @param {String} reportActionID + * @param {Object} reportAction */ -function deleteOptimisticReportAction(reportID, reportActionID) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - [reportActionID]: null, - }); - const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(reportID, reportActionID); - if (linkedTransactionID) { - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); +function clearReportActionErrors(reportID, reportAction) { + if (reportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { + // Delete the optimistic action + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + [reportAction.reportActionID]: null, + }); + + // If the optimistic action was a CREATED action, delete the report too + if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); + } + + // If there's a linked transaction, delete that too + const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(reportID, reportAction.reportActionID); + if (linkedTransactionID) { + Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); + } } -} -/** - * @param {String} reportID - * @param {String} reportActionID - */ -function clearReportActionErrors(reportID, reportActionID) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - [reportActionID]: { - errors: null, - }, - }); -} - -/** - * This method clears the errors for a chat where send money action was done - * @param {String} chatReportID - * @param {String} reportActionID - */ -function clearSendMoneyErrors(chatReportID, reportActionID) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, { - [reportActionID]: { + [reportAction.reportActionID]: { errors: null, }, }); } export { + // eslint-disable-next-line import/prefer-default-export clearReportActionErrors, - deleteOptimisticReportAction, - clearSendMoneyErrors, }; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index c43da3092177..7e96faceba88 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -304,13 +304,7 @@ class ReportActionItem extends Component { )} > { - if (this.props.action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { - ReportActions.deleteOptimisticReportAction(this.props.report.reportID, this.props.action.reportActionID); - } else { - ReportActions.clearReportActionErrors(this.props.report.reportID, this.props.action.reportActionID); - } - }} + onClose={() => ReportActions.clearReportActionErrors(this.props.report.reportID, this.props.action)} pendingAction={this.props.draftMessage ? null : this.props.action.pendingAction} errors={this.props.action.errors} errorRowStyles={[styles.ml10, styles.mr2]} diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index 735448aff342..b286ebee640a 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -8,6 +8,7 @@ import * as TestHelper from '../utils/TestHelper'; import DateUtils from '../../src/libs/DateUtils'; import * as NumberUtils from '../../src/libs/NumberUtils'; import * as Localize from '../../src/libs/Localize'; +import * as ReportActions from '../../src/libs/actions/ReportActions'; const CARLOS_EMAIL = 'cmartins@expensifail.com'; const JULES_EMAIL = 'jules@expensifail.com'; @@ -601,6 +602,39 @@ describe('actions/IOU', () => { }); })) + // If the user clears the errors on the IOU action + .then(() => new Promise((resolve) => { + ReportActions.clearReportActionErrors(chatReportID, iouAction); + resolve(); + })) + + // Then the reportAction should be removed from Onyx + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`, + waitForCollectionCallback: true, + callback: (reportActionsForReport) => { + Onyx.disconnect(connectionID); + iouAction = _.find(reportActionsForReport, reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU); + expect(iouAction).toBeFalsy(); + resolve(); + }, + }); + })) + + // Along with the associated transaction + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + waitForCollectionCallback: true, + callback: (transaction) => { + Onyx.disconnect(connectionID); + expect(transaction).toBeFalsy(); + resolve(); + }, + }); + })) + // Cleanup .then(fetch.succeed); }); From 9e86e643c67f5e0c838a9a4eace86ed4aa5aa8f9 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 16:41:10 -0700 Subject: [PATCH 72/85] Correctly implement DeleteReport --- src/languages/en.js | 1 + src/languages/es.js | 1 + src/libs/actions/IOU.js | 7 +++-- src/libs/actions/Report.js | 38 +++++++++++++++++++++++-- src/libs/actions/ReportActions.js | 7 ++--- tests/actions/IOUTest.js | 46 +++++++++++++++++++++++++++++++ 6 files changed, 91 insertions(+), 9 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index 80e3451b01b8..be8783a5edb4 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -1223,6 +1223,7 @@ export default { }, }, report: { + genericCreateReportFailureMessage: 'Unexpected error creating this chat, please try again later', genericAddCommentFailureMessage: 'Unexpected error while posting the comment, please try again later', noActivityYet: 'No activity yet', }, diff --git a/src/languages/es.js b/src/languages/es.js index e4382b7e9e2f..944e0722f933 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -1224,6 +1224,7 @@ export default { }, }, report: { + genericCreateReportFailureMessage: 'Error inesperado al crear el chat. Por favor, inténtalo más tarde', genericAddCommentFailureMessage: 'Error inesperado al agregar el comentario. Por favor, inténtalo más tarde', noActivityYet: 'Sin actividad todavía', }, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index d7e5af1f8d12..6877ba23a36d 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -152,7 +152,6 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport.reportID}`, value: { [optimisticReportAction.reportActionID]: { - ...optimisticReportAction, errors: { [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), }, @@ -178,7 +177,11 @@ function requestMoney(report, amount, currency, recipientEmail, participant, com errorFields: null, }, }; - chatReportFailureData.value.pendingFields = null; + chatReportFailureData.value.errorFields = { + createChat: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), + }, + }; // Then add an optimistic created action optimisticReportActionsData.value[optimisticCreatedAction.reportActionID] = optimisticCreatedAction; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 6991ae7f316d..ee6202582122 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -50,6 +50,12 @@ Onyx.connect({ }, }); +let allReportActions; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + callback: val => allReportActions = val, +}); + const allReports = {}; let conciergeChatReportID; const typingWatchTimers = {}; @@ -1103,13 +1109,40 @@ function addPolicyReport(policy, reportName, visibility) { Navigation.navigate(ROUTES.getReportRoute(policyReport.reportID)); } +/** + * Deletes a report, along with its reportActions, any linked reports, and any linked IOU report. + * + * @param {String} reportID + */ +function deleteReport(reportID) { + const report = allReports[reportID]; + const onyxData = { + [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: null, + }; + + // Delete linked transactions + const reportActionsForReport = allReportActions[reportID]; + _.chain(reportActionsForReport) + .filter(reportAction => reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) + .map(reportAction => reportAction.originalMessage.IOUTransactionID) + .uniq() + .each(transactionID => onyxData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] = null); + + Onyx.multiSet(onyxData); + + // Delete linked IOU report + if (report && report.iouReportID) { + deleteReport(report.iouReportID); + } +} + /** * @param {String} reportID The reportID of the policy report (workspace room) */ function navigateToConciergeChatAndDeleteReport(reportID) { navigateToConciergeChat(); - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, null); + deleteReport(reportID); } /** @@ -1479,6 +1512,7 @@ export { navigateToConciergeChat, setReportWithDraft, addPolicyReport, + deleteReport, navigateToConciergeChatAndDeleteReport, setIsComposerFullSize, markCommentAsUnread, diff --git a/src/libs/actions/ReportActions.js b/src/libs/actions/ReportActions.js index e7e6d63c220d..55d81cb6eb14 100644 --- a/src/libs/actions/ReportActions.js +++ b/src/libs/actions/ReportActions.js @@ -14,16 +14,13 @@ function clearReportActionErrors(reportID, reportAction) { [reportAction.reportActionID]: null, }); - // If the optimistic action was a CREATED action, delete the report too - if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); - } - // If there's a linked transaction, delete that too const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(reportID, reportAction.reportActionID); if (linkedTransactionID) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); } + + return; } Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index b286ebee640a..49c59e418239 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -9,6 +9,7 @@ import DateUtils from '../../src/libs/DateUtils'; import * as NumberUtils from '../../src/libs/NumberUtils'; import * as Localize from '../../src/libs/Localize'; import * as ReportActions from '../../src/libs/actions/ReportActions'; +import * as Report from '../../src/libs/actions/Report'; const CARLOS_EMAIL = 'cmartins@expensifail.com'; const JULES_EMAIL = 'jules@expensifail.com'; @@ -635,6 +636,51 @@ describe('actions/IOU', () => { }); })) + // If a user clears the errors on the CREATED action (which, technically are just errors on the report) + .then(() => new Promise((resolve) => { + Report.deleteReport(chatReportID); + resolve(); + })) + + // Then the report should be deleted + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (allReports) => { + Onyx.disconnect(connectionID); + _.each(allReports, report => expect(report).toBeFalsy()); + resolve(); + }, + }); + })) + + // All reportActions should also be deleted + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + waitForCollectionCallback: true, + callback: (allReportActions) => { + Onyx.disconnect(connectionID); + _.each(allReportActions, reportAction => expect(reportAction).toBeFalsy()); + resolve(); + }, + }); + })) + + // All transactions should also be deleted + .then(() => new Promise((resolve) => { + const connectionID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (allTransactions) => { + Onyx.disconnect(connectionID); + _.each(allTransactions, transaction => expect(transaction).toBeFalsy()); + resolve(); + }, + }); + })) + // Cleanup .then(fetch.succeed); }); From 92cbd4fb46b3645d3a63de1da86b40c483633488 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 16:55:52 -0700 Subject: [PATCH 73/85] Correctly implement RBR for SplitBill and SendMoney --- src/libs/actions/IOU.js | 98 +++++++++++++++++++++++++++-------------- 1 file changed, 66 insertions(+), 32 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 6877ba23a36d..cc4cf644fde0 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -310,11 +310,6 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment ]; const successData = [ - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${groupChatReport.reportID}`, - value: {pendingFields: {createChat: null}}, - }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${groupChatReport.reportID}`, @@ -330,27 +325,30 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment }, ]; - const failureData = [ - { + if (!existingGroupChatReport) { + successData.push({ onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${groupChatReport.reportID}`, - value: { - pendingFields: {createChat: null}, - }, - }, + value: {pendingFields: {createChat: null}}, + }); + } + + const failureData = [ { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${groupChatReport.reportID}`, value: { - ...(existingGroupChatReport ? {} : {[groupCreatedReportAction.reportActionID]: {pendingAction: null}}), - [groupIOUReportAction.reportActionID]: {pendingAction: null}, + [groupIOUReportAction.reportActionID]: { + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), + }, + }, }, }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${groupTransaction.transactionID}`, value: { - pendingAction: null, errors: { [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), }, @@ -358,6 +356,22 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment }, ]; + if (!existingGroupChatReport) { + failureData.push({ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${groupChatReport.reportID}`, + value: { + errorFields: { + createChat: { + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), + }, + }, + }, + }, + }); + } + // Loop through participants creating individual chats, iouReports and reportActionIDs as needed const splitAmount = IOUUtils.calculateAmount(participants, amount, false); const splits = [{email: currentUserEmail, amount: IOUUtils.calculateAmount(participants, amount, true)}]; @@ -445,11 +459,6 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment ); successData.push( - { - onyxMethod: CONST.ONYX.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.reportID}`, - value: {pendingFields: {createChat: null}}, - }, { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oneOnOneChatReport.reportID}`, @@ -468,24 +477,19 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment }, ); - failureData.push( - { + if (!existingOneOnOneChatReport) { + successData.push({ onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.reportID}`, - value: { - pendingFields: {createChat: null}, - hasOutstandingIOU: existingOneOnOneChatReport ? existingOneOnOneChatReport.hasOutstandingIOU : false, - iouReportID: existingOneOnOneChatReport ? existingOneOnOneChatReport.iouReportID : null, - }, - }, + value: {pendingFields: {createChat: null}}, + }); + } + + failureData.push( { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oneOnOneChatReport.reportID}`, value: { - ...(existingOneOnOneChatReport - ? {} - : {[oneOnOneCreatedReportAction.reportActionID]: {pendingAction: null}} - ), [oneOnOneIOUReportAction.reportActionID]: {pendingAction: null}, }, }, @@ -493,7 +497,6 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${oneOnOneTransaction.transactionID}`, value: { - pendingAction: null, errors: { [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), }, @@ -501,6 +504,24 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment }, ); + if (!existingOneOnOneChatReport) { + failureData.push({ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.reportID}`, + value: { + hasOutstandingIOU: existingOneOnOneChatReport ? existingOneOnOneChatReport.hasOutstandingIOU : false, + iouReportID: existingOneOnOneChatReport ? existingOneOnOneChatReport.iouReportID : null, + errorFields: { + createChat: { + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), + }, + }, + }, + }, + }); + } + // Regardless of the number of participants, we always want to push the iouReport update to onyxData optimisticData.push({ // We want to use set in case we are creating the the optimistic chat. @@ -859,6 +880,19 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType key: optimisticChatReportData.key, value: {pendingFields: null}, }); + failureData.push({ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: optimisticChatReportData.key, + value: { + errorFields: { + createChat: { + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), + }, + }, + }, + }, + }); // Add an optimistic created action to the optimistic reportActions data optimisticReportActionsData.value[optimisticCreatedAction.reportActionID] = optimisticCreatedAction; From 75bffd37dcf3d1add60dcf5e73d3ff3c1e835c67 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 17:09:23 -0700 Subject: [PATCH 74/85] Fix incorrect extra nesting of errors --- src/libs/actions/IOU.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index cc4cf644fde0..7014aabfae09 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -363,9 +363,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment value: { errorFields: { createChat: { - errors: { - [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), - }, + [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), }, }, }, @@ -513,9 +511,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment iouReportID: existingOneOnOneChatReport ? existingOneOnOneChatReport.iouReportID : null, errorFields: { createChat: { - errors: { - [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), - }, + [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), }, }, }, @@ -886,9 +882,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType value: { errorFields: { createChat: { - errors: { - [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), - }, + [DateUtils.getMicroseconds()]: Localize.translateLocal('report.genericCreateReportFailureMessage'), }, }, }, From 37b7aaac32627e09208e82b789060283ddc41a30 Mon Sep 17 00:00:00 2001 From: rory Date: Wed, 3 May 2023 17:33:00 -0700 Subject: [PATCH 75/85] Fix deleteReport and RBR for SplitBill --- src/libs/HttpUtils.js | 8 ++++++++ src/libs/actions/IOU.js | 6 +++++- src/libs/actions/Report.js | 11 +++++++++-- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index db76fb45b0d9..072f21791f8b 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -42,6 +42,14 @@ function processHTTPRequest(url, method = 'get', body = null, canCancel = true, signal = command === CONST.NETWORK.COMMAND.RECONNECT_APP ? reconnectAppCancellationController.signal : cancellationController.signal; } + if (command === 'RequestMoney' || command === 'SplitBill' || command === 'SplitBillAndOpenReport') { + console.log('RORY_DEBUG transactionID', body.get('transactionID')); + return Promise.resolve({ + jsonCode: 400, + message: 'Bad Request', + }); + } + return fetch(url, { // We hook requests to the same Controller signal, so we can cancel them all at once signal, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7014aabfae09..89745f7a0632 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -488,7 +488,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, amount, comment onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${oneOnOneChatReport.reportID}`, value: { - [oneOnOneIOUReportAction.reportActionID]: {pendingAction: null}, + [oneOnOneIOUReportAction.reportActionID]: { + errors: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('iou.error.genericCreateFailureMessage'), + }, + }, }, }, { diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index ee6202582122..378bd0d83e1d 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -20,6 +20,7 @@ import DateUtils from '../DateUtils'; import * as ReportActionsUtils from '../ReportActionsUtils'; import * as OptionsListUtils from '../OptionsListUtils'; import * as Localize from '../Localize'; +import * as CollectionUtils from '../CollectionUtils'; let currentUserEmail; let currentUserAccountID; @@ -50,10 +51,16 @@ Onyx.connect({ }, }); -let allReportActions; +const allReportActions = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - callback: val => allReportActions = val, + callback: (actions, key) => { + if (!key || !actions) { + return; + } + const reportID = CollectionUtils.extractCollectionItemID(key); + allReportActions[reportID] = actions; + }, }); const allReports = {}; From a01c8b90d232613ddcc4f610b20920fc95bcb5c1 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 4 May 2023 10:14:56 -0700 Subject: [PATCH 76/85] Remove accidentally committed code --- src/libs/HttpUtils.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/libs/HttpUtils.js b/src/libs/HttpUtils.js index 072f21791f8b..db76fb45b0d9 100644 --- a/src/libs/HttpUtils.js +++ b/src/libs/HttpUtils.js @@ -42,14 +42,6 @@ function processHTTPRequest(url, method = 'get', body = null, canCancel = true, signal = command === CONST.NETWORK.COMMAND.RECONNECT_APP ? reconnectAppCancellationController.signal : cancellationController.signal; } - if (command === 'RequestMoney' || command === 'SplitBill' || command === 'SplitBillAndOpenReport') { - console.log('RORY_DEBUG transactionID', body.get('transactionID')); - return Promise.resolve({ - jsonCode: 400, - message: 'Bad Request', - }); - } - return fetch(url, { // We hook requests to the same Controller signal, so we can cancel them all at once signal, From af9c8248d2528566fe45096c7d07792a4886e95e Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 4 May 2023 11:12:52 -0700 Subject: [PATCH 77/85] Update IntlPolyfill to include ListFormat --- package-lock.json | 166 +++++++++++------- package.json | 9 +- src/CONST.js | 8 + src/libs/IntlPolyfill.js | 66 +++++++ src/libs/IntlPolyfill/index.js | 15 -- src/libs/IntlPolyfill/index.native.js | 17 -- src/libs/IntlPolyfill/polyfillNumberFormat.js | 13 -- src/libs/IntlPolyfill/shouldPolyfill.js | 29 --- 8 files changed, 184 insertions(+), 139 deletions(-) create mode 100644 src/libs/IntlPolyfill.js delete mode 100644 src/libs/IntlPolyfill/index.js delete mode 100644 src/libs/IntlPolyfill/index.native.js delete mode 100644 src/libs/IntlPolyfill/polyfillNumberFormat.js delete mode 100644 src/libs/IntlPolyfill/shouldPolyfill.js diff --git a/package-lock.json b/package-lock.json index 704c5f438fdc..5ee55af932e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,11 @@ "license": "MIT", "dependencies": { "@expensify/react-native-web": "0.18.15", - "@formatjs/intl-getcanonicallocales": "^1.5.8", - "@formatjs/intl-locale": "^2.4.21", - "@formatjs/intl-numberformat": "^6.2.5", - "@formatjs/intl-pluralrules": "^4.0.13", + "@formatjs/intl-getcanonicallocales": "^2.2.0", + "@formatjs/intl-listformat": "^7.2.2", + "@formatjs/intl-locale": "^3.3.0", + "@formatjs/intl-numberformat": "^8.5.0", + "@formatjs/intl-pluralrules": "^5.2.2", "@gorhom/portal": "^1.0.14", "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", "@onfido/react-native-sdk": "7.4.0", @@ -2474,58 +2475,77 @@ "license": "MIT" }, "node_modules/@formatjs/ecma402-abstract": { - "version": "1.11.4", - "license": "MIT", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.15.0.tgz", + "integrity": "sha512-7bAYAv0w4AIao9DNg0avfOLTCPE9woAgs6SpXuMq11IN3A+l+cq8ghczwqSZBM11myvPSJA7vLn72q0rJ0QK6Q==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.32", + "tslib": "^2.4.0" + } + }, + "node_modules/@formatjs/intl-enumerator": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-enumerator/-/intl-enumerator-1.3.0.tgz", + "integrity": "sha512-q563xxoaQC6lu4VcDTsf/bbVtSDjXElbNGF5oguT3TpF3jdqxJgS4o1M+6qwjoubUYknLMfHM89OXU1rzhqSVQ==", "dependencies": { - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" + "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-getcanonicallocales": { - "version": "1.9.2", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.2.0.tgz", + "integrity": "sha512-LvjCj2DFaD8NSHW5pGBOcvgTzn5bT9aj4iNY/ejwVWkpAowoy1B4bZVXAJXLda5zqPV16zw/IXhxseiSflO/4A==", "dependencies": { - "tslib": "^2.1.0" + "tslib": "^2.4.0" } }, - "node_modules/@formatjs/intl-locale": { - "version": "2.4.47", - "license": "MIT", + "node_modules/@formatjs/intl-listformat": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.2.2.tgz", + "integrity": "sha512-YIruRGwUrmgVOXjWi6VbwPcRNBkEfgK2DFjyyqopCmpfJ+39vnl46oLpVchErnuXs6kkARy5GcGaGV7xRsH4lw==", "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/intl-getcanonicallocales": "1.9.2", - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "1.15.0", + "@formatjs/intl-localematcher": "0.2.32", + "tslib": "^2.4.0" } }, - "node_modules/@formatjs/intl-localematcher": { - "version": "0.2.25", - "license": "MIT", + "node_modules/@formatjs/intl-locale": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-3.3.0.tgz", + "integrity": "sha512-mKfnuvPZqzrOBpwnWHXfmdp78oLe0fNHGevX/k7KMk9LqTEIL64qt1G26kRwvFs88NdAYrCJVVOKShTTt0Qw6Q==", "dependencies": { - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "1.15.0", + "@formatjs/intl-enumerator": "1.3.0", + "@formatjs/intl-getcanonicallocales": "2.2.0", + "tslib": "^2.4.0" } }, - "node_modules/@formatjs/intl-numberformat": { - "version": "6.2.10", - "license": "MIT", + "node_modules/@formatjs/intl-localematcher": { + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz", + "integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==", "dependencies": { - "@formatjs/ecma402-abstract": "1.7.1", - "tslib": "^2.1.0" + "tslib": "^2.4.0" } }, - "node_modules/@formatjs/intl-numberformat/node_modules/@formatjs/ecma402-abstract": { - "version": "1.7.1", - "license": "MIT", + "node_modules/@formatjs/intl-numberformat": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-numberformat/-/intl-numberformat-8.5.0.tgz", + "integrity": "sha512-z6qseXkCBp5UpSmg7oUKcq/mzum6ZMmMWrSkcVerF2jW289yX7ElXVxD0Da3VwumSVhJUxxYeyYogBbTFQ/kLw==", "dependencies": { - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "1.15.0", + "@formatjs/intl-localematcher": "0.2.32", + "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-pluralrules": { - "version": "4.3.3", - "license": "MIT", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.2.tgz", + "integrity": "sha512-mEbnbRzsSCIYqaBmrmUlOsPu5MG6KfMcnzekPzUrUucX2dNiI1KWBGHK6IoXl5c8zx60L1NXJ6cSQ7akoc15SQ==", "dependencies": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "1.15.0", + "@formatjs/intl-localematcher": "0.2.32", + "tslib": "^2.4.0" } }, "node_modules/@gar/promisify": { @@ -42839,53 +42859,77 @@ "version": "1.0.0" }, "@formatjs/ecma402-abstract": { - "version": "1.11.4", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.15.0.tgz", + "integrity": "sha512-7bAYAv0w4AIao9DNg0avfOLTCPE9woAgs6SpXuMq11IN3A+l+cq8ghczwqSZBM11myvPSJA7vLn72q0rJ0QK6Q==", "requires": { - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" + "@formatjs/intl-localematcher": "0.2.32", + "tslib": "^2.4.0" + } + }, + "@formatjs/intl-enumerator": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-enumerator/-/intl-enumerator-1.3.0.tgz", + "integrity": "sha512-q563xxoaQC6lu4VcDTsf/bbVtSDjXElbNGF5oguT3TpF3jdqxJgS4o1M+6qwjoubUYknLMfHM89OXU1rzhqSVQ==", + "requires": { + "tslib": "^2.4.0" } }, "@formatjs/intl-getcanonicallocales": { - "version": "1.9.2", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-2.2.0.tgz", + "integrity": "sha512-LvjCj2DFaD8NSHW5pGBOcvgTzn5bT9aj4iNY/ejwVWkpAowoy1B4bZVXAJXLda5zqPV16zw/IXhxseiSflO/4A==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@formatjs/intl-listformat": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.2.2.tgz", + "integrity": "sha512-YIruRGwUrmgVOXjWi6VbwPcRNBkEfgK2DFjyyqopCmpfJ+39vnl46oLpVchErnuXs6kkARy5GcGaGV7xRsH4lw==", "requires": { - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "1.15.0", + "@formatjs/intl-localematcher": "0.2.32", + "tslib": "^2.4.0" } }, "@formatjs/intl-locale": { - "version": "2.4.47", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-3.3.0.tgz", + "integrity": "sha512-mKfnuvPZqzrOBpwnWHXfmdp78oLe0fNHGevX/k7KMk9LqTEIL64qt1G26kRwvFs88NdAYrCJVVOKShTTt0Qw6Q==", "requires": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/intl-getcanonicallocales": "1.9.2", - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "1.15.0", + "@formatjs/intl-enumerator": "1.3.0", + "@formatjs/intl-getcanonicallocales": "2.2.0", + "tslib": "^2.4.0" } }, "@formatjs/intl-localematcher": { - "version": "0.2.25", + "version": "0.2.32", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz", + "integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==", "requires": { - "tslib": "^2.1.0" + "tslib": "^2.4.0" } }, "@formatjs/intl-numberformat": { - "version": "6.2.10", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-numberformat/-/intl-numberformat-8.5.0.tgz", + "integrity": "sha512-z6qseXkCBp5UpSmg7oUKcq/mzum6ZMmMWrSkcVerF2jW289yX7ElXVxD0Da3VwumSVhJUxxYeyYogBbTFQ/kLw==", "requires": { - "@formatjs/ecma402-abstract": "1.7.1", - "tslib": "^2.1.0" - }, - "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.7.1", - "requires": { - "tslib": "^2.1.0" - } - } + "@formatjs/ecma402-abstract": "1.15.0", + "@formatjs/intl-localematcher": "0.2.32", + "tslib": "^2.4.0" } }, "@formatjs/intl-pluralrules": { - "version": "4.3.3", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-5.2.2.tgz", + "integrity": "sha512-mEbnbRzsSCIYqaBmrmUlOsPu5MG6KfMcnzekPzUrUucX2dNiI1KWBGHK6IoXl5c8zx60L1NXJ6cSQ7akoc15SQ==", "requires": { - "@formatjs/ecma402-abstract": "1.11.4", - "@formatjs/intl-localematcher": "0.2.25", - "tslib": "^2.1.0" + "@formatjs/ecma402-abstract": "1.15.0", + "@formatjs/intl-localematcher": "0.2.32", + "tslib": "^2.4.0" } }, "@gar/promisify": { diff --git a/package.json b/package.json index b0659bfcb9f1..af888e04a59e 100644 --- a/package.json +++ b/package.json @@ -44,10 +44,11 @@ }, "dependencies": { "@expensify/react-native-web": "0.18.15", - "@formatjs/intl-getcanonicallocales": "^1.5.8", - "@formatjs/intl-locale": "^2.4.21", - "@formatjs/intl-numberformat": "^6.2.5", - "@formatjs/intl-pluralrules": "^4.0.13", + "@formatjs/intl-getcanonicallocales": "^2.2.0", + "@formatjs/intl-listformat": "^7.2.2", + "@formatjs/intl-locale": "^3.3.0", + "@formatjs/intl-numberformat": "^8.5.0", + "@formatjs/intl-pluralrules": "^5.2.2", "@gorhom/portal": "^1.0.14", "@oguzhnatly/react-native-image-manipulator": "github:Expensify/react-native-image-manipulator#c5f654fc9d0ad7cc5b89d50b34ecf8b0e3f4d050", "@onfido/react-native-sdk": "7.4.0", diff --git a/src/CONST.js b/src/CONST.js index 7baa3cda3ed0..c8350d787ed6 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -2256,6 +2256,14 @@ const CONST = { GENERIC_ZIP_CODE_REGEX: /^(?:(?![\s-])[\w -]{0,9}[\w])?$/, + INTL_POLYFILLS: { + GET_CANONICAL_LOCALES: 'intl-getcanonicallocales', + LOCALE: 'intl-locale', + PLURAL_RULES: 'intl-pluralrules', + NUMBER_FORMAT: 'intl-numberformat', + LIST_FORMAT: 'intl-listformat', + }, + // Values for checking if polyfill is required on a platform POLYFILL_TEST: { STYLE: 'currency', diff --git a/src/libs/IntlPolyfill.js b/src/libs/IntlPolyfill.js new file mode 100644 index 000000000000..019a35e7d8a2 --- /dev/null +++ b/src/libs/IntlPolyfill.js @@ -0,0 +1,66 @@ +import lodashGet from 'lodash/get'; +import lodashMerge from 'lodash/merge'; +import {shouldPolyfill as shouldPolyfillGetCanonicalLocales} from '@formatjs/intl-getcanonicallocales/should-polyfill'; +import {shouldPolyfill as shouldPolyfillIntlLocale} from '@formatjs/intl-locale/should-polyfill'; +import {shouldPolyfill as shouldPolyfillPluralRules} from '@formatjs/intl-pluralrules/should-polyfill'; +import {shouldPolyfill as shouldPolyfillNumberFormat} from '@formatjs/intl-numberformat/should-polyfill'; +import {shouldPolyfill as shouldPolyfillListFormat} from '@formatjs/intl-listformat/should-polyfill'; +import CONST from '../CONST'; +import BaseLocaleListener from './Localize/LocaleListener/BaseLocaleListener'; + +const polyfills = {}; + +/** + * Check if the locale data is as expected on the device. + * Ensures that the currency data is consistent across devices. + * + * @returns {Boolean} + */ +function hasOldCurrencyData() { + return ( + new Intl.NumberFormat(CONST.LOCALES.DEFAULT, { + style: CONST.POLYFILL_TEST.STYLE, + currency: CONST.POLYFILL_TEST.CURRENCY, + currencyDisplay: CONST.POLYFILL_TEST.FORMAT, + }).format(CONST.POLYFILL_TEST.SAMPLE_INPUT) !== CONST.POLYFILL_TEST.EXPECTED_OUTPUT + ); +} + +/** + * Polyfill the Intl API if the ICU version if needed + * This ensures that the currency data is consistent across platforms and browsers. + * + * @param {String} locale + */ +export default function intlPolyfill(locale) { + if (shouldPolyfillGetCanonicalLocales() && !polyfills[CONST.INTL_POLYFILLS.GET_CANONICAL_LOCALES]) { + require('@formatjs/intl-getcanonicallocales/polyfill'); + polyfills[CONST.INTL_POLYFILLS.GET_CANONICAL_LOCALES] = true; + } + + if (shouldPolyfillIntlLocale() && !polyfills[CONST.INTL_POLYFILLS.LOCALE]) { + require('@formatjs/intl-locale/polyfill'); + polyfills[CONST.INTL_POLYFILLS.LOCALE] = true; + } + + if (shouldPolyfillPluralRules(locale) && !lodashGet(polyfills, [CONST.INTL_POLYFILLS.PLURAL_RULES, locale], false)) { + require('@formatjs/intl-pluralrules/polyfill-force'); + require(`@formatjs/intl-pluralrules/locale-data/${locale}`); + lodashMerge(polyfills, {[CONST.INTL_POLYFILLS.PLURAL_RULES]: {locale: true}}); + } + + if ((shouldPolyfillNumberFormat(locale) || hasOldCurrencyData()) && !lodashGet(polyfills, [CONST.INTL_POLYFILLS.NUMBER_FORMAT, locale], false)) { + require('@formatjs/intl-numberformat/polyfill-force'); + require(`@formatjs/intl-numberformat/locale-data/${locale}`); + lodashMerge(polyfills, {[CONST.INTL_POLYFILLS.NUMBER_FORMAT]: {locale: true}}); + } + + if (shouldPolyfillListFormat(locale) && !lodashGet(polyfills, [CONST.INTL_POLYFILLS.LIST_FORMAT, locale], false)) { + require('@formatjs/intl-listformat/polyfill-force'); + require(`@formatjs/intl-listformat/locale-data/${locale}`); + lodashMerge(polyfills, {[CONST.INTL_POLYFILLS.LIST_FORMAT]: {locale: true}}); + } +} + +intlPolyfill(CONST.LOCALES.DEFAULT); +BaseLocaleListener.connect(intlPolyfill); diff --git a/src/libs/IntlPolyfill/index.js b/src/libs/IntlPolyfill/index.js deleted file mode 100644 index 17d07f32cfce..000000000000 --- a/src/libs/IntlPolyfill/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import shouldPolyfill from './shouldPolyfill'; -import polyfillNumberFormat from './polyfillNumberFormat'; - -/** - * Polyfill the Intl API if the ICU version is old. - * This ensures that the currency data is consistent across platforms and browsers. - */ -export default function intlPolyfill() { - if (!shouldPolyfill()) { - return; - } - - // Just need to polyfill Intl.NumberFormat for web based platforms - polyfillNumberFormat(); -} diff --git a/src/libs/IntlPolyfill/index.native.js b/src/libs/IntlPolyfill/index.native.js deleted file mode 100644 index b104d490e091..000000000000 --- a/src/libs/IntlPolyfill/index.native.js +++ /dev/null @@ -1,17 +0,0 @@ -import shouldPolyfill from './shouldPolyfill'; -import polyfillNumberFormat from './polyfillNumberFormat'; - -/** - * Polyfill the Intl API, always performed for native devices. - */ -export default function polyfill() { - if (!shouldPolyfill()) { - return; - } - - // Native devices require extra polyfills - require('@formatjs/intl-getcanonicallocales/polyfill'); - require('@formatjs/intl-locale/polyfill'); - require('@formatjs/intl-pluralrules/polyfill'); - polyfillNumberFormat(); -} diff --git a/src/libs/IntlPolyfill/polyfillNumberFormat.js b/src/libs/IntlPolyfill/polyfillNumberFormat.js deleted file mode 100644 index 91bda9d6c368..000000000000 --- a/src/libs/IntlPolyfill/polyfillNumberFormat.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Common imports that are required for all platforms - */ - -function polyfillNumberFormat() { - require('@formatjs/intl-numberformat/polyfill-force'); - - // Load en & es Locale data - require('@formatjs/intl-numberformat/locale-data/en'); - require('@formatjs/intl-numberformat/locale-data/es'); -} - -export default polyfillNumberFormat; diff --git a/src/libs/IntlPolyfill/shouldPolyfill.js b/src/libs/IntlPolyfill/shouldPolyfill.js deleted file mode 100644 index 0845056d9a72..000000000000 --- a/src/libs/IntlPolyfill/shouldPolyfill.js +++ /dev/null @@ -1,29 +0,0 @@ -import CONST from '../../CONST'; - -/** - * Check if the locale data is as expected on the device. - * Ensures that the currency data is consistent across devices. - * @returns {Boolean} - */ -function oldCurrencyData() { - return ( - new Intl.NumberFormat(CONST.LOCALES.DEFAULT, { - style: CONST.POLYFILL_TEST.STYLE, - currency: CONST.POLYFILL_TEST.CURRENCY, - currencyDisplay: CONST.POLYFILL_TEST.FORMAT, - }).format(CONST.POLYFILL_TEST.SAMPLE_INPUT) !== CONST.POLYFILL_TEST.EXPECTED_OUTPUT - ); -} - -/** - * Check for the existence of the Intl API and ensure results will - * be as expected. - * @returns {Boolean} - */ -export default function shouldPolyfill() { - return ( - typeof Intl === 'undefined' - || !('NumberFormat' in Intl) - || oldCurrencyData() - ); -} From 8a51895848b67ad94667566e58b28137d16bebc8 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 4 May 2023 11:34:53 -0700 Subject: [PATCH 78/85] Fix lint errors from bad merge --- src/components/ReportTransaction.js | 1 - src/libs/actions/IOU.js | 12 ------------ tests/unit/IOUUtilsTest.js | 3 ++- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/ReportTransaction.js b/src/components/ReportTransaction.js index b43d89ebdce0..d3819da47a48 100644 --- a/src/components/ReportTransaction.js +++ b/src/components/ReportTransaction.js @@ -2,7 +2,6 @@ import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import styles from '../styles/styles'; -import CONST from '../CONST'; import * as IOU from '../libs/actions/IOU'; import * as ReportActions from '../libs/actions/ReportActions'; import reportActionPropTypes from '../pages/home/report/reportActionPropTypes'; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index a4ac34a559ac..6fa895e19443 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -43,18 +43,6 @@ Onyx.connect({ }, }); -let preferredLocale = CONST.LOCALES.DEFAULT; -Onyx.connect({ - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: (val) => { - if (!val) { - return; - } - - preferredLocale = val; - }, -}); - /** * Request money from another user * diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 35cb963a8eca..04fdf1dff55e 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import * as IOUUtils from '../../src/libs/IOUUtils'; import * as ReportUtils from '../../src/libs/ReportUtils'; import * as NumberUtils from '../../src/libs/NumberUtils'; +import CONST from '../../src/CONST'; import ONYXKEYS from '../../src/ONYXKEYS'; import waitForPromisesToResolve from '../utils/waitForPromisesToResolve'; import currencyList from './currencyList.json'; @@ -38,7 +39,7 @@ function deleteMoneyRequest(moneyRequestAction, isOffline = false) { moneyRequestAction.originalMessage.amount, moneyRequestAction.originalMessage.currency, moneyRequestAction.originalMessage.IOUTransactionID, - isOffline + isOffline, ); } From 28954e01d2a6039c965849465782f0efe20244d9 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 4 May 2023 11:48:26 -0700 Subject: [PATCH 79/85] Fix IOUUtilsTest --- tests/unit/IOUUtilsTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 04fdf1dff55e..fbac3e602139 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -38,8 +38,8 @@ function deleteMoneyRequest(moneyRequestAction, isOffline = false) { CONST.IOU.REPORT_ACTION_TYPE.DELETE, moneyRequestAction.originalMessage.amount, moneyRequestAction.originalMessage.currency, - moneyRequestAction.originalMessage.IOUTransactionID, isOffline, + moneyRequestAction.originalMessage.IOUTransactionID, ); } From c03cb8bd778509034b46e67a6b1d836334e2c37a Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 4 May 2023 11:52:54 -0700 Subject: [PATCH 80/85] Fix polyfill export --- src/libs/IntlPolyfill.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/libs/IntlPolyfill.js b/src/libs/IntlPolyfill.js index 019a35e7d8a2..4a65134d26e9 100644 --- a/src/libs/IntlPolyfill.js +++ b/src/libs/IntlPolyfill.js @@ -32,7 +32,7 @@ function hasOldCurrencyData() { * * @param {String} locale */ -export default function intlPolyfill(locale) { +function intlPolyfill(locale) { if (shouldPolyfillGetCanonicalLocales() && !polyfills[CONST.INTL_POLYFILLS.GET_CANONICAL_LOCALES]) { require('@formatjs/intl-getcanonicallocales/polyfill'); polyfills[CONST.INTL_POLYFILLS.GET_CANONICAL_LOCALES] = true; @@ -62,5 +62,12 @@ export default function intlPolyfill(locale) { } } -intlPolyfill(CONST.LOCALES.DEFAULT); -BaseLocaleListener.connect(intlPolyfill); +export default function init() { + intlPolyfill(CONST.LOCALES.DEFAULT); + BaseLocaleListener.connect((locale) => { + if (!locale) { + return; + } + intlPolyfill(locale); + }); +} From e68b986ed58e2df25bbb7b461873532bd056799c Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 4 May 2023 14:01:25 -0700 Subject: [PATCH 81/85] Fix Intl polyfill --- src/CONST.js | 8 -- src/libs/IntlPolyfill.js | 73 ------------------- src/libs/IntlPolyfill/index.js | 10 +++ src/libs/IntlPolyfill/index.native.js | 14 ++++ src/libs/IntlPolyfill/polyfillListFormat.js | 15 ++++ src/libs/IntlPolyfill/polyfillNumberFormat.js | 28 +++++++ src/libs/Localize/index.js | 28 ++++--- 7 files changed, 85 insertions(+), 91 deletions(-) delete mode 100644 src/libs/IntlPolyfill.js create mode 100644 src/libs/IntlPolyfill/index.js create mode 100644 src/libs/IntlPolyfill/index.native.js create mode 100644 src/libs/IntlPolyfill/polyfillListFormat.js create mode 100644 src/libs/IntlPolyfill/polyfillNumberFormat.js diff --git a/src/CONST.js b/src/CONST.js index 25353929a181..7e6946474997 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -2257,14 +2257,6 @@ const CONST = { GENERIC_ZIP_CODE_REGEX: /^(?:(?![\s-])[\w -]{0,9}[\w])?$/, - INTL_POLYFILLS: { - GET_CANONICAL_LOCALES: 'intl-getcanonicallocales', - LOCALE: 'intl-locale', - PLURAL_RULES: 'intl-pluralrules', - NUMBER_FORMAT: 'intl-numberformat', - LIST_FORMAT: 'intl-listformat', - }, - // Values for checking if polyfill is required on a platform POLYFILL_TEST: { STYLE: 'currency', diff --git a/src/libs/IntlPolyfill.js b/src/libs/IntlPolyfill.js deleted file mode 100644 index 4a65134d26e9..000000000000 --- a/src/libs/IntlPolyfill.js +++ /dev/null @@ -1,73 +0,0 @@ -import lodashGet from 'lodash/get'; -import lodashMerge from 'lodash/merge'; -import {shouldPolyfill as shouldPolyfillGetCanonicalLocales} from '@formatjs/intl-getcanonicallocales/should-polyfill'; -import {shouldPolyfill as shouldPolyfillIntlLocale} from '@formatjs/intl-locale/should-polyfill'; -import {shouldPolyfill as shouldPolyfillPluralRules} from '@formatjs/intl-pluralrules/should-polyfill'; -import {shouldPolyfill as shouldPolyfillNumberFormat} from '@formatjs/intl-numberformat/should-polyfill'; -import {shouldPolyfill as shouldPolyfillListFormat} from '@formatjs/intl-listformat/should-polyfill'; -import CONST from '../CONST'; -import BaseLocaleListener from './Localize/LocaleListener/BaseLocaleListener'; - -const polyfills = {}; - -/** - * Check if the locale data is as expected on the device. - * Ensures that the currency data is consistent across devices. - * - * @returns {Boolean} - */ -function hasOldCurrencyData() { - return ( - new Intl.NumberFormat(CONST.LOCALES.DEFAULT, { - style: CONST.POLYFILL_TEST.STYLE, - currency: CONST.POLYFILL_TEST.CURRENCY, - currencyDisplay: CONST.POLYFILL_TEST.FORMAT, - }).format(CONST.POLYFILL_TEST.SAMPLE_INPUT) !== CONST.POLYFILL_TEST.EXPECTED_OUTPUT - ); -} - -/** - * Polyfill the Intl API if the ICU version if needed - * This ensures that the currency data is consistent across platforms and browsers. - * - * @param {String} locale - */ -function intlPolyfill(locale) { - if (shouldPolyfillGetCanonicalLocales() && !polyfills[CONST.INTL_POLYFILLS.GET_CANONICAL_LOCALES]) { - require('@formatjs/intl-getcanonicallocales/polyfill'); - polyfills[CONST.INTL_POLYFILLS.GET_CANONICAL_LOCALES] = true; - } - - if (shouldPolyfillIntlLocale() && !polyfills[CONST.INTL_POLYFILLS.LOCALE]) { - require('@formatjs/intl-locale/polyfill'); - polyfills[CONST.INTL_POLYFILLS.LOCALE] = true; - } - - if (shouldPolyfillPluralRules(locale) && !lodashGet(polyfills, [CONST.INTL_POLYFILLS.PLURAL_RULES, locale], false)) { - require('@formatjs/intl-pluralrules/polyfill-force'); - require(`@formatjs/intl-pluralrules/locale-data/${locale}`); - lodashMerge(polyfills, {[CONST.INTL_POLYFILLS.PLURAL_RULES]: {locale: true}}); - } - - if ((shouldPolyfillNumberFormat(locale) || hasOldCurrencyData()) && !lodashGet(polyfills, [CONST.INTL_POLYFILLS.NUMBER_FORMAT, locale], false)) { - require('@formatjs/intl-numberformat/polyfill-force'); - require(`@formatjs/intl-numberformat/locale-data/${locale}`); - lodashMerge(polyfills, {[CONST.INTL_POLYFILLS.NUMBER_FORMAT]: {locale: true}}); - } - - if (shouldPolyfillListFormat(locale) && !lodashGet(polyfills, [CONST.INTL_POLYFILLS.LIST_FORMAT, locale], false)) { - require('@formatjs/intl-listformat/polyfill-force'); - require(`@formatjs/intl-listformat/locale-data/${locale}`); - lodashMerge(polyfills, {[CONST.INTL_POLYFILLS.LIST_FORMAT]: {locale: true}}); - } -} - -export default function init() { - intlPolyfill(CONST.LOCALES.DEFAULT); - BaseLocaleListener.connect((locale) => { - if (!locale) { - return; - } - intlPolyfill(locale); - }); -} diff --git a/src/libs/IntlPolyfill/index.js b/src/libs/IntlPolyfill/index.js new file mode 100644 index 000000000000..3925b98729a9 --- /dev/null +++ b/src/libs/IntlPolyfill/index.js @@ -0,0 +1,10 @@ +import polyfillNumberFormat from './polyfillNumberFormat'; + +/** + * Polyfill the Intl API if the ICU version is old. + * This ensures that the currency data is consistent across platforms and browsers. + */ +export default function intlPolyfill() { + // Just need to polyfill Intl.NumberFormat for web based platforms + polyfillNumberFormat(); +} diff --git a/src/libs/IntlPolyfill/index.native.js b/src/libs/IntlPolyfill/index.native.js new file mode 100644 index 000000000000..a628654fefea --- /dev/null +++ b/src/libs/IntlPolyfill/index.native.js @@ -0,0 +1,14 @@ +import polyfillNumberFormat from './polyfillNumberFormat'; +import polyfillListFormat from './polyfillListFormat'; + +/** + * Polyfill the Intl API, always performed for native devices. + */ +export default function polyfill() { + // Native devices require extra polyfills + require('@formatjs/intl-getcanonicallocales/polyfill'); + require('@formatjs/intl-locale/polyfill'); + require('@formatjs/intl-pluralrules/polyfill'); + polyfillNumberFormat(); + polyfillListFormat(); +} diff --git a/src/libs/IntlPolyfill/polyfillListFormat.js b/src/libs/IntlPolyfill/polyfillListFormat.js new file mode 100644 index 000000000000..420eaede8fd0 --- /dev/null +++ b/src/libs/IntlPolyfill/polyfillListFormat.js @@ -0,0 +1,15 @@ +export default function () { + console.log('RORY_DEBUG polyfilling ListFormat?'); + if (Intl && 'ListFormat' in Intl) { + console.log('RORY_DEBUG not polyfilling ListFormat'); + return; + } + + console.log('RORY_DEBUG ListFormat polyfill should be running like now-ish'); + + require('@formatjs/intl-listformat/polyfill-force'); + + // Load en & es Locale data + require('@formatjs/intl-listformat/locale-data/en'); + require('@formatjs/intl-listformat/locale-data/es'); +} diff --git a/src/libs/IntlPolyfill/polyfillNumberFormat.js b/src/libs/IntlPolyfill/polyfillNumberFormat.js new file mode 100644 index 000000000000..67c4c775e228 --- /dev/null +++ b/src/libs/IntlPolyfill/polyfillNumberFormat.js @@ -0,0 +1,28 @@ +import CONST from '../../CONST'; + +/** + * Check if the locale data is as expected on the device. + * Ensures that the currency data is consistent across devices. + * @returns {Boolean} + */ +function hasOldCurrencyData() { + return ( + new Intl.NumberFormat(CONST.LOCALES.DEFAULT, { + style: CONST.POLYFILL_TEST.STYLE, + currency: CONST.POLYFILL_TEST.CURRENCY, + currencyDisplay: CONST.POLYFILL_TEST.FORMAT, + }).format(CONST.POLYFILL_TEST.SAMPLE_INPUT) !== CONST.POLYFILL_TEST.EXPECTED_OUTPUT + ); +} + +export default function () { + if (Intl && 'NumberFormat' in Intl && !hasOldCurrencyData()) { + return; + } + + require('@formatjs/intl-numberformat/polyfill-force'); + + // Load en & es Locale data + require('@formatjs/intl-numberformat/locale-data/en'); + require('@formatjs/intl-numberformat/locale-data/es'); +} diff --git a/src/libs/Localize/index.js b/src/libs/Localize/index.js index 90beb3066d28..f46a795b192c 100644 --- a/src/libs/Localize/index.js +++ b/src/libs/Localize/index.js @@ -12,18 +12,23 @@ import BaseLocaleListener from './LocaleListener/BaseLocaleListener'; // Listener when an update in Onyx happens so we use the updated locale when translating/localizing items. LocaleListener.connect(); -const CONJUNCTION_LIST_FORMATS_FOR_LOCALES = _.reduce(CONST.LOCALES, (memo, locale) => { - // This is not a supported locale, so we'll use ES_ES instead - if (locale === CONST.LOCALES.ES_ES_ONFIDO) { +// Note: This has to be initialized inside a function and not at the top level of the file, because Intl is polyfilled, +// and if React Native executes this code upon import, then the polyfill will not be available yet and it will barf +let CONJUNCTION_LIST_FORMATS_FOR_LOCALES; +function init () { + CONJUNCTION_LIST_FORMATS_FOR_LOCALES = _.reduce(CONST.LOCALES, (memo, locale) => { + // This is not a supported locale, so we'll use ES_ES instead + if (locale === CONST.LOCALES.ES_ES_ONFIDO) { + // eslint-disable-next-line no-param-reassign + memo[locale] = new Intl.ListFormat(CONST.LOCALES.ES_ES, {style: 'long', type: 'conjunction'}); + return memo; + } + // eslint-disable-next-line no-param-reassign - memo[locale] = new Intl.ListFormat(CONST.LOCALES.ES_ES, {style: 'long', type: 'conjunction'}); + memo[locale] = new Intl.ListFormat(locale, {style: 'long', type: 'conjunction'}); return memo; - } - - // eslint-disable-next-line no-param-reassign - memo[locale] = new Intl.ListFormat(locale, {style: 'long', type: 'conjunction'}); - return memo; -}, {}); + }, {}); +} /** * Return translated string for given locale and phrase @@ -89,6 +94,9 @@ function translateLocal(phrase, variables) { * @return {String} */ function arrayToString(anArray) { + if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) { + init(); + } const listFormat = CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()]; return listFormat.format(anArray); } From 7a9fbbc8771321f58f036a29e929554fdb8d4de2 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 4 May 2023 14:48:36 -0700 Subject: [PATCH 82/85] Remove debug logs --- src/libs/IntlPolyfill/polyfillListFormat.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libs/IntlPolyfill/polyfillListFormat.js b/src/libs/IntlPolyfill/polyfillListFormat.js index 420eaede8fd0..21bb26cadc4b 100644 --- a/src/libs/IntlPolyfill/polyfillListFormat.js +++ b/src/libs/IntlPolyfill/polyfillListFormat.js @@ -1,12 +1,8 @@ export default function () { - console.log('RORY_DEBUG polyfilling ListFormat?'); if (Intl && 'ListFormat' in Intl) { - console.log('RORY_DEBUG not polyfilling ListFormat'); return; } - console.log('RORY_DEBUG ListFormat polyfill should be running like now-ish'); - require('@formatjs/intl-listformat/polyfill-force'); // Load en & es Locale data From 74bef7753e666c03b412c343abcc67add9c4da23 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 4 May 2023 14:54:00 -0700 Subject: [PATCH 83/85] Fix lint --- src/libs/Localize/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Localize/index.js b/src/libs/Localize/index.js index f46a795b192c..5956ab83c7ed 100644 --- a/src/libs/Localize/index.js +++ b/src/libs/Localize/index.js @@ -15,7 +15,7 @@ LocaleListener.connect(); // Note: This has to be initialized inside a function and not at the top level of the file, because Intl is polyfilled, // and if React Native executes this code upon import, then the polyfill will not be available yet and it will barf let CONJUNCTION_LIST_FORMATS_FOR_LOCALES; -function init () { +function init() { CONJUNCTION_LIST_FORMATS_FOR_LOCALES = _.reduce(CONST.LOCALES, (memo, locale) => { // This is not a supported locale, so we'll use ES_ES instead if (locale === CONST.LOCALES.ES_ES_ONFIDO) { From 78b59bd8fef97169abcac7c9647378a95686f255 Mon Sep 17 00:00:00 2001 From: Rory Abraham <47436092+roryabraham@users.noreply.github.com> Date: Thu, 4 May 2023 21:18:22 -0700 Subject: [PATCH 84/85] Update src/libs/CurrencyUtils.js Co-authored-by: Carlos Martins --- src/libs/CurrencyUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CurrencyUtils.js b/src/libs/CurrencyUtils.js index aca93e205bef..7dbd8c417bfc 100644 --- a/src/libs/CurrencyUtils.js +++ b/src/libs/CurrencyUtils.js @@ -90,7 +90,7 @@ function convertToSmallestUnit(currency, amountAsFloat) { } /** - * Takes an amount as an interget and converts it to a floating point amount. + * Takes an amount as an integer and converts it to a floating point amount. * For example, give [25, USD], return 0.25 * Given [2550, USD], return 25.50 * Given [2500, JPY], return 2500 From dbf456dff8b14fb7d06c54a3b121313ed49049d9 Mon Sep 17 00:00:00 2001 From: Rory Abraham <47436092+roryabraham@users.noreply.github.com> Date: Thu, 4 May 2023 21:18:35 -0700 Subject: [PATCH 85/85] Update src/libs/CurrencyUtils.js Co-authored-by: Carlos Martins --- src/libs/CurrencyUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CurrencyUtils.js b/src/libs/CurrencyUtils.js index 7dbd8c417bfc..01146913149a 100644 --- a/src/libs/CurrencyUtils.js +++ b/src/libs/CurrencyUtils.js @@ -95,7 +95,7 @@ function convertToSmallestUnit(currency, amountAsFloat) { * Given [2550, USD], return 25.50 * Given [2500, JPY], return 2500 * - * @note we do not currency support any currencies with more than two decimal places. + * @note we do not support any currencies with more than two decimal places. * * @param {String} currency * @param {Number} amountAsInt