From d4718f65d36e5ee1d8c77e9f655b8a69f53d7057 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 7 Feb 2024 11:59:27 -0500 Subject: [PATCH 01/98] use lastUpdateID and previousUpdateID to apply updates --- .../PushNotification/NotificationType.ts | 2 ++ ...bscribeToReportCommentPushNotifications.ts | 32 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/libs/Notification/PushNotification/NotificationType.ts b/src/libs/Notification/PushNotification/NotificationType.ts index d6ec246eddf7..40778f38c0d4 100644 --- a/src/libs/Notification/PushNotification/NotificationType.ts +++ b/src/libs/Notification/PushNotification/NotificationType.ts @@ -18,6 +18,8 @@ type ReportCommentNotificationData = { shouldScrollToLastUnread?: boolean; roomName?: string; onyxData?: OnyxServerUpdate[]; + lastUpdateID?: number; + previousUpdateID?: number; }; /** diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 070f83e19e0f..6ac7c31fdd2a 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -1,15 +1,18 @@ import Onyx from 'react-native-onyx'; +import * as OnyxUpdates from '@libs/actions/OnyxUpdates'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import * as SequentialQueue from '@libs/Network/SequentialQueue'; import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils'; import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; import {doesReportBelongToWorkspace, getReport} from '@libs/ReportUtils'; import Visibility from '@libs/Visibility'; import * as Modal from '@userActions/Modal'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import backgroundRefresh from './backgroundRefresh'; import PushNotification from './index'; let lastVisitedPath: string | undefined; @@ -27,10 +30,31 @@ Onyx.connect({ * Setup reportComment push notification callbacks. */ export default function subscribeToReportCommentPushNotifications() { - PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData}) => { + PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData, lastUpdateID, previousUpdateID}) => { Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID}); - Onyx.update(onyxData ?? []); - backgroundRefresh(); + + if (onyxData && lastUpdateID && previousUpdateID) { + const updates: OnyxUpdatesFromServer = { + type: CONST.ONYX_UPDATE_TYPES.PUSHER, + lastUpdateID, + previousUpdateID, + updates: [ + { + eventType: 'eventType', + data: onyxData, + }, + ], + }; + + if (!OnyxUpdates.doesClientNeedToBeUpdated(previousUpdateID)) { + OnyxUpdates.apply(updates); + return; + } + + // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. + SequentialQueue.pause(); + OnyxUpdates.saveUpdateInformation(updates); + } }); // Open correct report when push notification is clicked From 151b9ed364c9d27540412ddd43bb16f818f2d7fe Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 7 Feb 2024 12:08:08 -0500 Subject: [PATCH 02/98] log when data is present or missing --- .../subscribeToReportCommentPushNotifications.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 6ac7c31fdd2a..4aca364d48c8 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -34,6 +34,8 @@ export default function subscribeToReportCommentPushNotifications() { Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID}); if (onyxData && lastUpdateID && previousUpdateID) { + Log.info('[PushNotification] reliable onyx update received', false, {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); + const updates: OnyxUpdatesFromServer = { type: CONST.ONYX_UPDATE_TYPES.PUSHER, lastUpdateID, @@ -54,6 +56,8 @@ export default function subscribeToReportCommentPushNotifications() { // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. SequentialQueue.pause(); OnyxUpdates.saveUpdateInformation(updates); + } else { + Log.hmmm("[PushNotification] Didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); } }); From 9e7951afa5d4a588a357e2ade79e4113c165f58a Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Wed, 7 Feb 2024 15:01:10 -0500 Subject: [PATCH 03/98] DRY up reliable update code --- .../subscribeToReportCommentPushNotifications.ts | 11 +---------- src/libs/actions/OnyxUpdates.ts | 15 ++++++++++++++- src/libs/actions/User.ts | 11 ++--------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 4aca364d48c8..4586671d71ee 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -2,7 +2,6 @@ import Onyx from 'react-native-onyx'; import * as OnyxUpdates from '@libs/actions/OnyxUpdates'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; -import * as SequentialQueue from '@libs/Network/SequentialQueue'; import getPolicyMemberAccountIDs from '@libs/PolicyMembersUtils'; import {extractPolicyIDFromPath} from '@libs/PolicyUtils'; import {doesReportBelongToWorkspace, getReport} from '@libs/ReportUtils'; @@ -47,15 +46,7 @@ export default function subscribeToReportCommentPushNotifications() { }, ], }; - - if (!OnyxUpdates.doesClientNeedToBeUpdated(previousUpdateID)) { - OnyxUpdates.apply(updates); - return; - } - - // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. - SequentialQueue.pause(); - OnyxUpdates.saveUpdateInformation(updates); + OnyxUpdates.applyOnyxUpdatesReliably(updates); } else { Log.hmmm("[PushNotification] Didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); } diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index cfb4735f0638..bcf7a9373ae0 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -2,6 +2,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {Merge} from 'type-fest'; import Log from '@libs/Log'; +import * as SequentialQueue from '@libs/Network/SequentialQueue'; import PusherUtils from '@libs/PusherUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -141,5 +142,17 @@ function doesClientNeedToBeUpdated(previousUpdateID = 0): boolean { return lastUpdateIDAppliedToClient < previousUpdateID; } +function applyOnyxUpdatesReliably(updates: OnyxUpdatesFromServer) { + const previousUpdateID = Number(updates.previousUpdateID) || 0; + if (!doesClientNeedToBeUpdated(previousUpdateID)) { + apply(updates); + return; + } + + // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. + SequentialQueue.pause(); + saveUpdateInformation(updates); +} + // eslint-disable-next-line import/prefer-default-export -export {saveUpdateInformation, doesClientNeedToBeUpdated, apply}; +export {saveUpdateInformation, doesClientNeedToBeUpdated, apply, applyOnyxUpdatesReliably}; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index a8ef33a92e38..064453bca181 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -39,7 +39,7 @@ import type ReportAction from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import * as Link from './Link'; -import * as OnyxUpdates from './OnyxUpdates'; +import {applyOnyxUpdatesReliably} from './OnyxUpdates'; import * as PersonalDetails from './PersonalDetails'; import * as Report from './Report'; import * as Session from './Session'; @@ -503,14 +503,7 @@ function subscribeToUserEvents() { updates: pushJSON.updates ?? [], previousUpdateID: Number(pushJSON.previousUpdateID || 0), }; - if (!OnyxUpdates.doesClientNeedToBeUpdated(Number(pushJSON.previousUpdateID || 0))) { - OnyxUpdates.apply(updates); - return; - } - - // If we reached this point, we need to pause the queue while we prepare to fetch older OnyxUpdates. - SequentialQueue.pause(); - OnyxUpdates.saveUpdateInformation(updates); + applyOnyxUpdatesReliably(updates); }); // Handles Onyx updates coming from Pusher through the mega multipleEvents. From 02ad87af5c391b2fd81f53bb6272bcf840a56011 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Fri, 9 Feb 2024 18:21:15 -0500 Subject: [PATCH 04/98] re-add background refresh --- .../subscribeToReportCommentPushNotifications.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 8c31fddb0db8..8fcf281b1443 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -12,6 +12,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import backgroundRefresh from './backgroundRefresh'; import PushNotification from './index'; let lastVisitedPath: string | undefined; @@ -50,6 +51,8 @@ export default function subscribeToReportCommentPushNotifications() { } else { Log.hmmm("[PushNotification] Didn't apply onyx updates because some data is missing", {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); } + + backgroundRefresh(); }); // Open correct report when push notification is clicked From 33f9c411246dddc1720001fe099eaf3213535ba2 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 12 Feb 2024 16:46:10 -0500 Subject: [PATCH 05/98] use new airship update type --- src/CONST.ts | 1 + .../subscribeToReportCommentPushNotifications.ts | 2 +- src/libs/actions/OnyxUpdateManager.ts | 3 ++- src/libs/actions/OnyxUpdates.ts | 2 +- src/types/onyx/OnyxUpdatesFromServer.ts | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 79895d20aa57..4f93340de379 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3025,6 +3025,7 @@ const CONST = { ONYX_UPDATE_TYPES: { HTTPS: 'https', PUSHER: 'pusher', + AIRSHIP: 'airship', }, EVENTS: { SCROLLING: 'scrolling', diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index 8fcf281b1443..7f86d3ddb9ac 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -37,7 +37,7 @@ export default function subscribeToReportCommentPushNotifications() { Log.info('[PushNotification] reliable onyx update received', false, {lastUpdateID, previousUpdateID, onyxDataCount: onyxData?.length ?? 0}); const updates: OnyxUpdatesFromServer = { - type: CONST.ONYX_UPDATE_TYPES.PUSHER, + type: CONST.ONYX_UPDATE_TYPES.AIRSHIP, lastUpdateID, previousUpdateID, updates: [ diff --git a/src/libs/actions/OnyxUpdateManager.ts b/src/libs/actions/OnyxUpdateManager.ts index ab0dea960b27..b4554f9461ce 100644 --- a/src/libs/actions/OnyxUpdateManager.ts +++ b/src/libs/actions/OnyxUpdateManager.ts @@ -41,7 +41,8 @@ export default () => { if ( !(typeof value === 'object' && !!value) || !('type' in value) || - (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) && !(value.type === CONST.ONYX_UPDATE_TYPES.PUSHER && value.updates)) + (!(value.type === CONST.ONYX_UPDATE_TYPES.HTTPS && value.request && value.response) && + !((value.type === CONST.ONYX_UPDATE_TYPES.PUSHER || value.type === CONST.ONYX_UPDATE_TYPES.AIRSHIP) && value.updates)) ) { console.debug('[OnyxUpdateManager] Invalid format found for updates, cleaning and unpausing the queue'); Onyx.set(ONYXKEYS.ONYX_UPDATES_FROM_SERVER, null); diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index bcf7a9373ae0..ab26ad330b6f 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -108,7 +108,7 @@ function apply({lastUpdateID, type, request, response, updates}: OnyxUpdatesFrom if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response) { return applyHTTPSOnyxUpdates(request, response); } - if (type === CONST.ONYX_UPDATE_TYPES.PUSHER && updates) { + if ((type === CONST.ONYX_UPDATE_TYPES.PUSHER || type === CONST.ONYX_UPDATE_TYPES.AIRSHIP) && updates) { return applyPusherOnyxUpdates(updates); } } diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts index f6104ed470a0..3c6933da19ba 100644 --- a/src/types/onyx/OnyxUpdatesFromServer.ts +++ b/src/types/onyx/OnyxUpdatesFromServer.ts @@ -13,7 +13,7 @@ type OnyxUpdateEvent = { }; type OnyxUpdatesFromServer = { - type: 'https' | 'pusher'; + type: 'https' | 'pusher' | 'airship'; lastUpdateID: number | string; previousUpdateID: number | string; request?: Request; From 1a71d782b93aa881152cf225467f8835bb396791 Mon Sep 17 00:00:00 2001 From: ren-jones <153645623+ren-jones@users.noreply.github.com> Date: Fri, 16 Feb 2024 08:28:00 -0600 Subject: [PATCH 06/98] Create Change-or-add-email-address.md New settings help article --- .../Change-or-add-email-address.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/articles/expensify-classic/settings/account-settings/Change-or-add-email-address.md diff --git a/docs/articles/expensify-classic/settings/account-settings/Change-or-add-email-address.md b/docs/articles/expensify-classic/settings/account-settings/Change-or-add-email-address.md new file mode 100644 index 000000000000..754b9a7f9ac0 --- /dev/null +++ b/docs/articles/expensify-classic/settings/account-settings/Change-or-add-email-address.md @@ -0,0 +1,24 @@ +--- +title: Change or add email address +description: Update your Expensify email address or add a secondary email +--- +
+ +The primary email address on your Expensify account is the email that receives email updates and notifications for your account. You can add a secondary email address in order to +- Change your primary email to a new one. +- Connect your personal email address as a secondary login if your primary email address is one from your employer. This allows you to always have access to your Expensify account, even if your employer changes. + +{% include info.html %} +Before you can remove a primary email address, you must add a new one to your Expensify account and make it the primary using the steps below. Email addresses must be added as a secondary login before they can be made the primary. +{% include end-info.html %} + +*Note: This process is currently not available from the mobile app and must be completed from the Expensify website.* + +1. Hover over Settings, then click **Account**. +2. Under the Account Details tab, scroll down to the Secondary Logins section and click **Add Secondary Login**. +3. Enter the email address or phone number you wish to use as a secondary login. For phone numbers, be sure to include the international code, if applicable. +4. Find the email or text message from Expensify containing the Magic Code and enter it into the field. +5. To make the new email address the primary address for your account, click **Make Primary**. + +You can keep both logins, or you can click **Remove** next to the old email address to delete it from your account. +
From fd19cfa6e09f750c31b17c3b9c5e9bf6130ce809 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 20 Feb 2024 16:04:02 +0100 Subject: [PATCH 07/98] Pass amount when paying money request --- src/libs/actions/IOU.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 94f08e5a682c..f4bd88fbba1f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3292,10 +3292,11 @@ function getSendMoneyParams( }; } -function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, recipient: Participant, paymentMethodType: PaymentMethodType): PayMoneyRequestData { +function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, recipient: Participant, paymentMethodType: PaymentMethodType): { failureData: OnyxUpdate[]; successData: OnyxUpdate[]; params: { amount: number; reportActionID: string; iouReportID: string; paymentMethodType: "Expensify" | "ACH" | "Elsewhere" | "delete" | "create" | "cancel" | "pay" | "approve" | "split" | "decline" | "ach" | "instant"; chatReportID: string }; optimisticData: OnyxUpdate[] } { + const amount = -(iouReport.total ?? 0); const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.PAY, - -(iouReport.total ?? 0), + amount, iouReport.currency ?? '', '', [recipient], @@ -3442,6 +3443,7 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT chatReportID: chatReport.reportID, reportActionID: optimisticIOUReportAction.reportActionID, paymentMethodType, + amount, }, optimisticData, successData, From 57aec6689eefa20c16fc4e3e799207b02d1120a7 Mon Sep 17 00:00:00 2001 From: Alberto Date: Tue, 20 Feb 2024 16:05:25 +0100 Subject: [PATCH 08/98] Add to params --- src/libs/API/parameters/PayMoneyRequestParams.ts | 1 + src/libs/actions/IOU.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/API/parameters/PayMoneyRequestParams.ts b/src/libs/API/parameters/PayMoneyRequestParams.ts index edf05b6ce528..4a769f057e10 100644 --- a/src/libs/API/parameters/PayMoneyRequestParams.ts +++ b/src/libs/API/parameters/PayMoneyRequestParams.ts @@ -5,6 +5,7 @@ type PayMoneyRequestParams = { chatReportID: string; reportActionID: string; paymentMethodType: PaymentMethodType; + amount?: number; }; export default PayMoneyRequestParams; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index f4bd88fbba1f..14086d681796 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3292,7 +3292,7 @@ function getSendMoneyParams( }; } -function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, recipient: Participant, paymentMethodType: PaymentMethodType): { failureData: OnyxUpdate[]; successData: OnyxUpdate[]; params: { amount: number; reportActionID: string; iouReportID: string; paymentMethodType: "Expensify" | "ACH" | "Elsewhere" | "delete" | "create" | "cancel" | "pay" | "approve" | "split" | "decline" | "ach" | "instant"; chatReportID: string }; optimisticData: OnyxUpdate[] } { +function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, recipient: Participant, paymentMethodType: PaymentMethodType): PayMoneyRequestData { const amount = -(iouReport.total ?? 0); const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.PAY, From 8ac8f5a36e4886e6e55d4a96d671ee389376c8c1 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 23 Feb 2024 15:51:09 +0700 Subject: [PATCH 09/98] Add current to default miliage rate --- src/CONST.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 008002a71078..83dbd0c9fce8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6,6 +6,12 @@ import * as KeyCommand from 'react-native-key-command'; import * as Url from './libs/Url'; import SCREENS from './SCREENS'; +type RateAndUnit = { + unit: string, + rate: number +} +type CurrencyDefaultMileageRate = Record + // Creating a default array and object this way because objects ({}) and arrays ([]) are not stable types. // Freezing the array ensures that it cannot be unintentionally modified. const EMPTY_ARRAY = Object.freeze([]); @@ -3305,6 +3311,16 @@ const CONST = { ADDRESS: 3, }, }, + CURRENCY_TO_DEFAULT_MILEAGE_RATE: JSON.parse(`{ + "EUR": { + "unit": "kilometer", + "rate": 1 + }, + "USD": { + "unit": "mile", + "rate": 2 + } + }`) as CurrencyDefaultMileageRate, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; From 18197585cc81ed1757423452ac4ceeabc193873c Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 23 Feb 2024 16:38:35 +0700 Subject: [PATCH 10/98] fix lint --- src/CONST.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 83dbd0c9fce8..aebf84b68027 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -7,10 +7,10 @@ import * as Url from './libs/Url'; import SCREENS from './SCREENS'; type RateAndUnit = { - unit: string, - rate: number -} -type CurrencyDefaultMileageRate = Record + unit: string; + rate: number; +}; +type CurrencyDefaultMileageRate = Record; // Creating a default array and object this way because objects ({}) and arrays ([]) are not stable types. // Freezing the array ensures that it cannot be unintentionally modified. @@ -3320,7 +3320,7 @@ const CONST = { "unit": "mile", "rate": 2 } - }`) as CurrencyDefaultMileageRate, + }`) as CurrencyDefaultMileageRate, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; From b5d4175e189240d6305e55ce84e76a2f20343053 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Fri, 23 Feb 2024 11:44:25 +0100 Subject: [PATCH 11/98] start migrating the files --- tests/unit/{LocalizeTests.js => LocalizeTests.ts} | 0 tests/unit/{TranslateTest.js => TranslateTest.ts} | 4 +++- ...ployTest.js => createOrUpdateStagingDeployTest.ts} | 11 +++++++---- ...anceParametersTest.js => enhanceParametersTest.ts} | 1 + ...sionUpdaterTest.js => nativeVersionUpdaterTest.ts} | 0 5 files changed, 11 insertions(+), 5 deletions(-) rename tests/unit/{LocalizeTests.js => LocalizeTests.ts} (100%) rename tests/unit/{TranslateTest.js => TranslateTest.ts} (96%) rename tests/unit/{createOrUpdateStagingDeployTest.js => createOrUpdateStagingDeployTest.ts} (98%) rename tests/unit/{enhanceParametersTest.js => enhanceParametersTest.ts} (96%) rename tests/unit/{nativeVersionUpdaterTest.js => nativeVersionUpdaterTest.ts} (100%) diff --git a/tests/unit/LocalizeTests.js b/tests/unit/LocalizeTests.ts similarity index 100% rename from tests/unit/LocalizeTests.js rename to tests/unit/LocalizeTests.ts diff --git a/tests/unit/TranslateTest.js b/tests/unit/TranslateTest.ts similarity index 96% rename from tests/unit/TranslateTest.js rename to tests/unit/TranslateTest.ts index d23fa52fc798..fd3c4f474e88 100644 --- a/tests/unit/TranslateTest.js +++ b/tests/unit/TranslateTest.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import {AnnotationError} from '@actions/core'; import _ from 'underscore'; +import * as translations from '@src/languages/translations'; import CONFIG from '../../src/CONFIG'; import CONST from '../../src/CONST'; -import * as translations from '../../src/languages/translations'; +// import * as translations from '../../src/languages/translations'; import * as Localize from '../../src/libs/Localize'; const originalTranslations = _.clone(translations); diff --git a/tests/unit/createOrUpdateStagingDeployTest.js b/tests/unit/createOrUpdateStagingDeployTest.ts similarity index 98% rename from tests/unit/createOrUpdateStagingDeployTest.js rename to tests/unit/createOrUpdateStagingDeployTest.ts index 75600f0d4fc8..28daf8c1674d 100644 --- a/tests/unit/createOrUpdateStagingDeployTest.js +++ b/tests/unit/createOrUpdateStagingDeployTest.ts @@ -1,7 +1,8 @@ /** * @jest-environment node */ -import * as core from '@actions/core'; + +/* eslint-disable @typescript-eslint/naming-convention */ import * as fns from 'date-fns'; import {vol} from 'memfs'; import path from 'path'; @@ -19,7 +20,9 @@ const mockGetPullRequestsMergedBetween = jest.fn(); beforeAll(() => { // Mock core module - core.getInput = mockGetInput; + jest.mock('@actions/core', () => ({ + getInput: mockGetInput, + })); // Mock octokit module const moctokit = { @@ -295,7 +298,7 @@ describe('createOrUpdateStagingDeployCash', () => { owner: CONST.GITHUB_OWNER, repo: CONST.APP_REPO, issue_number: openStagingDeployCashBefore.number, - // eslint-disable-next-line max-len + // eslint-disable-next-line max-len, @typescript-eslint/naming-convention html_url: `https://github.com/Expensify/App/issues/${openStagingDeployCashBefore.number}`, // eslint-disable-next-line max-len body: @@ -371,7 +374,7 @@ describe('createOrUpdateStagingDeployCash', () => { owner: CONST.GITHUB_OWNER, repo: CONST.APP_REPO, issue_number: openStagingDeployCashBefore.number, - // eslint-disable-next-line max-len + // eslint-disable-next-line max-len, @typescript-eslint/naming-convention html_url: `https://github.com/Expensify/App/issues/${openStagingDeployCashBefore.number}`, // eslint-disable-next-line max-len body: diff --git a/tests/unit/enhanceParametersTest.js b/tests/unit/enhanceParametersTest.ts similarity index 96% rename from tests/unit/enhanceParametersTest.js rename to tests/unit/enhanceParametersTest.ts index 6829732f1633..bf2756767401 100644 --- a/tests/unit/enhanceParametersTest.js +++ b/tests/unit/enhanceParametersTest.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ import Onyx from 'react-native-onyx'; import CONFIG from '../../src/CONFIG'; import enhanceParameters from '../../src/libs/Network/enhanceParameters'; diff --git a/tests/unit/nativeVersionUpdaterTest.js b/tests/unit/nativeVersionUpdaterTest.ts similarity index 100% rename from tests/unit/nativeVersionUpdaterTest.js rename to tests/unit/nativeVersionUpdaterTest.ts From b832c2c6b28bb6670a5f9a70c98440df478f6183 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 26 Feb 2024 00:08:49 +0700 Subject: [PATCH 12/98] feature: enable p2p/splits in App --- src/CONST.ts | 2 ++ src/ONYXKEYS.ts | 4 +++ .../MoneyRequestConfirmationList.js | 7 ++--- ...oraryForRefactorRequestConfirmationList.js | 7 ++--- src/libs/DistanceRequestUtils.ts | 25 ++++++++++++++++- src/libs/Permissions.ts | 5 ++++ src/libs/actions/IOU.ts | 27 ++++++++++++++++--- src/pages/iou/request/IOURequestStartPage.js | 4 +-- ...yForRefactorRequestParticipantsSelector.js | 12 ++++----- .../MoneyRequestParticipantsSelector.js | 9 ++++--- src/types/onyx/LastSelectedDistanceRates.ts | 5 ++++ src/types/onyx/index.ts | 2 ++ 12 files changed, 86 insertions(+), 23 deletions(-) create mode 100644 src/types/onyx/LastSelectedDistanceRates.ts diff --git a/src/CONST.ts b/src/CONST.ts index 008002a71078..a3df727251f0 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -307,6 +307,7 @@ const CONST = { BETA_COMMENT_LINKING: 'commentLinking', VIOLATIONS: 'violations', REPORT_FIELDS: 'reportFields', + P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests', }, BUTTON_STATES: { DEFAULT: 'default', @@ -1392,6 +1393,7 @@ const CONST = { MILEAGE_IRS_RATE: 0.655, DEFAULT_RATE: 'Default Rate', RATE_DECIMALS: 3, + FAKE_P2P_ID: '__fake_p2p_id__', }, TERMS: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ee8bceeab44a..aad05bee7c2c 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -126,6 +126,9 @@ const ONYXKEYS = { /** This NVP contains the choice that the user made on the engagement modal */ NVP_INTRO_SELECTED: 'introSelected', + /** The NVP with the last distance rate used per policy */ + NVP_LAST_SELECTED_DISTANCE_RATES: 'lastSelectedDistanceRates', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -511,6 +514,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; [ONYXKEYS.NVP_HAS_DISMISSED_IDLE_PANEL]: boolean; [ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected; + [ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates; [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean; [ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData; [ONYXKEYS.IS_PLAID_DISABLED]: boolean; diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 0de601bc9f61..843ae41905e1 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -217,7 +217,7 @@ function MoneyRequestConfirmationList(props) { const {onSendMoney, onConfirm, onSelectParticipant} = props; const {translate, toLocaleDigit} = useLocalize(); const transaction = props.transaction; - const {canUseViolations} = usePermissions(); + const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(); const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST; const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT; @@ -227,6 +227,7 @@ function MoneyRequestConfirmationList(props) { const {unit, rate, currency} = props.mileageRate; const distance = lodashGet(transaction, 'routes.route0.distance', 0); + const displayDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0; // A flag for showing the categories field @@ -720,12 +721,12 @@ function MoneyRequestConfirmationList(props) { {props.isDistanceRequest && ( Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} - disabled={didConfirm || !isTypeRequest} + disabled={didConfirm || canUseP2PDistanceRequests || !isTypeRequest} interactive={!props.isReadOnly} /> )} diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 3939e847707d..b7ee85d2513d 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -243,7 +243,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); - const {canUseViolations} = usePermissions(); + const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; @@ -251,6 +251,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const {unit, rate, currency} = mileageRate; const distance = lodashGet(transaction, 'routes.route0.distance', 0); + const displayDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; // A flag for showing the categories field @@ -680,12 +681,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm || !isTypeRequest} + disabled={didConfirm || canUseP2PDistanceRequests || !isTypeRequest} interactive={!isReadOnly} /> ), diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index a42cb6a8f756..0a02cd6796ca 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -7,6 +7,7 @@ import * as CurrencyUtils from './CurrencyUtils'; import * as PolicyUtils from './PolicyUtils'; type DefaultMileageRate = { + rateID?: string; rate?: number; currency?: string; unit: Unit; @@ -38,6 +39,7 @@ function getDefaultMileageRate(policy: OnyxEntry): DefaultMileageRate | } return { + rateID: distanceRate.customUnitRateID, rate: distanceRate.rate, currency: distanceRate.currency, unit: distanceUnit.attributes.unit, @@ -76,6 +78,27 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string return convertedDistance.toFixed(2); } +/** + * @param hasRoute Whether the route exists for the distance request + * @param distanceInMeters Distance traveled + * @param unit Unit that should be used to display the distance + * @param rate Expensable amount allowed per unit + * @param translate Translate function + * @returns A string that describes the distance traveled + */ +function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, rate: number, translate: LocaleContextProps['translate']): string { + if (!hasRoute || !rate) { + return translate('iou.routePending'); + } + + const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); + const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); + const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); + const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; + + return `${distanceInUnits} ${unitString}`; +} + /** * @param hasRoute Whether the route exists for the distance request * @param distanceInMeters Distance traveled @@ -124,4 +147,4 @@ function getDistanceRequestAmount(distance: number, unit: Unit, rate: number): n return Math.round(roundedDistance * rate); } -export default {getDefaultMileageRate, getDistanceMerchant, getDistanceRequestAmount}; +export default {getDefaultMileageRate, getDistanceForDisplay, getDistanceMerchant, getDistanceRequestAmount}; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index ce5e0e674c59..8eaf33605e8f 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -26,6 +26,10 @@ function canUseViolations(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas); } +function canUseP2PDistanceRequests(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.P2P_DISTANCE_REQUESTS) || canUseAllBetas(betas); +} + /** * Link previews are temporarily disabled. */ @@ -40,4 +44,5 @@ export default { canUseLinkPreviews, canUseViolations, canUseReportFields, + canUseP2PDistanceRequests, }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index f39728e7d31c..8d708e7f4d42 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -26,6 +26,7 @@ import type { import {WRITE_COMMANDS} from '@libs/API/types'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import * as IOUUtils from '@libs/IOUUtils'; @@ -218,12 +219,27 @@ Onyx.connect({ }, }); +let lastSelectedDistanceRates: OnyxEntry = {}; +Onyx.connect({ + key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + callback: (value) => { + lastSelectedDistanceRates = value; + }, +}); + /** * Initialize money request info - * @param reportID to attach the transaction to + * @param report to attach the transaction to + * @param policy + * @param isFromGlobalCreate * @param iouRequestType one of manual/scan/distance */ -function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) { +function initMoneyRequest( + report: OnyxEntry, + policy: OnyxEntry, + isFromGlobalCreate: boolean, + iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL, +) { // Generate a brand new transactionID const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID; // Disabling this line since currentDate can be an empty string @@ -237,6 +253,11 @@ function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequ waypoint0: {}, waypoint1: {}, }; + let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; + if (report?.isPolicyExpenseChat) { + customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? '']?.customUnitRateID ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.rateID ?? ''; + } + comment.customUnit = {customUnitRateID}; } // Store the transaction in Onyx and mark it as not saved so it can be cleaned up later @@ -247,7 +268,7 @@ function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequ created, currency: currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD, iouRequestType, - reportID, + reportID: report?.reportID ?? '', transactionID: newTransactionID, isFromGlobalCreate, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 05e3d7c96311..d1b416f79fdc 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -98,8 +98,8 @@ function IOURequestStartPage({ if (transaction.reportID === reportID) { return; } - IOU.initMoneyRequest(reportID, isFromGlobalCreate, transactionRequestType.current); - }, [transaction, reportID, iouType, isFromGlobalCreate]); + IOU.initMoneyRequest(report, policy, isFromGlobalCreate, transactionRequestType.current); + }, [transaction, reportID, report, policy, iouType, isFromGlobalCreate]); const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 238b66c0e727..b4b95107dc52 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -14,6 +14,7 @@ import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -90,6 +91,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); + const {canUseP2PDistanceRequests} = usePermissions(); const offlineMessage = isOffline ? [`${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}`, {isTranslated: true}] : ''; @@ -120,18 +122,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // sees the option to request money from their admin on their own Workspace Chat. iouType === CONST.IOU.TYPE.REQUEST, - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, {}, [], false, {}, [], - - // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. - // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, ); @@ -256,7 +254,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; - const isAllowedToSplit = iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE; + const isAllowedToSplit = canUseP2PDistanceRequests || iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE; const handleConfirmSelection = useCallback(() => { if (shouldShowSplitBillErrorMessage) { diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 3fde970327d7..ec45fafd5954 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -14,6 +14,7 @@ import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/UserListItem'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import useSearchTermAndSearch from '@hooks/useSearchTermAndSearch'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -94,6 +95,7 @@ function MoneyRequestParticipantsSelector({ const referralContentType = iouType === CONST.IOU.TYPE.SEND ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST; const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); + const {canUseP2PDistanceRequests} = usePermissions(); const maxParticipantsReached = participants.length === CONST.REPORT.MAXIMUM_PARTICIPANTS; const setSearchTermAndSearchInServer = useSearchTermAndSearch(setSearchTerm, maxParticipantsReached); @@ -113,8 +115,7 @@ function MoneyRequestParticipantsSelector({ // sees the option to request money from their admin on their own Workspace Chat. iouType === CONST.IOU.TYPE.REQUEST, - // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - !isDistanceRequest, + canUseP2PDistanceRequests || !isDistanceRequest, false, {}, [], @@ -123,7 +124,7 @@ function MoneyRequestParticipantsSelector({ [], // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - !isDistanceRequest, + canUseP2PDistanceRequests || !isDistanceRequest, true, ); return { @@ -272,7 +273,7 @@ function MoneyRequestParticipantsSelector({ // the app from crashing on native when you try to do this, we'll going to show error message if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); const shouldShowSplitBillErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; - const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; + const isAllowedToSplit = (canUseP2PDistanceRequests || !isDistanceRequest) && iouType !== CONST.IOU.TYPE.SEND; const handleConfirmSelection = useCallback(() => { if (shouldShowSplitBillErrorMessage) { diff --git a/src/types/onyx/LastSelectedDistanceRates.ts b/src/types/onyx/LastSelectedDistanceRates.ts new file mode 100644 index 000000000000..4ebf39e43bcb --- /dev/null +++ b/src/types/onyx/LastSelectedDistanceRates.ts @@ -0,0 +1,5 @@ +import type {Rate} from './Policy'; + +type LastSelectedDistanceRates = Record; + +export default LastSelectedDistanceRates; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index c24e0871e0ed..e654ba30f53d 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -19,6 +19,7 @@ import type IntroSelected from './IntroSelected'; import type InvitedEmailsToAccountIDs from './InvitedEmailsToAccountIDs'; import type IOU from './IOU'; import type LastPaymentMethod from './LastPaymentMethod'; +import type LastSelectedDistanceRates from './LastSelectedDistanceRates'; import type Locale from './Locale'; import type {LoginList} from './Login'; import type Login from './Login'; @@ -148,6 +149,7 @@ export type { PolicyReportFields, RecentlyUsedReportFields, LastPaymentMethod, + LastSelectedDistanceRates, InvitedEmailsToAccountIDs, Log, }; From 8da91dcb079bfafd3fe0b28e4367e3d30b9efe63 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 26 Feb 2024 00:22:06 +0700 Subject: [PATCH 13/98] fix lint --- src/components/MoneyRequestConfirmationList.js | 4 ++-- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 4 ++-- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 2 +- .../MoneyRequestParticipantsSelector.js | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 7a4eedcd281b..e9291c8c8457 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -227,7 +227,6 @@ function MoneyRequestConfirmationList(props) { const {unit, rate, currency} = props.mileageRate; const distance = lodashGet(transaction, 'routes.route0.distance', 0); - const displayDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const shouldCalculateDistanceAmount = props.isDistanceRequest && props.iouAmount === 0; // A flag for showing the categories field @@ -262,6 +261,7 @@ function MoneyRequestConfirmationList(props) { props.isDistanceRequest ? currency : props.iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); + const formattedDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const defaultTaxKey = props.policyTaxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${props.policyTaxRates.taxes[defaultTaxKey].name} (${props.policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; @@ -721,7 +721,7 @@ function MoneyRequestConfirmationList(props) { {props.isDistanceRequest && ( Date: Mon, 26 Feb 2024 00:37:30 +0700 Subject: [PATCH 14/98] fix: waypoints are not saved --- src/libs/actions/IOU.ts | 15 +++++---------- src/pages/iou/request/IOURequestStartPage.js | 4 ++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 4dc745b0e3b3..0e8d3f392feb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -231,17 +231,11 @@ Onyx.connect({ /** * Initialize money request info - * @param report to attach the transaction to - * @param policy + * @param reportID to attach the transaction to * @param isFromGlobalCreate * @param iouRequestType one of manual/scan/distance */ -function initMoneyRequest( - report: OnyxEntry, - policy: OnyxEntry, - isFromGlobalCreate: boolean, - iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL, -) { +function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) { // Generate a brand new transactionID const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID; // Disabling this line since currentDate can be an empty string @@ -255,9 +249,10 @@ function initMoneyRequest( waypoint0: {}, waypoint1: {}, }; + const report = allReports?.[reportID]; let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; if (report?.isPolicyExpenseChat) { - customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? '']?.customUnitRateID ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.rateID ?? ''; + customUnitRateID = lastSelectedDistanceRates?.[report?.policyID ?? '']?.customUnitRateID ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.rateID ?? ''; } comment.customUnit = {customUnitRateID}; } @@ -270,7 +265,7 @@ function initMoneyRequest( created, currency: currentUserPersonalDetails.localCurrencyCode ?? CONST.CURRENCY.USD, iouRequestType, - reportID: report?.reportID ?? '', + reportID, transactionID: newTransactionID, isFromGlobalCreate, merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index d1b416f79fdc..05e3d7c96311 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -98,8 +98,8 @@ function IOURequestStartPage({ if (transaction.reportID === reportID) { return; } - IOU.initMoneyRequest(report, policy, isFromGlobalCreate, transactionRequestType.current); - }, [transaction, reportID, report, policy, iouType, isFromGlobalCreate]); + IOU.initMoneyRequest(reportID, isFromGlobalCreate, transactionRequestType.current); + }, [transaction, reportID, iouType, isFromGlobalCreate]); const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); From 11d67d190e78ed3bc9820eb25947d5fff174069e Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 26 Feb 2024 00:45:18 +0700 Subject: [PATCH 15/98] fix typecheck --- src/libs/actions/IOU.ts | 5 +++-- src/pages/iou/request/IOURequestStartPage.js | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0e8d3f392feb..92454226519f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -232,10 +232,11 @@ Onyx.connect({ /** * Initialize money request info * @param reportID to attach the transaction to + * @param policy * @param isFromGlobalCreate * @param iouRequestType one of manual/scan/distance */ -function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) { +function initMoneyRequest(reportID: string, policy: OnyxEntry, isFromGlobalCreate: boolean, iouRequestType: IOURequestType = CONST.IOU.REQUEST_TYPE.MANUAL) { // Generate a brand new transactionID const newTransactionID = CONST.IOU.OPTIMISTIC_TRANSACTION_ID; // Disabling this line since currentDate can be an empty string @@ -252,7 +253,7 @@ function initMoneyRequest(reportID: string, isFromGlobalCreate: boolean, iouRequ const report = allReports?.[reportID]; let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; if (report?.isPolicyExpenseChat) { - customUnitRateID = lastSelectedDistanceRates?.[report?.policyID ?? '']?.customUnitRateID ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.rateID ?? ''; + customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? '']?.customUnitRateID ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.rateID ?? ''; } comment.customUnit = {customUnitRateID}; } diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index 05e3d7c96311..8f682b7ffc5e 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -98,8 +98,8 @@ function IOURequestStartPage({ if (transaction.reportID === reportID) { return; } - IOU.initMoneyRequest(reportID, isFromGlobalCreate, transactionRequestType.current); - }, [transaction, reportID, iouType, isFromGlobalCreate]); + IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, transactionRequestType.current); + }, [transaction, policy, reportID, iouType, isFromGlobalCreate]); const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); @@ -117,10 +117,10 @@ function IOURequestStartPage({ if (newIouType === previousIOURequestType) { return; } - IOU.initMoneyRequest(reportID, isFromGlobalCreate, newIouType); + IOU.initMoneyRequest(reportID, policy, isFromGlobalCreate, newIouType); transactionRequestType.current = newIouType; }, - [previousIOURequestType, reportID, isFromGlobalCreate], + [policy, previousIOURequestType, reportID, isFromGlobalCreate], ); if (!transaction.transactionID) { From 867979957e65f66125e5e11db230d8ba987a510b Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 26 Feb 2024 00:57:05 +0700 Subject: [PATCH 16/98] get report from onyx --- src/libs/actions/IOU.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 92454226519f..f3112ed00415 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -250,9 +250,9 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry, waypoint0: {}, waypoint1: {}, }; - const report = allReports?.[reportID]; + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; - if (report?.isPolicyExpenseChat) { + if (ReportUtils.isPolicyExpenseChat(report)) { customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? '']?.customUnitRateID ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.rateID ?? ''; } comment.customUnit = {customUnitRateID}; From b4401b3243a6e0d35482a479125d47d95e61871c Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 26 Feb 2024 15:07:04 +0700 Subject: [PATCH 17/98] revert: show p2p distance --- src/components/MoneyRequestConfirmationList.js | 7 +++---- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 7 +++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index e9291c8c8457..7d896a9aef39 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -261,7 +261,6 @@ function MoneyRequestConfirmationList(props) { props.isDistanceRequest ? currency : props.iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); - const formattedDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const defaultTaxKey = props.policyTaxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${props.policyTaxRates.taxes[defaultTaxKey].name} (${props.policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; @@ -720,13 +719,13 @@ function MoneyRequestConfirmationList(props) { )} {props.isDistanceRequest && ( Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} - disabled={didConfirm || canUseP2PDistanceRequests || !isTypeRequest} + disabled={didConfirm || !(canUseP2PDistanceRequests || isTypeRequest)} interactive={!props.isReadOnly} /> )} diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 88b339508367..2431acea8077 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -285,7 +285,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ isDistanceRequest ? currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode); - const formattedDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const defaultTaxKey = policyTaxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${policyTaxRates.taxes[defaultTaxKey].name} (${policyTaxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; @@ -680,13 +679,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm || canUseP2PDistanceRequests || !isTypeRequest} + disabled={didConfirm || !(canUseP2PDistanceRequests || isTypeRequest)} interactive={!isReadOnly} /> ), From f9c03d8a7008355ee18ed7143b7446e8cc8c63f7 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 26 Feb 2024 15:12:54 +0700 Subject: [PATCH 18/98] fix lint --- src/components/MoneyRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 7d896a9aef39..ac7d2cfae725 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -720,7 +720,7 @@ function MoneyRequestConfirmationList(props) { {props.isDistanceRequest && ( Date: Mon, 26 Feb 2024 09:22:28 +0100 Subject: [PATCH 19/98] finish migrating createOrUpdateStagingDeployTest to TypeScript --- tests/unit/TranslateTest.ts | 1 + tests/unit/createOrUpdateStagingDeployTest.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/TranslateTest.ts b/tests/unit/TranslateTest.ts index fd3c4f474e88..48baed43b448 100644 --- a/tests/unit/TranslateTest.ts +++ b/tests/unit/TranslateTest.ts @@ -2,6 +2,7 @@ import {AnnotationError} from '@actions/core'; import _ from 'underscore'; import * as translations from '@src/languages/translations'; +import {TranslationPaths} from '@src/languages/types'; import CONFIG from '../../src/CONFIG'; import CONST from '../../src/CONST'; // import * as translations from '../../src/languages/translations'; diff --git a/tests/unit/createOrUpdateStagingDeployTest.ts b/tests/unit/createOrUpdateStagingDeployTest.ts index 28daf8c1674d..38ba4942a785 100644 --- a/tests/unit/createOrUpdateStagingDeployTest.ts +++ b/tests/unit/createOrUpdateStagingDeployTest.ts @@ -52,8 +52,8 @@ beforeAll(() => { list: jest.fn().mockResolvedValue([]), }, }, - paginate: jest.fn().mockImplementation((objectMethod) => objectMethod().then(({data}) => data)), - }; + paginate: jest.fn().mockImplementation((objectMethod: () => Promise<{data: unknown}>) => objectMethod().then(({data}) => data)), + } as typeof GithubUtils.octokit; GithubUtils.internalOctokit = moctokit; // Mock GitUtils From 3d040a06c543d666c1cd46bd04c99131e3010d3c Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 26 Feb 2024 15:58:20 +0700 Subject: [PATCH 20/98] add beta check --- src/components/MoneyRequestConfirmationList.js | 2 +- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index ac7d2cfae725..b7cb530612be 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -719,7 +719,7 @@ function MoneyRequestConfirmationList(props) { )} {props.isDistanceRequest && ( Date: Mon, 26 Feb 2024 10:39:26 +0100 Subject: [PATCH 21/98] remove lodash from TranslateTest --- tests/unit/TranslateTest.ts | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/unit/TranslateTest.ts b/tests/unit/TranslateTest.ts index 48baed43b448..cdae72466d01 100644 --- a/tests/unit/TranslateTest.ts +++ b/tests/unit/TranslateTest.ts @@ -1,14 +1,12 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {AnnotationError} from '@actions/core'; -import _ from 'underscore'; import * as translations from '@src/languages/translations'; -import {TranslationPaths} from '@src/languages/types'; +import type {TranslationFlatObject} from '@src/languages/types'; import CONFIG from '../../src/CONFIG'; import CONST from '../../src/CONST'; -// import * as translations from '../../src/languages/translations'; import * as Localize from '../../src/libs/Localize'; -const originalTranslations = _.clone(translations); +const originalTranslations = {...translations}; translations.default = { [CONST.LOCALES.EN]: translations.flattenObject({ testKey1: 'English', @@ -58,28 +56,32 @@ describe('translate', () => { }); describe('Translation Keys', () => { - function traverseKeyPath(source, path, keyPaths) { - const pathArray = keyPaths || []; + function traverseKeyPath(source: TranslationFlatObject, path?: string, keyPaths?: string[]): string[] { + const pathArray = keyPaths ?? []; const keyPath = path ? `${path}.` : ''; - _.each(_.keys(source), (key) => { - if (_.isObject(source[key]) && !_.isFunction(source[key])) { + Object.keys(source).forEach((key) => { + if (typeof source[key] === 'object' && typeof source[key] !== 'function') { traverseKeyPath(source[key], keyPath + key, pathArray); } else { pathArray.push(keyPath + key); } }); + return pathArray; } - const excludeLanguages = [CONST.LOCALES.EN, CONST.LOCALES.ES_ES]; - const languages = _.without(_.keys(originalTranslations.default), ...excludeLanguages); + function arrayDifference(array1: string[], array2: string[]): string[] { + return [array1, array2].reduce((a, b) => a.filter((c) => !b.includes(c))); + } + const excludeLanguages: Array<'en' | 'es-ES'> = [CONST.LOCALES.EN, CONST.LOCALES.ES_ES]; + const languages = Object.keys(originalTranslations.default).filter((ln) => !(excludeLanguages as string[]).includes(ln)); const mainLanguage = originalTranslations.default.en; const mainLanguageKeys = traverseKeyPath(mainLanguage); - _.each(languages, (ln) => { + languages.forEach((ln) => { const languageKeys = traverseKeyPath(originalTranslations.default[ln]); it(`Does ${ln} locale have all the keys`, () => { - const hasAllKeys = _.difference(mainLanguageKeys, languageKeys); + const hasAllKeys = arrayDifference(mainLanguageKeys, languageKeys); if (hasAllKeys.length) { console.debug(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`); AnnotationError(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`); @@ -88,7 +90,7 @@ describe('Translation Keys', () => { }); it(`Does ${ln} locale have unused keys`, () => { - const hasAllKeys = _.difference(languageKeys, mainLanguageKeys); + const hasAllKeys = arrayDifference(languageKeys, mainLanguageKeys); if (hasAllKeys.length) { console.debug(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`); AnnotationError(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`); From eb5121cae49bcd46535ac57605576b8bc28ccca6 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 26 Feb 2024 15:57:51 +0100 Subject: [PATCH 22/98] type casting in TranslateTest --- tests/unit/TranslateTest.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tests/unit/TranslateTest.ts b/tests/unit/TranslateTest.ts index cdae72466d01..6268410a2a1a 100644 --- a/tests/unit/TranslateTest.ts +++ b/tests/unit/TranslateTest.ts @@ -1,12 +1,13 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import {AnnotationError} from '@actions/core'; import * as translations from '@src/languages/translations'; -import type {TranslationFlatObject} from '@src/languages/types'; +import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; import CONFIG from '../../src/CONFIG'; import CONST from '../../src/CONST'; import * as Localize from '../../src/libs/Localize'; const originalTranslations = {...translations}; + +// @ts-expect-error - We are modifying the translations object for testing purposes translations.default = { [CONST.LOCALES.EN]: translations.flattenObject({ testKey1: 'English', @@ -25,33 +26,36 @@ translations.default = { describe('translate', () => { it('Test present key in full locale', () => { - expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey1')).toBe('Spanish ES'); + expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey1' as TranslationPaths)).toBe('Spanish ES'); }); it('Test when key is not found in full locale, but present in language', () => { - expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey2')).toBe('Spanish Word 2'); - expect(Localize.translate(CONST.LOCALES.ES, 'testKey2')).toBe('Spanish Word 2'); + expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2'); + expect(Localize.translate(CONST.LOCALES.ES, 'testKey2' as TranslationPaths)).toBe('Spanish Word 2'); }); it('Test when key is not found in full locale and language, but present in default', () => { - expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey3')).toBe('Test Word 3'); + expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey3' as TranslationPaths)).toBe('Test Word 3'); }); test('Test when key is not found in default', () => { - expect(() => Localize.translate(CONST.LOCALES.ES_ES, 'testKey4')).toThrow(Error); + expect(() => Localize.translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toThrow(Error); }); test('Test when key is not found in default (Production Mode)', () => { const ORIGINAL_IS_IN_PRODUCTION = CONFIG.IS_IN_PRODUCTION; + // @ts-expect-error - We are modifying the CONFIG object for testing purposes CONFIG.IS_IN_PRODUCTION = true; - expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey4')).toBe('testKey4'); + expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toBe('testKey4'); + // @ts-expect-error - We are modifying the CONFIG object for testing purposes CONFIG.IS_IN_PRODUCTION = ORIGINAL_IS_IN_PRODUCTION; }); it('Test when translation value is a function', () => { const expectedValue = 'With variable Test Variable'; const testVariable = 'Test Variable'; - expect(Localize.translate(CONST.LOCALES.EN, 'testKeyGroup.testFunction', {testVariable})).toBe(expectedValue); + // @ts-expect-error - We are modifying the translations object for testing purposes + expect(Localize.translate(CONST.LOCALES.EN, 'testKeyGroup.testFunction' as TranslationPaths, {testVariable})).toBe(expectedValue); }); }); @@ -60,7 +64,7 @@ describe('Translation Keys', () => { const pathArray = keyPaths ?? []; const keyPath = path ? `${path}.` : ''; Object.keys(source).forEach((key) => { - if (typeof source[key] === 'object' && typeof source[key] !== 'function') { + if (typeof source[key as keyof TranslationFlatObject] === 'object' && typeof source[key as keyof TranslationFlatObject] !== 'function') { traverseKeyPath(source[key], keyPath + key, pathArray); } else { pathArray.push(keyPath + key); @@ -78,13 +82,13 @@ describe('Translation Keys', () => { const mainLanguageKeys = traverseKeyPath(mainLanguage); languages.forEach((ln) => { - const languageKeys = traverseKeyPath(originalTranslations.default[ln]); + const languageKeys = traverseKeyPath(originalTranslations.default[ln as keyof typeof originalTranslations.default]); it(`Does ${ln} locale have all the keys`, () => { const hasAllKeys = arrayDifference(mainLanguageKeys, languageKeys); if (hasAllKeys.length) { console.debug(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`); - AnnotationError(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`); + Error(`🏹 [ ${hasAllKeys.join(', ')} ] are missing from ${ln}.js`); } expect(hasAllKeys).toEqual([]); }); @@ -93,7 +97,7 @@ describe('Translation Keys', () => { const hasAllKeys = arrayDifference(languageKeys, mainLanguageKeys); if (hasAllKeys.length) { console.debug(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`); - AnnotationError(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`); + Error(`🏹 [ ${hasAllKeys.join(', ')} ] are unused keys in ${ln}.js`); } expect(hasAllKeys).toEqual([]); }); @@ -102,7 +106,7 @@ describe('Translation Keys', () => { describe('flattenObject', () => { it('It should work correctly', () => { - const func = ({content}) => `This is the content: ${content}`; + const func = ({content}: {content: string}) => `This is the content: ${content}`; const simpleObject = { common: { yes: 'Yes', From 99fcce89f5e0536b4b45aba52cac322853f95790 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Mon, 26 Feb 2024 16:12:08 +0100 Subject: [PATCH 23/98] migrate TranslateTest to TypeScript --- tests/unit/TranslateTest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/TranslateTest.ts b/tests/unit/TranslateTest.ts index 6268410a2a1a..ab7241ede42a 100644 --- a/tests/unit/TranslateTest.ts +++ b/tests/unit/TranslateTest.ts @@ -65,6 +65,7 @@ describe('Translation Keys', () => { const keyPath = path ? `${path}.` : ''; Object.keys(source).forEach((key) => { if (typeof source[key as keyof TranslationFlatObject] === 'object' && typeof source[key as keyof TranslationFlatObject] !== 'function') { + // @ts-expect-error - We are modifying the translations object for testing purposes traverseKeyPath(source[key], keyPath + key, pathArray); } else { pathArray.push(keyPath + key); From 379e64103d89b062d865b51bacca2db028eaf568 Mon Sep 17 00:00:00 2001 From: "David E. Gelhar" Date: Tue, 27 Feb 2024 07:39:08 -0500 Subject: [PATCH 24/98] allow navigating back from SAML signin --- src/libs/Navigation/NavigationRoot.tsx | 7 ++++++- src/pages/signin/SAMLSignInPage/index.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 20c426a74c71..6694b950fb2b 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -45,7 +45,7 @@ function parseAndLogRoute(state: NavigationState) { const focusedRoute = findFocusedRoute(state); - if (focusedRoute?.name !== SCREENS.NOT_FOUND) { + if (focusedRoute?.name !== SCREENS.NOT_FOUND && focusedRoute?.name !== SCREENS.SAML_SIGN_IN) { updateLastVisitedPath(currentPath); } @@ -70,6 +70,8 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N const initialState = useMemo( () => { + Log.info('Navigating lastVisitedPath', false, { lastVisitedPath: lastVisitedPath}); + if (!lastVisitedPath) { return undefined; } @@ -82,6 +84,9 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N } const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config); + + Log.info('Navigating initialState', false, {initialState: initialState, lastVisitedPath: lastVisitedPath}); + return adaptedState; }, // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/pages/signin/SAMLSignInPage/index.tsx b/src/pages/signin/SAMLSignInPage/index.tsx index 701c2917bea6..1ff9d02672be 100644 --- a/src/pages/signin/SAMLSignInPage/index.tsx +++ b/src/pages/signin/SAMLSignInPage/index.tsx @@ -7,7 +7,7 @@ import type {SAMLSignInPageOnyxProps, SAMLSignInPageProps} from './types'; function SAMLSignInPage({credentials}: SAMLSignInPageProps) { useEffect(() => { - window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self'); + window.location.replace(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials?.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`); }, [credentials?.login]); return ; From 4eca3a13ce408181c0fba2fe0fbe6f0d18732301 Mon Sep 17 00:00:00 2001 From: "David E. Gelhar" Date: Tue, 27 Feb 2024 08:10:26 -0500 Subject: [PATCH 25/98] remove debug logging --- src/libs/Navigation/NavigationRoot.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 6694b950fb2b..2ca4c5178a5e 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -70,8 +70,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N const initialState = useMemo( () => { - Log.info('Navigating lastVisitedPath', false, { lastVisitedPath: lastVisitedPath}); - if (!lastVisitedPath) { return undefined; } @@ -84,9 +82,6 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: N } const {adaptedState} = getAdaptedStateFromPath(lastVisitedPath, linkingConfig.config); - - Log.info('Navigating initialState', false, {initialState: initialState, lastVisitedPath: lastVisitedPath}); - return adaptedState; }, // eslint-disable-next-line react-hooks/exhaustive-deps From 128debb2d3f9d8b3dff276fb56efdb5ed793fafd Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 27 Feb 2024 15:22:24 +0100 Subject: [PATCH 26/98] apply suggested changes --- src/types/utils/asMutable.ts | 5 +++++ tests/unit/TranslateTest.ts | 35 +++++++++++++++++----------------- tests/utils/arrayDifference.ts | 5 +++++ 3 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 src/types/utils/asMutable.ts create mode 100644 tests/utils/arrayDifference.ts diff --git a/src/types/utils/asMutable.ts b/src/types/utils/asMutable.ts new file mode 100644 index 000000000000..57c49058cd14 --- /dev/null +++ b/src/types/utils/asMutable.ts @@ -0,0 +1,5 @@ +import type {Writable} from 'type-fest'; + +const asMutable = (value: T): Writable => value as Writable; + +export default asMutable; diff --git a/tests/unit/TranslateTest.ts b/tests/unit/TranslateTest.ts index ab7241ede42a..40ece1d7525e 100644 --- a/tests/unit/TranslateTest.ts +++ b/tests/unit/TranslateTest.ts @@ -1,14 +1,15 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; import * as translations from '@src/languages/translations'; import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; -import CONFIG from '../../src/CONFIG'; -import CONST from '../../src/CONST'; -import * as Localize from '../../src/libs/Localize'; +import * as Localize from '@src/libs/Localize'; +import asMutable from '@src/types/utils/asMutable'; +import arrayDifference from '../utils/arrayDifference'; const originalTranslations = {...translations}; -// @ts-expect-error - We are modifying the translations object for testing purposes -translations.default = { +asMutable(translations).default = { [CONST.LOCALES.EN]: translations.flattenObject({ testKey1: 'English', testKey2: 'Test Word 2', @@ -44,17 +45,15 @@ describe('translate', () => { test('Test when key is not found in default (Production Mode)', () => { const ORIGINAL_IS_IN_PRODUCTION = CONFIG.IS_IN_PRODUCTION; - // @ts-expect-error - We are modifying the CONFIG object for testing purposes - CONFIG.IS_IN_PRODUCTION = true; + asMutable(CONFIG).IS_IN_PRODUCTION = true; expect(Localize.translate(CONST.LOCALES.ES_ES, 'testKey4' as TranslationPaths)).toBe('testKey4'); - // @ts-expect-error - We are modifying the CONFIG object for testing purposes - CONFIG.IS_IN_PRODUCTION = ORIGINAL_IS_IN_PRODUCTION; + asMutable(CONFIG).IS_IN_PRODUCTION = ORIGINAL_IS_IN_PRODUCTION; }); it('Test when translation value is a function', () => { const expectedValue = 'With variable Test Variable'; const testVariable = 'Test Variable'; - // @ts-expect-error - We are modifying the translations object for testing purposes + // @ts-expect-error - TranslationPaths doesn't include testKeyGroup.testFunction as a valid key expect(Localize.translate(CONST.LOCALES.EN, 'testKeyGroup.testFunction' as TranslationPaths, {testVariable})).toBe(expectedValue); }); }); @@ -63,8 +62,8 @@ describe('Translation Keys', () => { function traverseKeyPath(source: TranslationFlatObject, path?: string, keyPaths?: string[]): string[] { const pathArray = keyPaths ?? []; const keyPath = path ? `${path}.` : ''; - Object.keys(source).forEach((key) => { - if (typeof source[key as keyof TranslationFlatObject] === 'object' && typeof source[key as keyof TranslationFlatObject] !== 'function') { + (Object.keys(source) as Array).forEach((key) => { + if (typeof source[key] === 'object' && typeof source[key] !== 'function') { // @ts-expect-error - We are modifying the translations object for testing purposes traverseKeyPath(source[key], keyPath + key, pathArray); } else { @@ -74,11 +73,9 @@ describe('Translation Keys', () => { return pathArray; } - function arrayDifference(array1: string[], array2: string[]): string[] { - return [array1, array2].reduce((a, b) => a.filter((c) => !b.includes(c))); - } - const excludeLanguages: Array<'en' | 'es-ES'> = [CONST.LOCALES.EN, CONST.LOCALES.ES_ES]; - const languages = Object.keys(originalTranslations.default).filter((ln) => !(excludeLanguages as string[]).includes(ln)); + + const excludeLanguages = [CONST.LOCALES.EN, CONST.LOCALES.ES_ES]; + const languages = Object.keys(originalTranslations.default).filter((ln) => !excludeLanguages.some((excludeLanguage) => excludeLanguage === ln)); const mainLanguage = originalTranslations.default.en; const mainLanguageKeys = traverseKeyPath(mainLanguage); @@ -105,9 +102,11 @@ describe('Translation Keys', () => { }); }); +type ReportContentArgs = {content: string}; + describe('flattenObject', () => { it('It should work correctly', () => { - const func = ({content}: {content: string}) => `This is the content: ${content}`; + const func = ({content}: ReportContentArgs) => `This is the content: ${content}`; const simpleObject = { common: { yes: 'Yes', diff --git a/tests/utils/arrayDifference.ts b/tests/utils/arrayDifference.ts new file mode 100644 index 000000000000..71c3c2c8f015 --- /dev/null +++ b/tests/utils/arrayDifference.ts @@ -0,0 +1,5 @@ +function arrayDifference(array1: T[], array2: T[]): T[] { + return [array1, array2].reduce((a, b) => a.filter((c) => !b.includes(c))); +} + +export default arrayDifference; From 4c9375a07fa1f4631acdcfe6a514591bbc9e9f7b Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 28 Feb 2024 07:56:29 +0100 Subject: [PATCH 27/98] move arrayDifference to global utils --- {tests => src}/utils/arrayDifference.ts | 0 tests/unit/TranslateTest.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename {tests => src}/utils/arrayDifference.ts (100%) diff --git a/tests/utils/arrayDifference.ts b/src/utils/arrayDifference.ts similarity index 100% rename from tests/utils/arrayDifference.ts rename to src/utils/arrayDifference.ts diff --git a/tests/unit/TranslateTest.ts b/tests/unit/TranslateTest.ts index 40ece1d7525e..0be29a29cb12 100644 --- a/tests/unit/TranslateTest.ts +++ b/tests/unit/TranslateTest.ts @@ -5,7 +5,7 @@ import * as translations from '@src/languages/translations'; import type {TranslationFlatObject, TranslationPaths} from '@src/languages/types'; import * as Localize from '@src/libs/Localize'; import asMutable from '@src/types/utils/asMutable'; -import arrayDifference from '../utils/arrayDifference'; +import arrayDifference from '@src/utils/arrayDifference'; const originalTranslations = {...translations}; From 57f7e198f2a6c85b5d37b3123ec825868ff3a500 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 29 Feb 2024 02:53:34 +0700 Subject: [PATCH 28/98] resolve feedbacks --- src/CONST.ts | 2 +- src/components/MoneyRequestConfirmationList.tsx | 9 ++++++--- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 9 ++++++--- src/libs/DistanceRequestUtils.ts | 2 +- src/libs/actions/IOU.ts | 2 +- src/types/onyx/LastSelectedDistanceRates.ts | 4 +--- src/types/onyx/Policy.ts | 2 +- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 75487fa6c162..e314856c9031 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1396,7 +1396,7 @@ const CONST = { MILEAGE_IRS_RATE: 0.655, DEFAULT_RATE: 'Default Rate', RATE_DECIMALS: 3, - FAKE_P2P_ID: '__fake_p2p_id__', + FAKE_P2P_ID: '_FAKE_P2P_ID_', }, TERMS: { diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index 6da05e52d3fd..8387ceddbe98 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -272,6 +272,7 @@ function MoneyRequestConfirmationList({ isDistanceRequest ? mileageRate?.currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction?.taxAmount, iouCurrencyCode); + const formattedDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, mileageRate?.unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, mileageRate?.rate ?? 0, translate); const defaultTaxKey = taxRates?.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${taxRates?.taxes[defaultTaxKey].name} (${taxRates?.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) ?? ''; @@ -597,6 +598,7 @@ function MoneyRequestConfirmationList({ styles.mb2, ]); + const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isSplitBill); const receiptData = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction ?? null, receiptPath, receiptFilename) : null; return ( // @ts-expect-error TODO: Remove this once OptionsSelector (https://github.com/Expensify/App/issues/25125) is migrated to TypeScript. @@ -738,13 +740,14 @@ function MoneyRequestConfirmationList({ )} {isDistanceRequest && ( Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(iouType, reportID))} - disabled={didConfirm || !(canUseP2PDistanceRequests || isTypeRequest)} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + disabled={didConfirm || !canEditDistance} interactive={!isReadOnly} /> )} diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 5374e7fcb0c8..81bdf110373d 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -247,6 +247,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit); const {unit, rate, currency} = mileageRate; const distance = lodashGet(transaction, 'routes.route0.distance', 0); @@ -285,6 +286,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ isDistanceRequest ? currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode); + const formattedDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const defaultTaxKey = taxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; @@ -679,13 +681,14 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} - disabled={didConfirm || !(canUseP2PDistanceRequests || isTypeRequest)} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + disabled={didConfirm || !canEditDistance} interactive={!isReadOnly} /> ), diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 8e95394605a3..3585d8e12a92 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -32,7 +32,7 @@ function getDefaultMileageRate(policy: OnyxEntry): MileageRate | null { } return { - rateID: distanceRate.customUnitRateID, + customUnitRateID: distanceRate.customUnitRateID, rate: distanceRate.rate, currency: distanceRate.currency ?? 'USD', unit: distanceUnit.attributes.unit, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c4388563af9f..b9eba1368ce9 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -255,7 +255,7 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry, const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; if (ReportUtils.isPolicyExpenseChat(report)) { - customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? '']?.customUnitRateID ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.rateID ?? ''; + customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; } comment.customUnit = {customUnitRateID}; } diff --git a/src/types/onyx/LastSelectedDistanceRates.ts b/src/types/onyx/LastSelectedDistanceRates.ts index 4ebf39e43bcb..1db1cf32b160 100644 --- a/src/types/onyx/LastSelectedDistanceRates.ts +++ b/src/types/onyx/LastSelectedDistanceRates.ts @@ -1,5 +1,3 @@ -import type {Rate} from './Policy'; - -type LastSelectedDistanceRates = Record; +type LastSelectedDistanceRates = Record; export default LastSelectedDistanceRates; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 2e4447e92c8b..c7a2a7f8a4e1 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -18,7 +18,7 @@ type Attributes = { }; type MileageRate = { - rateID?: string; + customUnitRateID?: string; unit: Unit; rate?: number; currency: string; From e137f6234675a54877022d30d8202bbf281a42af Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 1 Mar 2024 03:24:09 +0700 Subject: [PATCH 29/98] reapply changes --- src/components/MoneyRequestConfirmationList.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index df2781d3ea89..4f892e9ace75 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -217,11 +217,12 @@ function MoneyRequestConfirmationList(props) { const {onSendMoney, onConfirm, onSelectParticipant} = props; const {translate, toLocaleDigit} = useLocalize(); const transaction = props.transaction; - const {canUseViolations} = usePermissions(); + const {canUseP2PDistanceRequests, canUseViolations} = usePermissions(); const isTypeRequest = props.iouType === CONST.IOU.TYPE.REQUEST; const isSplitBill = props.iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = props.iouType === CONST.IOU.TYPE.SEND; + const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isSplitBill); const isSplitWithScan = isSplitBill && props.isScanRequest; @@ -262,6 +263,7 @@ function MoneyRequestConfirmationList(props) { props.isDistanceRequest ? currency : props.iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); + const formattedDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const defaultTaxKey = taxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; @@ -720,13 +722,14 @@ function MoneyRequestConfirmationList(props) { )} {props.isDistanceRequest && ( Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} - disabled={didConfirm || !isTypeRequest} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + disabled={didConfirm || !canEditDistance} interactive={!props.isReadOnly} /> )} From 7a3fe0756c67e82d3395beb8623e0647ef139d4d Mon Sep 17 00:00:00 2001 From: dragnoir Date: Mon, 4 Mar 2024 09:15:04 +0100 Subject: [PATCH 30/98] Fix: Category - add spacing between long category name and category status --- src/pages/workspace/categories/WorkspaceCategoriesPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index d15011489bac..8a35c085ac56 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -57,7 +57,9 @@ function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesP isSelected: !!selectedCategories[value.name], rightElement: ( - {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')} + + {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')} + Date: Mon, 4 Mar 2024 15:32:41 +0700 Subject: [PATCH 31/98] refactor distancefordisplay func --- src/components/MoneyRequestConfirmationList.js | 3 +-- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 3 +-- src/libs/DistanceRequestUtils.ts | 8 +++----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 4f892e9ace75..9c322cee56ac 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -263,7 +263,6 @@ function MoneyRequestConfirmationList(props) { props.isDistanceRequest ? currency : props.iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(props.transaction.taxAmount, props.iouCurrencyCode); - const formattedDistance = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); const defaultTaxKey = taxRates.defaultExternalID; const defaultTaxName = (defaultTaxKey && `${taxRates.taxes[defaultTaxKey].name} (${taxRates.taxes[defaultTaxKey].value}) • ${translate('common.default')}`) || ''; @@ -723,7 +722,7 @@ function MoneyRequestConfirmationList(props) { {props.isDistanceRequest && ( Date: Mon, 4 Mar 2024 09:39:19 +0100 Subject: [PATCH 32/98] apply suggested changes to arrayDifference util funciton --- src/utils/arrayDifference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/arrayDifference.ts b/src/utils/arrayDifference.ts index 71c3c2c8f015..c1c87544aa25 100644 --- a/src/utils/arrayDifference.ts +++ b/src/utils/arrayDifference.ts @@ -1,4 +1,4 @@ -function arrayDifference(array1: T[], array2: T[]): T[] { +function arrayDifference(array1: TItem[], array2: TItem[]): TItem[] { return [array1, array2].reduce((a, b) => a.filter((c) => !b.includes(c))); } From 26c397935f9013ed14fa605bd60d7640f5a9ed3d Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 4 Mar 2024 16:02:33 +0700 Subject: [PATCH 33/98] allow distance request in group chat and dm --- src/pages/iou/MoneyRequestSelectorPage.js | 4 +++- src/pages/iou/request/IOURequestStartPage.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 8d7272df63e9..62b1adf1fb8c 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -10,6 +10,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -62,6 +63,7 @@ const defaultProps = { function MoneyRequestSelectorPage(props) { const styles = useThemeStyles(); const [isDraggingOver, setIsDraggingOver] = useState(false); + const {canUseP2PDistanceRequests} = usePermissions(); const iouType = lodashGet(props.route, 'params.iouType', ''); const reportID = lodashGet(props.route, 'params.reportID', ''); @@ -75,7 +77,7 @@ function MoneyRequestSelectorPage(props) { const isFromGlobalCreate = !reportID; const isExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); const isExpenseReport = ReportUtils.isExpenseReport(props.report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; const resetMoneyRequestInfo = () => { const moneyRequestID = `${iouType}${reportID}`; diff --git a/src/pages/iou/request/IOURequestStartPage.js b/src/pages/iou/request/IOURequestStartPage.js index fa6fee2202ac..b1ae257b792f 100644 --- a/src/pages/iou/request/IOURequestStartPage.js +++ b/src/pages/iou/request/IOURequestStartPage.js @@ -13,6 +13,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -80,6 +81,7 @@ function IOURequestStartPage({ }; const transactionRequestType = useRef(TransactionUtils.getRequestType(transaction)); const previousIOURequestType = usePrevious(transactionRequestType.current); + const {canUseP2PDistanceRequests} = usePermissions(); const isFromGlobalCreate = _.isEmpty(report.reportID); useFocusEffect( @@ -107,7 +109,7 @@ function IOURequestStartPage({ const isExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isExpenseReport = ReportUtils.isExpenseReport(report); - const shouldDisplayDistanceRequest = isExpenseChat || isExpenseReport || isFromGlobalCreate; + const shouldDisplayDistanceRequest = canUseP2PDistanceRequests || isExpenseChat || isExpenseReport || isFromGlobalCreate; // Allow the user to create the request if we are creating the request in global menu or the report can create the request const isAllowedToCreateRequest = _.isEmpty(report.reportID) || ReportUtils.canCreateRequest(report, policy, iouType); From fa8c48e7275d33a6d3da323cb788361f4fc226d9 Mon Sep 17 00:00:00 2001 From: Alberto Date: Mon, 4 Mar 2024 16:21:54 +0100 Subject: [PATCH 34/98] use absolute value --- src/libs/actions/IOU.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 641099fc9db9..d4b85edc7a90 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3638,7 +3638,7 @@ function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxT chatReportID: chatReport.reportID, reportActionID: optimisticIOUReportAction.reportActionID, paymentMethodType, - amount: total, + amount: Math.abs(total), }, optimisticData, successData, From 1cef0140429c47d2abdbd2a4c0ac832145aaaa38 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Mon, 4 Mar 2024 14:45:55 -0300 Subject: [PATCH 35/98] Remove old pusher update format --- src/libs/actions/User.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index a14e752b1015..7d414ea625ad 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -578,21 +578,8 @@ function subscribeToUserEvents() { // Handles the mega multipleEvents from Pusher which contains an array of single events. // Each single event is passed to PusherUtils in order to trigger the callbacks for that event PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.MULTIPLE_EVENTS, currentUserAccountID.toString(), (pushJSON) => { - // The data for this push event comes in two different formats: - // 1. Original format - this is what was sent before the RELIABLE_UPDATES project and will go away once RELIABLE_UPDATES is fully complete - // - The data is an array of objects, where each object is an onyx update - // Example: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}] - // 1. Reliable updates format - this is what was sent with the RELIABLE_UPDATES project and will be the format from now on - // - The data is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above) - // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]} - if (Array.isArray(pushJSON)) { - Log.warn('Received pusher event with array format'); - pushJSON.forEach((multipleEvent) => { - PusherUtils.triggerMultiEventHandler(multipleEvent.eventType, multipleEvent.data); - }); - return; - } - + // The data for the update is an object, containing updateIDs from the server and an array of onyx updates (this array is the same format as the original format above) + // Example: {lastUpdateID: 1, previousUpdateID: 0, updates: [{onyxMethod: 'whatever', key: 'foo', value: 'bar'}]} const updates = { type: CONST.ONYX_UPDATE_TYPES.PUSHER, lastUpdateID: Number(pushJSON.lastUpdateID || 0), From f31a60720c6e4ec28767189d1a9cff08c29010f5 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Mon, 4 Mar 2024 15:20:33 -0300 Subject: [PATCH 36/98] Remove old format from type --- src/libs/Pusher/pusher.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts index bc48111eadc5..81f63a75edbd 100644 --- a/src/libs/Pusher/pusher.ts +++ b/src/libs/Pusher/pusher.ts @@ -22,7 +22,7 @@ type Args = { authEndpoint: string; }; -type PushJSON = OnyxUpdateEvent[] | OnyxUpdatesFromServer; +type PushJSON = OnyxUpdatesFromServer; type UserIsTypingEvent = ReportUserIsTyping & { userLogin?: string; From be35d4c9eaf1277391ae41d19b58d79ea371aca9 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Mon, 4 Mar 2024 15:40:55 -0300 Subject: [PATCH 37/98] Remove unneeded type --- src/libs/Pusher/pusher.ts | 8 +++----- src/libs/PusherUtils.ts | 8 ++++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher.ts index 81f63a75edbd..3cb15c0f3fc3 100644 --- a/src/libs/Pusher/pusher.ts +++ b/src/libs/Pusher/pusher.ts @@ -5,7 +5,7 @@ import type {LiteralUnion, ValueOf} from 'type-fest'; import Log from '@libs/Log'; import type CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {OnyxUpdateEvent, OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; +import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import TYPE from './EventType'; import Pusher from './library'; @@ -22,8 +22,6 @@ type Args = { authEndpoint: string; }; -type PushJSON = OnyxUpdatesFromServer; - type UserIsTypingEvent = ReportUserIsTyping & { userLogin?: string; }; @@ -37,7 +35,7 @@ type PusherEventMap = { [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent; }; -type EventData = EventName extends keyof PusherEventMap ? PusherEventMap[EventName] : PushJSON; +type EventData = EventName extends keyof PusherEventMap ? PusherEventMap[EventName] : OnyxUpdatesFromServer; type EventCallbackError = {type: ValueOf; data: {code: number}}; @@ -413,4 +411,4 @@ export { getPusherSocketID, }; -export type {EventCallbackError, States, PushJSON, UserIsTypingEvent, UserIsLeavingRoomEvent}; +export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent}; diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts index 1ee75eb9c2f6..2bd79adef516 100644 --- a/src/libs/PusherUtils.ts +++ b/src/libs/PusherUtils.ts @@ -1,10 +1,10 @@ import type {OnyxUpdate} from 'react-native-onyx'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; +import type {OnyxUpdatesFromServer} from '@src/types/onyx'; import Log from './Log'; import NetworkConnection from './NetworkConnection'; import * as Pusher from './Pusher/pusher'; -import type {PushJSON} from './Pusher/pusher'; type Callback = (data: OnyxUpdate[]) => Promise; @@ -25,10 +25,10 @@ function triggerMultiEventHandler(eventType: string, data: OnyxUpdate[]): Promis /** * Abstraction around subscribing to private user channel events. Handles all logs and errors automatically. */ -function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: PushJSON) => void) { +function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string, onEvent: (pushJSON: OnyxUpdatesFromServer) => void) { const pusherChannelName = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${accountID}${CONFIG.PUSHER.SUFFIX}` as const; - function logPusherEvent(pushJSON: PushJSON) { + function logPusherEvent(pushJSON: OnyxUpdatesFromServer) { Log.info(`[Report] Handled ${eventName} event sent by Pusher`, false, pushJSON); } @@ -36,7 +36,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string NetworkConnection.triggerReconnectionCallbacks('Pusher re-subscribed to private user channel'); } - function onEventPush(pushJSON: PushJSON) { + function onEventPush(pushJSON: OnyxUpdatesFromServer) { logPusherEvent(pushJSON); onEvent(pushJSON); } From 938df966a7216c3a967a295826a6c7fc8b8f2a69 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Mon, 4 Mar 2024 15:51:15 -0300 Subject: [PATCH 38/98] Fix random error that popped up --- .../HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index 5d8c0f6ef81e..f7ade59276ca 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -21,7 +21,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; -import asMutable from '@src/types/utils/asMutable'; +import asMutable from '@src/types/utils/AsMutable'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type MentionUserRendererProps = WithCurrentUserPersonalDetailsProps & CustomRendererProps; From 4e54e23d5dc73a670abedfbfe2fc032847c1cdb3 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Mon, 4 Mar 2024 16:46:11 -0300 Subject: [PATCH 39/98] Revert back the asmutable change --- .../HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx index f7ade59276ca..5d8c0f6ef81e 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/MentionUserRenderer.tsx @@ -21,7 +21,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; -import asMutable from '@src/types/utils/AsMutable'; +import asMutable from '@src/types/utils/asMutable'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type MentionUserRendererProps = WithCurrentUserPersonalDetailsProps & CustomRendererProps; From 7bd676ab0cc15f83e5bab5c251d90c530bc93094 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Mon, 4 Mar 2024 23:06:44 +0300 Subject: [PATCH 40/98] fix copy to clipboard of hold and unhold actions --- src/pages/home/report/ContextMenu/ContextMenuActions.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 2fd0d0a964a8..e0edfc444af9 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -13,6 +13,7 @@ import EmailUtils from '@libs/EmailUtils'; import * as Environment from '@libs/Environment/Environment'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; +import * as Localize from '@libs/Localize'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; @@ -372,6 +373,10 @@ const ContextMenuActions: ContextMenuAction[] = [ } else if (ReportActionsUtils.isActionableMentionWhisper(reportAction)) { const mentionWhisperMessage = ReportActionsUtils.getActionableMentionWhisperMessage(reportAction); setClipboardMessage(mentionWhisperMessage); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.HOLD) { + Clipboard.setString(Localize.translateLocal('iou.heldRequest', {comment: reportAction.message?.[1]?.text ?? ''})); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) { + Clipboard.setString(Localize.translateLocal('iou.unheldRequest')); } else if (content) { setClipboardMessage(content); } else if (messageText) { From 06f5b2b7a8cf96a090f65a24d7b30d87376bf610 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Mon, 4 Mar 2024 17:12:32 -0300 Subject: [PATCH 41/98] Fix tests --- tests/utils/PusherHelper.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts index 4ee3e63b13e7..dcd144e77596 100644 --- a/tests/utils/PusherHelper.ts +++ b/tests/utils/PusherHelper.ts @@ -26,12 +26,17 @@ function setup() { function emitOnyxUpdate(args: OnyxUpdate[]) { const channel = Pusher.getChannel(CHANNEL_NAME); - channel?.emit(Pusher.TYPE.MULTIPLE_EVENTS, [ - { - eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, - data: args, - }, - ]); + channel?.emit(Pusher.TYPE.MULTIPLE_EVENTS, { + type: 'pusher', + lastUpdateID: null, + previousUpdateID: null, + updates: [ + { + eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, + data: args, + }, + ], + }); } function teardown() { From 9e3a9f303c14b6e594c9f2817fc3099cfd901d36 Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Mon, 4 Mar 2024 18:02:28 -0300 Subject: [PATCH 42/98] Fix one test --- tests/ui/UnreadIndicatorsTest.js | 95 +++++++++++++++++--------------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 6051f04f570e..3f9ea31ff1cb 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -376,55 +376,60 @@ describe('Unread Indicators', () => { const createdReportActionID = NumberUtils.rand64(); const commentReportActionID = NumberUtils.rand64(); const channel = Pusher.getChannel(`${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${USER_A_ACCOUNT_ID}${CONFIG.PUSHER.SUFFIX}`); - channel.emit(Pusher.TYPE.MULTIPLE_EVENTS, [ - { - eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, - data: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, - value: { - reportID: NEW_REPORT_ID, - reportName: CONST.REPORT.DEFAULT_REPORT_NAME, - lastReadTime: '', - lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()), - lastMessageText: 'Comment 1', - lastActorAccountID: USER_C_ACCOUNT_ID, - participantAccountIDs: [USER_C_ACCOUNT_ID], - type: CONST.REPORT.TYPE.CHAT, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, - value: { - [createdReportActionID]: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - created: format(NEW_REPORT_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING), - reportActionID: createdReportActionID, + channel.emit(Pusher.TYPE.MULTIPLE_EVENTS, { + type: 'pusher', + lastUpdateID: null, + previousUpdateID: null, + updates: [ + { + eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, + data: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, + value: { + reportID: NEW_REPORT_ID, + reportName: CONST.REPORT.DEFAULT_REPORT_NAME, + lastReadTime: '', + lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()), + lastMessageText: 'Comment 1', + lastActorAccountID: USER_C_ACCOUNT_ID, + participantAccountIDs: [USER_C_ACCOUNT_ID], + type: CONST.REPORT.TYPE.CHAT, }, - [commentReportActionID]: { - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - actorAccountID: USER_C_ACCOUNT_ID, - person: [{type: 'TEXT', style: 'strong', text: 'User C'}], - created: format(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING), - message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], - reportActionID: commentReportActionID, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, + value: { + [createdReportActionID]: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + created: format(NEW_REPORT_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING), + reportActionID: createdReportActionID, + }, + [commentReportActionID]: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorAccountID: USER_C_ACCOUNT_ID, + person: [{type: 'TEXT', style: 'strong', text: 'User C'}], + created: format(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING), + message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], + reportActionID: commentReportActionID, + }, }, + shouldNotify: true, }, - shouldNotify: true, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + }, }, - }, - ], - }, - ]); + ], + }, + ], + }); return waitForBatchedUpdates(); }) .then(() => { From 5aabc64a5031248703c27cd27ab071009b2baffe Mon Sep 17 00:00:00 2001 From: Ionatan Wiznia Date: Mon, 4 Mar 2024 18:10:54 -0300 Subject: [PATCH 43/98] Use helper --- tests/ui/UnreadIndicatorsTest.js | 98 ++++++++++++++------------------ 1 file changed, 44 insertions(+), 54 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 3f9ea31ff1cb..851dc889728e 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -21,6 +21,7 @@ import * as Pusher from '../../src/libs/Pusher/pusher'; import PusherConnectionManager from '../../src/libs/PusherConnectionManager'; import ONYXKEYS from '../../src/ONYXKEYS'; import appSetup from '../../src/setup'; +import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; @@ -375,61 +376,50 @@ describe('Unread Indicators', () => { const NEW_REPORT_FIST_MESSAGE_CREATED_DATE = addSeconds(NEW_REPORT_CREATED_DATE, 1); const createdReportActionID = NumberUtils.rand64(); const commentReportActionID = NumberUtils.rand64(); - const channel = Pusher.getChannel(`${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${USER_A_ACCOUNT_ID}${CONFIG.PUSHER.SUFFIX}`); - channel.emit(Pusher.TYPE.MULTIPLE_EVENTS, { - type: 'pusher', - lastUpdateID: null, - previousUpdateID: null, - updates: [ - { - eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE, - data: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, - value: { - reportID: NEW_REPORT_ID, - reportName: CONST.REPORT.DEFAULT_REPORT_NAME, - lastReadTime: '', - lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()), - lastMessageText: 'Comment 1', - lastActorAccountID: USER_C_ACCOUNT_ID, - participantAccountIDs: [USER_C_ACCOUNT_ID], - type: CONST.REPORT.TYPE.CHAT, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, - value: { - [createdReportActionID]: { - actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, - automatic: false, - created: format(NEW_REPORT_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING), - reportActionID: createdReportActionID, - }, - [commentReportActionID]: { - actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, - actorAccountID: USER_C_ACCOUNT_ID, - person: [{type: 'TEXT', style: 'strong', text: 'User C'}], - created: format(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING), - message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], - reportActionID: commentReportActionID, - }, - }, - shouldNotify: true, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: { - [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), - }, - }, - ], + PusherHelper.emitOnyxUpdate([ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${NEW_REPORT_ID}`, + value: { + reportID: NEW_REPORT_ID, + reportName: CONST.REPORT.DEFAULT_REPORT_NAME, + lastReadTime: '', + lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()), + lastMessageText: 'Comment 1', + lastActorAccountID: USER_C_ACCOUNT_ID, + participantAccountIDs: [USER_C_ACCOUNT_ID], + type: CONST.REPORT.TYPE.CHAT, }, - ], - }); + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${NEW_REPORT_ID}`, + value: { + [createdReportActionID]: { + actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, + automatic: false, + created: format(NEW_REPORT_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING), + reportActionID: createdReportActionID, + }, + [commentReportActionID]: { + actionName: CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, + actorAccountID: USER_C_ACCOUNT_ID, + person: [{type: 'TEXT', style: 'strong', text: 'User C'}], + created: format(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, CONST.DATE.FNS_DB_FORMAT_STRING), + message: [{type: 'COMMENT', html: 'Comment 1', text: 'Comment 1'}], + reportActionID: commentReportActionID, + }, + }, + shouldNotify: true, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + value: { + [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + }, + }, + ]); return waitForBatchedUpdates(); }) .then(() => { From c0d651e47e36644c745ecffbadd819db8f7b15ca Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 4 Mar 2024 16:53:46 -0700 Subject: [PATCH 44/98] register route --- src/ROUTES.ts | 4 ++++ src/SCREENS.ts | 1 + src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 +++ src/libs/Navigation/types.ts | 3 +++ 5 files changed, 12 insertions(+) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cfc287ba2cdc..83ac055c448e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -546,6 +546,10 @@ const ROUTES = { route: 'workspace/:policyID/categories/settings', getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, }, + WORKSPACE_CREATE_CATEGORY: { + route: 'workspace/:policyID/categories/new', + getRoute: (policyID: string) => `workspace/${policyID}/categories/new` as const, + }, WORKSPACE_TAGS: { route: 'workspace/:policyID/tags', getRoute: (policyID: string) => `workspace/${policyID}/tags` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2369fe435feb..17073d289f8f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -223,6 +223,7 @@ const SCREENS = { DESCRIPTION: 'Workspace_Profile_Description', SHARE: 'Workspace_Profile_Share', NAME: 'Workspace_Profile_Name', + CATEGORY_CREATE: 'Category_Create', CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 545641957c9a..cee9e31cedea 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -251,6 +251,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceProfileCurrencyPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../pages/workspace/categories/CategorySettingsPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../pages/workspace/categories/CreateCategoryPage').default as React.ComponentType, [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, [SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType, [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 3ceb3c1ac7df..4f2810b2e501 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -280,6 +280,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route, }, + [SCREENS.WORKSPACE.CATEGORY_CREATE]: { + path: ROUTES.WORKSPACE_CREATE_CATEGORY.route, + }, [SCREENS.REIMBURSEMENT_ACCOUNT]: { path: ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.route, exact: true, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9426ca6e900c..feb7ec0f7cbe 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -91,6 +91,9 @@ type CentralPaneNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES]: { policyID: string; }; + [SCREENS.WORKSPACE.CATEGORY_CREATE]: { + policyID: string; + }; [SCREENS.WORKSPACE.TAGS]: { policyID: string; }; From 39a2af4d0aa8c1a271dc27e15bdddc0190931458 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 4 Mar 2024 17:10:07 -0700 Subject: [PATCH 45/98] add button on web --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/Navigation/types.ts | 1 + .../categories/CreateCategoryPage.tsx | 68 +++++++++++++++++++ .../categories/WorkspaceCategoriesPage.tsx | 18 ++++- 5 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/pages/workspace/categories/CreateCategoryPage.tsx diff --git a/src/languages/en.ts b/src/languages/en.ts index cfbc3ecd0565..59d08b13aa4e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1768,6 +1768,7 @@ export default { subtitle: 'Add a category to organize your spend.', }, genericFailureMessage: 'An error occurred while updating the category, please try again.', + addCategory: "Add category", }, tags: { requiresTag: 'Members must tag all spend', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9aea06797cc5..716a64a2650b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1792,6 +1792,7 @@ export default { subtitle: 'Añade una categoría para organizar tu gasto.', }, genericFailureMessage: 'Se ha producido un error al intentar eliminar la categoría. Por favor, inténtalo más tarde.', + addCategory: 'Añadir categoría', }, tags: { requiresTag: 'Los miembros deben etiquetar todos los gastos', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index feb7ec0f7cbe..f40c2772f3f4 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -96,6 +96,7 @@ type CentralPaneNavigatorParamList = { }; [SCREENS.WORKSPACE.TAGS]: { policyID: string; + categoryName: string; }; }; diff --git a/src/pages/workspace/categories/CreateCategoryPage.tsx b/src/pages/workspace/categories/CreateCategoryPage.tsx new file mode 100644 index 000000000000..8f0cfc4950b3 --- /dev/null +++ b/src/pages/workspace/categories/CreateCategoryPage.tsx @@ -0,0 +1,68 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useMemo, useState} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import Button from '@components/Button'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import TableListItem from '@components/SelectionList/TableListItem'; +import Text from '@components/Text'; +import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; +import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; + +type PolicyForList = { + value: string; + text: string; + keyForList: string; + isSelected: boolean; + rightElement: React.ReactNode; +}; + +type WorkspaceCategoriesOnyxProps = { + /** Collection of categories attached to a policy */ + policyCategories: OnyxEntry; +}; + +type WorkspaceCategoriesPageProps = WorkspaceCategoriesOnyxProps & StackScreenProps; + +function CreateCategoryPage({route}: WorkspaceCategoriesPageProps) { + + return ( + + + + Test + + + + ); +} + +CreateCategoryPage.displayName = 'CreateCategoryPage'; + +export default withOnyx({ + policyCategories: { + key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params.policyID}`, + }, +})(CreateCategoryPage); diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index d15011489bac..ae38a9f4e247 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -97,6 +97,10 @@ function WorkspaceCategoriesPage({policyCategories, route}: WorkspaceCategoriesP Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, category.text)); }; + const navigateToCreateCategoryPage = () => { + Navigation.navigate(ROUTES.WORKSPACE_CREATE_CATEGORY.getRoute(route.params.policyID)) + } + const settingsButton = (