From d38fb4d47c7d18d26afaab37c6e789edcac0127d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 3 Dec 2024 13:33:06 +0700 Subject: [PATCH 001/110] fix: paying expense and canceling payment don't scroll down to comments --- src/libs/actions/IOU.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 35d72d1b005c..624bc8153583 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7565,6 +7565,7 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O }, {optimisticData, successData, failureData}, ); + Report.notifyNewAction(expenseReport.reportID, userAccountID); } /** @@ -7623,6 +7624,7 @@ function payMoneyRequest(paymentType: PaymentMethodType, chatReport: OnyxTypes.R const apiCommand = paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY ? WRITE_COMMANDS.PAY_MONEY_REQUEST_WITH_WALLET : WRITE_COMMANDS.PAY_MONEY_REQUEST; API.write(apiCommand, params, {optimisticData, successData, failureData}); + Report.notifyNewAction(iouReport?.reportID ?? '', userAccountID); } function payInvoice(paymentMethodType: PaymentMethodType, chatReport: OnyxTypes.Report, invoiceReport: OnyxEntry, payAsBusiness = false) { From 8250f0590e2e0de7fa53527676f756d7c2cd0eb6 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 3 Dec 2024 14:13:48 +0700 Subject: [PATCH 002/110] prevent submit button from jumping when proceeding to the confirmation page --- .../step/IOURequestStepParticipants.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index fb2484ea414f..4552e94b3b5d 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -18,6 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Participant} from '@src/types/onyx/IOU'; +import KeyboardUtils from '@src/utils/keyboard'; import StepScreenWrapper from './StepScreenWrapper'; import type {WithFullTransactionOrNotFoundProps} from './withFullTransactionOrNotFound'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; @@ -131,11 +132,14 @@ function IOURequestStepParticipants({ transactionID, selectedReportID.current || reportID, ); - if (isCategorizing) { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute)); - } else { - Navigation.navigate(iouConfirmationPageRoute); - } + + const route = isCategorizing + ? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute) + : iouConfirmationPageRoute; + + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(route); + }); }, [iouType, transactionID, transaction, reportID, action, participants]); const navigateBack = useCallback(() => { @@ -153,7 +157,9 @@ function IOURequestStepParticipants({ IOU.setCustomUnitRateID(transactionID, rateID); IOU.setMoneyRequestParticipantsFromReport(transactionID, ReportUtils.getReport(selfDMReportID)); const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID); - Navigation.navigate(iouConfirmationPageRoute); + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(iouConfirmationPageRoute); + }); }; useEffect(() => { From c0d30f792930f1757d933f7370a618876b0f1335 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Wed, 4 Dec 2024 16:04:24 +0700 Subject: [PATCH 003/110] Update dismiss keyboard for web --- src/utils/keyboard.ts | 83 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 11 deletions(-) diff --git a/src/utils/keyboard.ts b/src/utils/keyboard.ts index a2b1d329aa0a..6cea7dc727cf 100644 --- a/src/utils/keyboard.ts +++ b/src/utils/keyboard.ts @@ -1,25 +1,82 @@ -import {Keyboard} from 'react-native'; +import {InteractionManager, Keyboard} from 'react-native'; +import getPlatform from '@libs/getPlatform'; +import CONST from '@src/CONST'; -let isVisible = false; +let isNativeKeyboardVisible = false; // Native keyboard visibility +let isWebKeyboardOpen = false; // Web keyboard visibility +const isWeb = getPlatform() === CONST.PLATFORM.WEB; +/** + * Initializes native keyboard visibility listeners + */ +const initializeNativeKeyboardListeners = () => { + Keyboard.addListener('keyboardDidHide', () => { + isNativeKeyboardVisible = false; + }); + + Keyboard.addListener('keyboardDidShow', () => { + isNativeKeyboardVisible = true; + }); +}; + +/** + * Checks if the given HTML element is a keyboard-related input + */ +const isKeyboardInput = (elem: HTMLElement): boolean => + (elem.tagName === 'INPUT' && !['button', 'submit', 'checkbox', 'file', 'image'].includes((elem as HTMLInputElement).type)) || elem.hasAttribute('contenteditable'); -Keyboard.addListener('keyboardDidHide', () => { - isVisible = false; -}); +/** + * Initializes web-specific keyboard visibility listeners + */ +const initializeWebKeyboardListeners = () => { + if (typeof document === 'undefined' || !isWeb) { + return; + } + + const handleFocusIn = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isWebKeyboardOpen = true; + } + }; + + const handleFocusOut = (e: FocusEvent) => { + const target = e.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isWebKeyboardOpen = false; + } + }; -Keyboard.addListener('keyboardDidShow', () => { - isVisible = true; -}); + document.addEventListener('focusin', handleFocusIn); + document.addEventListener('focusout', handleFocusOut); +}; +/** + * Dismisses the keyboard and resolves the promise when the dismissal is complete + */ const dismiss = (): Promise => { return new Promise((resolve) => { - if (!isVisible) { - resolve(); + if (isWeb) { + if (!isWebKeyboardOpen) { + resolve(); + return; + } + + Keyboard.dismiss(); + InteractionManager.runAfterInteractions(() => { + isWebKeyboardOpen = false; + resolve(); + }); return; } + if (!isNativeKeyboardVisible) { + resolve(); + return; + } + const subscription = Keyboard.addListener('keyboardDidHide', () => { - resolve(undefined); + resolve(); subscription.remove(); }); @@ -27,6 +84,10 @@ const dismiss = (): Promise => { }); }; +// Initialize listeners for native and web +initializeNativeKeyboardListeners(); +initializeWebKeyboardListeners(); + const utils = {dismiss}; export default utils; From ff74004bb4fec9c63b3cadf343da23746b45dcb4 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 5 Dec 2024 09:08:02 +0700 Subject: [PATCH 004/110] Add keyboard dismiss for web --- src/utils/keyboard.ts | 93 ----------------------------- src/utils/keyboard/index.ts | 32 ++++++++++ src/utils/keyboard/index.website.ts | 46 ++++++++++++++ 3 files changed, 78 insertions(+), 93 deletions(-) delete mode 100644 src/utils/keyboard.ts create mode 100644 src/utils/keyboard/index.ts create mode 100644 src/utils/keyboard/index.website.ts diff --git a/src/utils/keyboard.ts b/src/utils/keyboard.ts deleted file mode 100644 index 6cea7dc727cf..000000000000 --- a/src/utils/keyboard.ts +++ /dev/null @@ -1,93 +0,0 @@ -import {InteractionManager, Keyboard} from 'react-native'; -import getPlatform from '@libs/getPlatform'; -import CONST from '@src/CONST'; - -let isNativeKeyboardVisible = false; // Native keyboard visibility -let isWebKeyboardOpen = false; // Web keyboard visibility -const isWeb = getPlatform() === CONST.PLATFORM.WEB; -/** - * Initializes native keyboard visibility listeners - */ -const initializeNativeKeyboardListeners = () => { - Keyboard.addListener('keyboardDidHide', () => { - isNativeKeyboardVisible = false; - }); - - Keyboard.addListener('keyboardDidShow', () => { - isNativeKeyboardVisible = true; - }); -}; - -/** - * Checks if the given HTML element is a keyboard-related input - */ -const isKeyboardInput = (elem: HTMLElement): boolean => - (elem.tagName === 'INPUT' && !['button', 'submit', 'checkbox', 'file', 'image'].includes((elem as HTMLInputElement).type)) || elem.hasAttribute('contenteditable'); - -/** - * Initializes web-specific keyboard visibility listeners - */ -const initializeWebKeyboardListeners = () => { - if (typeof document === 'undefined' || !isWeb) { - return; - } - - const handleFocusIn = (e: FocusEvent) => { - const target = e.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isWebKeyboardOpen = true; - } - }; - - const handleFocusOut = (e: FocusEvent) => { - const target = e.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isWebKeyboardOpen = false; - } - }; - - document.addEventListener('focusin', handleFocusIn); - document.addEventListener('focusout', handleFocusOut); -}; - -/** - * Dismisses the keyboard and resolves the promise when the dismissal is complete - */ -const dismiss = (): Promise => { - return new Promise((resolve) => { - if (isWeb) { - if (!isWebKeyboardOpen) { - resolve(); - return; - } - - Keyboard.dismiss(); - InteractionManager.runAfterInteractions(() => { - isWebKeyboardOpen = false; - resolve(); - }); - - return; - } - - if (!isNativeKeyboardVisible) { - resolve(); - return; - } - - const subscription = Keyboard.addListener('keyboardDidHide', () => { - resolve(); - subscription.remove(); - }); - - Keyboard.dismiss(); - }); -}; - -// Initialize listeners for native and web -initializeNativeKeyboardListeners(); -initializeWebKeyboardListeners(); - -const utils = {dismiss}; - -export default utils; diff --git a/src/utils/keyboard/index.ts b/src/utils/keyboard/index.ts new file mode 100644 index 000000000000..a2b1d329aa0a --- /dev/null +++ b/src/utils/keyboard/index.ts @@ -0,0 +1,32 @@ +import {Keyboard} from 'react-native'; + +let isVisible = false; + +Keyboard.addListener('keyboardDidHide', () => { + isVisible = false; +}); + +Keyboard.addListener('keyboardDidShow', () => { + isVisible = true; +}); + +const dismiss = (): Promise => { + return new Promise((resolve) => { + if (!isVisible) { + resolve(); + + return; + } + + const subscription = Keyboard.addListener('keyboardDidHide', () => { + resolve(undefined); + subscription.remove(); + }); + + Keyboard.dismiss(); + }); +}; + +const utils = {dismiss}; + +export default utils; diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts new file mode 100644 index 000000000000..de7e89b9d660 --- /dev/null +++ b/src/utils/keyboard/index.website.ts @@ -0,0 +1,46 @@ +import {InteractionManager, Keyboard} from 'react-native'; + +let isVisible = false; + +const isKeyboardInput = (elem: HTMLElement): boolean => { + const inputTypesToIgnore = ['button', 'submit', 'checkbox', 'file', 'image']; + return (elem.tagName === 'INPUT' && !inputTypesToIgnore.includes((elem as HTMLInputElement).type)) || elem.tagName === 'TEXTAREA' || elem.hasAttribute('contenteditable'); +}; + +const handleFocusIn = (event: FocusEvent): void => { + const target = event.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isVisible = true; + } +}; + +const handleFocusOut = (event: FocusEvent): void => { + const target = event.target as HTMLElement; + if (target && isKeyboardInput(target)) { + isVisible = false; + } +}; + +document.addEventListener('focusin', handleFocusIn); +document.addEventListener('focusout', handleFocusOut); + +const dismiss = (): Promise => { + return new Promise((resolve) => { + if (!isVisible) { + resolve(); + return; + } + + Keyboard.dismiss(); + InteractionManager.runAfterInteractions(() => { + isVisible = false; + resolve(); + }); + }); +}; + +const utils = { + dismiss, +}; + +export default utils; From de76e7ecd4b5beee66119cde9e1d314a4138c09e Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 6 Dec 2024 16:04:58 +0700 Subject: [PATCH 005/110] fix: Navigation bar shows purple color in offline mode --- src/components/ScreenWrapper.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 09315bfb8a8e..7d87e338b738 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -8,7 +8,6 @@ import type {EdgeInsets} from 'react-native-safe-area-context'; import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useKeyboardState from '@hooks/useKeyboardState'; -import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useTackInputFocus from '@hooks/useTackInputFocus'; @@ -109,7 +108,7 @@ type ScreenWrapperProps = { type ScreenWrapperStatusContextType = { didScreenTransitionEnd: boolean; isSafeAreaTopPaddingApplied: boolean; - isSafeAreaBottomPaddingApplied: boolean; + includeSafeAreaPaddingBottom: boolean; }; const ScreenWrapperStatusContext = createContext(undefined); @@ -160,7 +159,6 @@ function ScreenWrapper( const styles = useThemeStyles(); const keyboardState = useKeyboardState(); const {isDevelopment} = useEnvironment(); - const {isOffline} = useNetwork(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; @@ -252,18 +250,17 @@ function ScreenWrapper( } // We always need the safe area padding bottom if we're showing the offline indicator since it is bottom-docked. - const isSafeAreaBottomPaddingApplied = includeSafeAreaPaddingBottom || (isOffline && shouldShowOfflineIndicator); - if (isSafeAreaBottomPaddingApplied) { + if (includeSafeAreaPaddingBottom) { paddingStyle.paddingBottom = paddingBottom; } - if (isSafeAreaBottomPaddingApplied && ignoreInsetsConsumption) { + if (includeSafeAreaPaddingBottom && ignoreInsetsConsumption) { paddingStyle.paddingBottom = unmodifiedPaddings.bottom; } const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileWebKit()); const contextValue = useMemo( - () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied}), - [didScreenTransitionEnd, isSafeAreaBottomPaddingApplied, isSafeAreaTopPaddingApplied], + () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, includeSafeAreaPaddingBottom}), + [didScreenTransitionEnd, includeSafeAreaPaddingBottom, isSafeAreaTopPaddingApplied], ); return ( @@ -305,7 +302,10 @@ function ScreenWrapper( } {isSmallScreenWidth && shouldShowOfflineIndicator && ( <> - + {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} From b545bac388de599554f0fdb3e7527edf7cd14b38 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 6 Dec 2024 16:17:11 +0700 Subject: [PATCH 006/110] fix typecheck --- src/components/ScreenWrapper.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 7d87e338b738..76efb954b64d 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -108,7 +108,7 @@ type ScreenWrapperProps = { type ScreenWrapperStatusContextType = { didScreenTransitionEnd: boolean; isSafeAreaTopPaddingApplied: boolean; - includeSafeAreaPaddingBottom: boolean; + isSafeAreaBottomPaddingApplied: boolean; }; const ScreenWrapperStatusContext = createContext(undefined); @@ -259,7 +259,7 @@ function ScreenWrapper( const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && Browser.isMobileWebKit()); const contextValue = useMemo( - () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, includeSafeAreaPaddingBottom}), + () => ({didScreenTransitionEnd, isSafeAreaTopPaddingApplied, isSafeAreaBottomPaddingApplied: includeSafeAreaPaddingBottom}), [didScreenTransitionEnd, includeSafeAreaPaddingBottom, isSafeAreaTopPaddingApplied], ); @@ -304,7 +304,11 @@ function ScreenWrapper( <> {/* Since import state is tightly coupled to the offline state, it is safe to display it when showing offline indicator */} From c01e82cd6e016fa234afcddf4aa4dd36bea40710 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:08:54 +0700 Subject: [PATCH 007/110] Specify dismiss keyboard behavior for Android native and Safari on mobile web --- .../step/IOURequestStepParticipants.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 4552e94b3b5d..c0f90dea0afd 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -5,7 +5,9 @@ import FormHelpMessage from '@components/FormHelpMessage'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {READ_COMMANDS} from '@libs/API/types'; +import * as Browser from '@libs/Browser'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; +import getPlatform from '@libs/getPlatform'; import HttpUtils from '@libs/HttpUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -15,6 +17,7 @@ import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyRequestPar import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {Participant} from '@src/types/onyx/IOU'; @@ -70,6 +73,8 @@ function IOURequestStepParticipants({ const receiptFilename = transaction?.filename; const receiptPath = transaction?.receipt?.source; const receiptType = transaction?.receipt?.type; + const isAndroidNative = getPlatform() === CONST.PLATFORM.ANDROID; + const isMobileSafari = Browser.isMobileSafari(); // When the component mounts, if there is a receipt, see if the image can be read from the disk. If not, redirect the user to the starting step of the flow. // This is because until the expense is saved, the receipt file is only stored in the browsers memory as a blob:// and if the browser is refreshed, then @@ -107,6 +112,19 @@ function IOURequestStepParticipants({ [iouType, reportID, transactionID], ); + const handleNavigation = useCallback( + (route: Route) => { + if (isAndroidNative || isMobileSafari) { + KeyboardUtils.dismiss().then(() => { + Navigation.navigate(route); + }); + } else { + Navigation.navigate(route); + } + }, + [isAndroidNative, isMobileSafari], + ); + const goToNextStep = useCallback(() => { const isCategorizing = action === CONST.IOU.ACTION.CATEGORIZE; const isShareAction = action === CONST.IOU.ACTION.SHARE; @@ -137,10 +155,8 @@ function IOURequestStepParticipants({ ? ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, iouType, transactionID, selectedReportID.current || reportID, iouConfirmationPageRoute) : iouConfirmationPageRoute; - KeyboardUtils.dismiss().then(() => { - Navigation.navigate(route); - }); - }, [iouType, transactionID, transaction, reportID, action, participants]); + handleNavigation(route); + }, [action, participants, iouType, transaction, transactionID, reportID, handleNavigation]); const navigateBack = useCallback(() => { IOUUtils.navigateToStartMoneyRequestStep(iouRequestType, iouType, transactionID, reportID, action); @@ -157,9 +173,8 @@ function IOURequestStepParticipants({ IOU.setCustomUnitRateID(transactionID, rateID); IOU.setMoneyRequestParticipantsFromReport(transactionID, ReportUtils.getReport(selfDMReportID)); const iouConfirmationPageRoute = ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, CONST.IOU.TYPE.TRACK, transactionID, selfDMReportID); - KeyboardUtils.dismiss().then(() => { - Navigation.navigate(iouConfirmationPageRoute); - }); + + handleNavigation(iouConfirmationPageRoute); }; useEffect(() => { From 219090eb39a9bbb157ffa21cf5e304a4e8e85c96 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 9 Dec 2024 13:54:39 +0700 Subject: [PATCH 008/110] add shouldReportBeInOptionList test --- tests/unit/ReportUtilsTest.ts | 166 +++++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index dc752ae73b1c..7ee447ec088d 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -4,9 +4,10 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import DateUtils from '@libs/DateUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; +import type {Beta, PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as NumberUtils from '../../src/libs/NumberUtils'; import * as LHNTestUtils from '../utils/LHNTestUtils'; @@ -1179,5 +1180,168 @@ describe('ReportUtils', () => { expect(ReportUtils.getGroupChatName(undefined, false, report)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); }); }); + + describe('shouldReportBeInOptionList tests', () => { + afterEach(() => Onyx.clear()); + + it('should return false when the report is marked as hidden', () => { + const report: Report = { + ...LHNTestUtils.getFakeReport(), + participants: { + '1': { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + }; + const currentReportId = ''; + const isInFocusMode = true; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeFalsy(); + }); + + it('should return false when the report does not have participants', () => { + const report: Report = { + ...LHNTestUtils.getFakeReport(), + participants: {}, + }; + const currentReportId = ''; + const isInFocusMode = true; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeFalsy(); + }); + + it('should return false when the report is the report that the user cannot access due to policy restrictions', () => { + const report: Report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.DOMAIN_ALL, + }; + const currentReportId = ''; + const isInFocusMode = false; + const betas: Beta[] = []; + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeFalsy(); + }); + + it('should return false when the report is the single transaction thread', async () => { + const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD'); + const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID); + const expenseCreatedAction = ReportUtils.buildOptimisticIOUReportAction( + 'create', + 100, + 'USD', + '', + [], + expenseTransaction.transactionID, + undefined, + expenseReport.reportID, + undefined, + false, + false, + undefined, + undefined, + ); + const transactionThreadReport = ReportUtils.buildTransactionThread(expenseCreatedAction, expenseReport); + expenseCreatedAction.childReportID = transactionThreadReport.reportID; + const currentReportId = '1'; + const isInFocusMode = false; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, { + [expenseCreatedAction.reportActionID]: expenseCreatedAction, + }); + expect( + ReportUtils.shouldReportBeInOptionList({ + report: transactionThreadReport, + currentReportId, + isInFocusMode, + betas, + policies: {}, + doesReportHaveViolations: false, + excludeEmptyChats: false, + }), + ).toBeFalsy(); + }); + + it('should return false when the report is empty chat and the excludeEmptyChats setting is true', () => { + const report = LHNTestUtils.getFakeReport(); + const currentReportId = ''; + const isInFocusMode = false; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: true}), + ).toBeFalsy(); + }); + + it('should return false when the user’s email is domain-based and the includeDomainEmail is false', () => { + const report = LHNTestUtils.getFakeReport(); + const currentReportId = ''; + const isInFocusMode = false; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + expect( + ReportUtils.shouldReportBeInOptionList({ + report, + currentReportId, + isInFocusMode, + betas, + policies: {}, + doesReportHaveViolations: false, + login: '+@domain.com', + excludeEmptyChats: false, + includeDomainEmail: false, + }), + ).toBeFalsy(); + }); + + it('should return false when the report has the parent message is pending removal', async () => { + const parentReport = LHNTestUtils.getFakeReport(); + const report = LHNTestUtils.getFakeReport(); + const parentReportAction: ReportAction = { + ...LHNTestUtils.getFakeReportAction(), + message: [ + { + type: 'COMMENT', + html: 'hey', + text: 'hey', + isEdited: false, + whisperedTo: [], + isDeletedParentAction: false, + moderationDecision: { + decision: CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE, + }, + }, + ], + childReportID: report.reportID, + }; + report.parentReportID = parentReport.reportID; + report.parentReportActionID = parentReportAction.reportActionID; + const currentReportId = ''; + const isInFocusMode = false; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, parentReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`, { + [parentReportAction.reportActionID]: parentReportAction, + }); + + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeFalsy(); + }); + + it('should return false when the report is read and we are in the focus mode', () => { + const report = LHNTestUtils.getFakeReport(); + const currentReportId = ''; + const isInFocusMode = true; + const betas = [CONST.BETAS.DEFAULT_ROOMS]; + expect( + ReportUtils.shouldReportBeInOptionList({report, currentReportId, isInFocusMode, betas, policies: {}, doesReportHaveViolations: false, excludeEmptyChats: false}), + ).toBeFalsy(); + }); + }); }); }); From e6002f55e7df8d6f7e3555de8251de1e35a0887c Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 9 Dec 2024 14:01:01 +0700 Subject: [PATCH 009/110] add ui test for hidden report --- tests/ui/LHNItemsPresence.tsx | 23 ++++++++++++++++++++++- tests/unit/ReportUtilsTest.ts | 5 +---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 6693c90adaa0..45eaf46b819d 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -5,7 +5,7 @@ import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentU import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList} from '@src/types/onyx'; +import type {PersonalDetailsList, Report} from '@src/types/onyx'; import type {ReportCollectionDataSet} from '@src/types/onyx/Report'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import * as TestHelper from '../utils/TestHelper'; @@ -204,5 +204,26 @@ describe('SidebarLinksData', () => { // Then the empty report should not appear in the sidebar. expect(getOptionRows()).toHaveLength(0); }); + + it('should not display the report marked as hidden', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const report: Report = { + ...LHNTestUtils.getFakeReport(), + participants: { + [TEST_USER_ACCOUNT_ID]: { + notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + }, + }, + }; + + // And a report marked as hidden is initialized in Onyx + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + }); + + // Then hidden report should not appear in the sidebar. + expect(getOptionRows()).toHaveLength(0); + }); }); }); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 7ee447ec088d..92adb79daf6f 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -1202,10 +1202,7 @@ describe('ReportUtils', () => { }); it('should return false when the report does not have participants', () => { - const report: Report = { - ...LHNTestUtils.getFakeReport(), - participants: {}, - }; + const report = LHNTestUtils.getFakeReport([]); const currentReportId = ''; const isInFocusMode = true; const betas = [CONST.BETAS.DEFAULT_ROOMS]; From 18596e8470b5815fc7588a7f996f794ec4689f70 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 9 Dec 2024 14:09:42 +0700 Subject: [PATCH 010/110] add ui test for the default room --- tests/ui/LHNItemsPresence.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 45eaf46b819d..9911bbeea9d9 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -225,5 +225,28 @@ describe('SidebarLinksData', () => { // Then hidden report should not appear in the sidebar. expect(getOptionRows()).toHaveLength(0); }); + + it('should not display the report the user cannot access due to policy restrictions', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const report: Report = { + ...LHNTestUtils.getFakeReport(), + chatType: CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, + }; + + // An admin room is initialized in Onyx + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + }); + + // The admin room should appear in the sidebar. + expect(getOptionRows()).toHaveLength(1); + + // When the defaultRooms beta is removed + await Onyx.merge(ONYXKEYS.BETAS, []); + + // The admin room should not appear in the sidebar. + expect(getOptionRows()).toHaveLength(0); + }); }); }); From 303544c817543bbf81bcd233edc4677303106a7b Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 9 Dec 2024 14:14:09 +0700 Subject: [PATCH 011/110] fix default room ui test --- tests/ui/LHNItemsPresence.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 9911bbeea9d9..099555d0436b 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -231,21 +231,22 @@ describe('SidebarLinksData', () => { LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { ...LHNTestUtils.getFakeReport(), - chatType: CONST.REPORT.CHAT_TYPE.POLICY_ADMINS, + chatType: CONST.REPORT.CHAT_TYPE.DOMAIN_ALL, + lastMessageText: 'fake last message', }; - // An admin room is initialized in Onyx + // A default room is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); - // The admin room should appear in the sidebar. + // The default room should appear in the sidebar. expect(getOptionRows()).toHaveLength(1); // When the defaultRooms beta is removed await Onyx.merge(ONYXKEYS.BETAS, []); - // The admin room should not appear in the sidebar. + // The default room should not appear in the sidebar. expect(getOptionRows()).toHaveLength(0); }); }); From 4450fb7557584028676315583f346d81e5a0170d Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 9 Dec 2024 14:20:44 +0700 Subject: [PATCH 012/110] add ui test for single transaction thread --- tests/ui/LHNItemsPresence.tsx | 45 +++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 099555d0436b..7a53a52cb831 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -12,6 +12,8 @@ import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; // Be sure to include the mocked permissions library, as some components that are rendered // during the test depend on its methods. @@ -240,14 +242,49 @@ describe('SidebarLinksData', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); - // The default room should appear in the sidebar. - expect(getOptionRows()).toHaveLength(1); - - // When the defaultRooms beta is removed + // And the defaultRooms beta is removed await Onyx.merge(ONYXKEYS.BETAS, []); // The default room should not appear in the sidebar. expect(getOptionRows()).toHaveLength(0); }); + + it('should not display the single transaction thread', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD'); + const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID); + const expenseCreatedAction = ReportUtils.buildOptimisticIOUReportAction( + 'create', + 100, + 'USD', + '', + [], + expenseTransaction.transactionID, + undefined, + expenseReport.reportID, + undefined, + false, + false, + undefined, + undefined, + ); + const transactionThreadReport = ReportUtils.buildTransactionThread(expenseCreatedAction, expenseReport); + expenseCreatedAction.childReportID = transactionThreadReport.reportID; + + // A single transaction thread is initialized in Onyx + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`]: transactionThreadReport, + }); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, { + [expenseCreatedAction.reportActionID]: expenseCreatedAction, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${expenseTransaction.transactionID}`, expenseTransaction); + + // This report should not appear in the sidebar. + expect(getOptionRows()).toHaveLength(0); + }); }); }); From 76ff4a2e843d497d23bf35a4b0bf9aec5e7c267a Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 9 Dec 2024 14:27:57 +0700 Subject: [PATCH 013/110] add ui test for the report with parent message pending removal --- tests/ui/LHNItemsPresence.tsx | 44 ++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 7a53a52cb831..792463f8c5e8 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -3,17 +3,17 @@ import type {ComponentType} from 'react'; import Onyx from 'react-native-onyx'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import * as Localize from '@libs/Localize'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, Report} from '@src/types/onyx'; +import type {PersonalDetailsList, Report, ReportAction} from '@src/types/onyx'; import type {ReportCollectionDataSet} from '@src/types/onyx/Report'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; // Be sure to include the mocked permissions library, as some components that are rendered // during the test depend on its methods. @@ -286,5 +286,43 @@ describe('SidebarLinksData', () => { // This report should not appear in the sidebar. expect(getOptionRows()).toHaveLength(0); }); + + it('should not display the report with parent message is pending removal', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const parentReport = LHNTestUtils.getFakeReport(); + const report = LHNTestUtils.getFakeReport(); + const parentReportAction: ReportAction = { + ...LHNTestUtils.getFakeReportAction(), + message: [ + { + type: 'COMMENT', + html: 'hey', + text: 'hey', + isEdited: false, + whisperedTo: [], + isDeletedParentAction: false, + moderationDecision: { + decision: CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE, + }, + }, + ], + childReportID: report.reportID, + }; + report.parentReportID = parentReport.reportID; + report.parentReportActionID = parentReportAction.reportActionID; + + // And a report with parent message is pending removal is initialized in Onyx + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, parentReport); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReport.reportID}`, { + [parentReportAction.reportActionID]: parentReportAction, + }); + + // This report should not appear in the sidebar. + expect(getOptionRows()).toHaveLength(0); + }); }); }); From 0dc095ef87939b0acb458f717f2121f5d48ec474 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 9 Dec 2024 14:38:58 +0700 Subject: [PATCH 014/110] add ui test for read report in the focus mode --- tests/ui/LHNItemsPresence.tsx | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 792463f8c5e8..cb4e6a92d0c8 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -211,7 +211,7 @@ describe('SidebarLinksData', () => { // When the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { - ...LHNTestUtils.getFakeReport(), + ...createReport(), participants: { [TEST_USER_ACCOUNT_ID]: { notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, @@ -232,7 +232,7 @@ describe('SidebarLinksData', () => { // When the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { - ...LHNTestUtils.getFakeReport(), + ...createReport(), chatType: CONST.REPORT.CHAT_TYPE.DOMAIN_ALL, lastMessageText: 'fake last message', }; @@ -290,8 +290,8 @@ describe('SidebarLinksData', () => { it('should not display the report with parent message is pending removal', async () => { // When the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); - const parentReport = LHNTestUtils.getFakeReport(); - const report = LHNTestUtils.getFakeReport(); + const parentReport = createReport(); + const report = createReport(); const parentReportAction: ReportAction = { ...LHNTestUtils.getFakeReportAction(), message: [ @@ -324,5 +324,29 @@ describe('SidebarLinksData', () => { // This report should not appear in the sidebar. expect(getOptionRows()).toHaveLength(0); }); + + it('should not display the read report in the focus mode', async () => { + // When the SidebarLinks are rendered. + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const report = createReport(); + const reportAction = LHNTestUtils.getFakeReportAction(); + + // And a read report with a message is initialized in Onyx + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { + [reportAction.reportActionID]: reportAction, + }); + + // This report should appear in the sidebar. + expect(getOptionRows()).toHaveLength(1); + + // When we are in the focus mode + await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); + + // This report should not appear in the sidebar. + expect(getOptionRows()).toHaveLength(0); + }); }); }); From 80877200debdbc3fc39af08c645218fdd12d5df5 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 9 Dec 2024 15:23:43 +0700 Subject: [PATCH 015/110] fix read report ui test --- tests/ui/LHNItemsPresence.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index cb4e6a92d0c8..7918dec3acb3 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -329,20 +329,13 @@ describe('SidebarLinksData', () => { // When the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report = createReport(); - const reportAction = LHNTestUtils.getFakeReportAction(); - // And a read report with a message is initialized in Onyx + // A read report is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { - [reportAction.reportActionID]: reportAction, - }); - - // This report should appear in the sidebar. - expect(getOptionRows()).toHaveLength(1); - // When we are in the focus mode + // And we are in the focus mode await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); // This report should not appear in the sidebar. From 75ef6bf1bc11c32e9c528ab3a196efc8764673d1 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:39:59 +0700 Subject: [PATCH 016/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 27d3bcc46f45..60efb4a7c0b5 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -390,7 +390,7 @@ describe('SidebarLinksData', () => { lastMessageText: 'fake last message', }; - // A default room is initialized in Onyx + // And a default room is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); From 42603f6909aeaadd22f0b5453203621e80c510f5 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:40:09 +0700 Subject: [PATCH 017/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 60efb4a7c0b5..84572ac65d3f 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -382,7 +382,7 @@ describe('SidebarLinksData', () => { }); it('should not display the report the user cannot access due to policy restrictions', async () => { - // When the SidebarLinks are rendered. + // When the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { ...createReport(), From 9f755dff0408c0a6a48d7ff5e936db7937c49182 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:40:15 +0700 Subject: [PATCH 018/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 84572ac65d3f..d50a1f42244c 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -361,7 +361,7 @@ describe('SidebarLinksData', () => { }); it('should not display the report marked as hidden', async () => { - // When the SidebarLinks are rendered. + // When the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { ...createReport(), From bb354fb56e8776195c90d357254747badd301c4d Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:40:24 +0700 Subject: [PATCH 019/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index d50a1f42244c..d59a27387ed9 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -372,7 +372,7 @@ describe('SidebarLinksData', () => { }, }; - // And a report marked as hidden is initialized in Onyx + // And a report with notification preference set as hidden is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); From 44423febda46a090a361a39c7ff39a1c6a6d1e17 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:40:31 +0700 Subject: [PATCH 020/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index d59a27387ed9..830230558430 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -474,7 +474,7 @@ describe('SidebarLinksData', () => { [parentReportAction.reportActionID]: parentReportAction, }); - // This report should not appear in the sidebar. + // This report should not appear in the sidebar until the moderation feature decides if the message should be removed expect(getOptionRows()).toHaveLength(0); }); From 6bfd5014d609b814460c58a94f08a69b04364811 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:40:40 +0700 Subject: [PATCH 021/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 830230558430..82e3d2de42ef 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -491,7 +491,7 @@ describe('SidebarLinksData', () => { // And we are in the focus mode await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); - // This report should not appear in the sidebar. + // Then this report should not appear in the sidebar. expect(getOptionRows()).toHaveLength(0); }); }); From 0f5a9ad6f5a4c82dba6e9b19e21234cb5cef4e1d Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:40:48 +0700 Subject: [PATCH 022/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 82e3d2de42ef..0529feb90a2d 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -483,7 +483,7 @@ describe('SidebarLinksData', () => { LHNTestUtils.getDefaultRenderedSidebarLinks(); const report = createReport(); - // A read report is initialized in Onyx + // And a read report is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); From 1390f9701764f84ca987ff553f8c9bbd89310bdb Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:41:05 +0700 Subject: [PATCH 023/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 0529feb90a2d..f7e56f0888f6 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -479,7 +479,7 @@ describe('SidebarLinksData', () => { }); it('should not display the read report in the focus mode', async () => { - // When the SidebarLinks are rendered. + // When the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const report = createReport(); From 0949558f77c20fca26616c383d98f35383d3e2e4 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:41:15 +0700 Subject: [PATCH 024/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index f7e56f0888f6..eaccd8849e00 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -403,7 +403,7 @@ describe('SidebarLinksData', () => { }); it('should not display the single transaction thread', async () => { - // When the SidebarLinks are rendered. + // When the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD'); const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID); From 264fe1962f8d7fcff9063d7ba71cb884af29ba1f Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:41:27 +0700 Subject: [PATCH 025/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index eaccd8849e00..a74dbe4bf1ab 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -436,7 +436,7 @@ describe('SidebarLinksData', () => { }); await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${expenseTransaction.transactionID}`, expenseTransaction); - // This report should not appear in the sidebar. + // Then such report should not appear in the sidebar because the highest level context is on the workspace chat with GBR that is visible in the LHN expect(getOptionRows()).toHaveLength(0); }); From cdc867c9a3a902401190487bade10770c004459d Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:41:37 +0700 Subject: [PATCH 026/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index a74dbe4bf1ab..3ac6a1303182 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -441,7 +441,7 @@ describe('SidebarLinksData', () => { }); it('should not display the report with parent message is pending removal', async () => { - // When the SidebarLinks are rendered. + // When the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const parentReport = createReport(); const report = createReport(); From 19147a1a01003c48370b016a945d2cbfbf892ffc Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:49:30 +0700 Subject: [PATCH 027/110] add a test case for empty notification --- tests/ui/LHNItemsPresence.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 3ac6a1303182..93c80ebf92be 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -381,6 +381,20 @@ describe('SidebarLinksData', () => { expect(getOptionRows()).toHaveLength(0); }); + it('should not display the report has empty notification preference', async () => { + // When the SidebarLinks are rendered + LHNTestUtils.getDefaultRenderedSidebarLinks(); + const report = createReport(false, [2]); + + // And a report with empty notification preference is initialized in Onyx + await initializeState({ + [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + }); + + // Then the report should not appear in the sidebar. + expect(getOptionRows()).toHaveLength(0); + }); + it('should not display the report the user cannot access due to policy restrictions', async () => { // When the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); From 5446ec90443957b70ef9fc20913ea842f57a05e7 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 15:59:56 +0700 Subject: [PATCH 028/110] fix read report test --- tests/ui/LHNItemsPresence.tsx | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 93c80ebf92be..87fbdfe794fd 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -495,17 +495,31 @@ describe('SidebarLinksData', () => { it('should not display the read report in the focus mode', async () => { // When the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); - const report = createReport(); + const report: Report = { + ...createReport(), + lastMessageText: 'fake last message', + lastActorAccountID: TEST_USER_ACCOUNT_ID, + }; - // And a read report is initialized in Onyx + // And a read report that isn't empty is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); - // And we are in the focus mode + await waitForBatchedUpdatesWithAct(); + + // And the user is in default mode + await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.DEFAULT); + + // Then the report should appear in the sidebar + expect(getOptionRows()).toHaveLength(1); + + await waitForBatchedUpdatesWithAct(); + + // When the user is in focus mode await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); - // Then this report should not appear in the sidebar. + // The report should not disappear in the sidebar because it's read expect(getOptionRows()).toHaveLength(0); }); }); From 8bbf6fe1d294a3b9551ad5dbbc47d911b20749c5 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 11 Dec 2024 22:25:35 +0700 Subject: [PATCH 029/110] Update tests/ui/LHNItemsPresence.tsx Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/ui/LHNItemsPresence.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 87fbdfe794fd..349d7afa272f 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -488,7 +488,7 @@ describe('SidebarLinksData', () => { [parentReportAction.reportActionID]: parentReportAction, }); - // This report should not appear in the sidebar until the moderation feature decides if the message should be removed + // Then report should not appear in the sidebar until the moderation feature decides if the message should be removed expect(getOptionRows()).toHaveLength(0); }); From eb1095db931d87b3ae34669c4aa3f4d0072a30ce Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Thu, 12 Dec 2024 15:25:37 +0700 Subject: [PATCH 030/110] update comment with when-and-then format --- tests/ui/LHNItemsPresence.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 349d7afa272f..67ad9ff0e816 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -211,7 +211,7 @@ describe('SidebarLinksData', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); - // The report should appear in the sidebar because it’s pinned. + // Then the report should appear in the sidebar because it’s pinned. expect(getOptionRows()).toHaveLength(1); await waitForBatchedUpdatesWithAct(); @@ -276,7 +276,7 @@ describe('SidebarLinksData', () => { await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.DEFAULT); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${archivedReport.reportID}`, reportNameValuePairs); - // The report should appear in the sidebar because it's archived + // Then the report should appear in the sidebar because it's archived expect(getOptionRows()).toHaveLength(1); }); @@ -290,7 +290,7 @@ describe('SidebarLinksData', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); - // The selfDM report should appear in the sidebar by default + // Then the selfDM report should appear in the sidebar by default expect(getOptionRows()).toHaveLength(1); }); @@ -312,7 +312,7 @@ describe('SidebarLinksData', () => { // And the user is in focus mode await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); - // The report should appear in the sidebar because it's unread + // Then the report should appear in the sidebar because it's unread expect(getOptionRows()).toHaveLength(1); // And the text is bold @@ -412,7 +412,7 @@ describe('SidebarLinksData', () => { // And the defaultRooms beta is removed await Onyx.merge(ONYXKEYS.BETAS, []); - // The default room should not appear in the sidebar. + // Then the default room should not appear in the sidebar. expect(getOptionRows()).toHaveLength(0); }); @@ -439,7 +439,7 @@ describe('SidebarLinksData', () => { const transactionThreadReport = ReportUtils.buildTransactionThread(expenseCreatedAction, expenseReport); expenseCreatedAction.childReportID = transactionThreadReport.reportID; - // A single transaction thread is initialized in Onyx + // And a single transaction thread is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`]: transactionThreadReport, }); From 0c4798f3568244e747ee7593ec0b950df74c586d Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Fri, 13 Dec 2024 00:25:18 +0700 Subject: [PATCH 031/110] fix: blank receipt holder when delete expense offline --- .../MoneyRequestPreviewContent.tsx | 14 ++++++++------ src/components/ReportActionItem/ReportPreview.tsx | 15 +++++++++------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 44e3b7488ba3..ec09bcd4bf00 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -348,12 +348,14 @@ function MoneyRequestPreviewContent({ !onPreviewPressed ? [styles.moneyRequestPreviewBox, containerStyles] : {}, ]} > - + {!isDeleted && ( + + )} {isEmptyObject(transaction) && !ReportActionsUtils.isMessageDeleted(action) && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? ( ) : ( diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index badab47b1c35..8f995b24cbe3 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -175,6 +175,7 @@ function ReportPreview({ ReportUtils.hasActionsWithErrors(iouReportID); const lastThreeTransactions = allTransactions.slice(-3); const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction})); + console.log(allTransactions); const showRTERViolationMessage = numberOfRequests === 1 && TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID ?? '-1', transactionViolations)); @@ -472,12 +473,14 @@ function ReportPreview({ accessibilityLabel={translate('iou.viewDetails')} > - + {lastThreeReceipts.length > 0 && ( + + )} From 7cf9e223d240396b5e4feac0eb149a6d8277dc8a Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Fri, 13 Dec 2024 00:33:39 +0700 Subject: [PATCH 032/110] fix lint --- src/components/ReportActionItem/ReportPreview.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 8f995b24cbe3..84634ca81b89 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -175,7 +175,6 @@ function ReportPreview({ ReportUtils.hasActionsWithErrors(iouReportID); const lastThreeTransactions = allTransactions.slice(-3); const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction})); - console.log(allTransactions); const showRTERViolationMessage = numberOfRequests === 1 && TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID ?? '-1', transactionViolations)); From bac130218b927a80510cacfb7351e3002435b371 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 12:53:19 -0800 Subject: [PATCH 033/110] Failing test only returns invoice chat when receiver type matches --- tests/unit/ReportUtilsTest.ts | 75 ++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index e1eda3171355..83f0bee3ca20 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {addDays, format as formatDate} from 'date-fns'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import DateUtils from '@libs/DateUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -1414,4 +1414,77 @@ describe('ReportUtils', () => { ).toBeTruthy(); }); }); + + describe('getInvoiceChatByParticipants', () => { + it('only returns an invoice chat if the receiver type matches', () => { + // Given an invoice chat that has been converted from an individual to policy receiver type + const convertedInvoiceChat = { + chatType: CONST.REPORT.CHAT_TYPE.INVOICE, + currency: 'USD', + description: '', + errorFields: null, + hasOutstandingChildRequest: false, + hasOutstandingChildTask: false, + invoiceReceiver: { + accountID: 33, + policyID: '5F2F82F98C848CAA', + type: 'policy', + }, + isCancelledIOU: false, + isOwnPolicyExpenseChat: false, + isPinned: false, + isWaitingOnBankAccount: false, + lastActionType: 'REPORTPREVIEW', + lastActorAccountID: '32', + lastMessageHtml: 'paid $1.00', + lastMessageText: 'paid $1.00', + lastMessageTranslationKey: '', + lastReadSequenceNumber: 0, + lastReadTime: '2024-12-13 19:45:28.942', + lastVisibleActionCreated: '2024-12-13 19:19:01.794', + lastVisibleActionLastModified: '2024-12-13 19:19:01.794', + managerID: 0, + nonReimbursableTotal: 0, + oldPolicyName: '', + ownerAccountID: 0, + participants: { + '32': { + notificationPreference: 'always', + role: 'admin', + }, + '33': { + notificationPreference: 'always', + permissions: ['read', 'write', 'share', 'own'], + }, + }, + policyAvatar: '', + policyID: 'CC048FA711B35B1F', + policyName: "53019's Workspace", + private_isArchived: '', + reportID: '7605647250932303', + reportName: 'Chat Report', + state: 'OPEN', + stateNum: 0, + statusNum: 0, + total: 0, + type: 'chat', + unheldNonReimbursableTotal: 0, + unheldTotal: 0, + visibility: 'private', + welcomeMessage: '', + writeCapability: 'all', + }; + + // Get an Onyx collection of reports + const reports: OnyxCollection = { + [convertedInvoiceChat.reportID]: convertedInvoiceChat, + }; + + // When we send another invoice to the individual from global create and call getInvoiceChatByParticipants + const invoiceChatReport = ReportUtils.getInvoiceChatByParticipants(convertedInvoiceChat.policyID, 33, reports); + + // Then no invoice chat should be returned because the receiver type does not match + expect(invoiceChatReport).toBeUndefined(); + }); + }); }); From 59d79fa2ad9b480432498c0e751d56864c08b272 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 13:03:33 -0800 Subject: [PATCH 034/110] Fix test, require invoice receiver to match --- src/libs/ReportUtils.ts | 5 +++-- src/types/onyx/Report.ts | 5 ++++- tests/unit/ReportUtilsTest.ts | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0b5d03e9e365..25a8055bc747 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -52,7 +52,7 @@ import type {ErrorFields, Errors, Icon, PendingAction} from '@src/types/onyx/Ony import type {OriginalMessageChangeLog, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {ConnectionName} from '@src/types/onyx/Policy'; -import type {NotificationPreference, Participants, PendingChatMember, Participant as ReportParticipant} from '@src/types/onyx/Report'; +import type {InvoiceReceiverType, NotificationPreference, Participants, PendingChatMember, Participant as ReportParticipant} from '@src/types/onyx/Report'; import type {Message, OldDotReportAction, ReportActions} from '@src/types/onyx/ReportAction'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import type {Comment, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; @@ -6746,7 +6746,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec /** * Attempts to find an invoice chat report in onyx with the provided policyID and receiverID. */ -function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, reports: OnyxCollection = allReports): OnyxEntry { +function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, receiverType: InvoiceReceiverType, reports: OnyxCollection = allReports): OnyxEntry { return Object.values(reports ?? {}).find((report) => { if (!report || !isInvoiceRoom(report) || isArchivedRoom(report)) { return false; @@ -6754,6 +6754,7 @@ function getInvoiceChatByParticipants(policyID: string, receiverID: string | num const isSameReceiver = report.invoiceReceiver && + report.invoiceReceiver.type === receiverType && (('accountID' in report.invoiceReceiver && report.invoiceReceiver.accountID === receiverID) || ('policyID' in report.invoiceReceiver && report.invoiceReceiver.policyID === receiverID)); diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index b89b6c8e0777..7f652904f0cb 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -61,6 +61,9 @@ type InvoiceReceiver = policyID: string; }; +/** Type of invoice receiver */ +type InvoiceReceiverType = InvoiceReceiver['type']; + /** Record of report participants, indexed by their accountID */ type Participants = Record; @@ -244,4 +247,4 @@ type ReportCollectionDataSet = CollectionDataSet { }; // When we send another invoice to the individual from global create and call getInvoiceChatByParticipants - const invoiceChatReport = ReportUtils.getInvoiceChatByParticipants(convertedInvoiceChat.policyID, 33, reports); + const invoiceChatReport = ReportUtils.getInvoiceChatByParticipants(convertedInvoiceChat.policyID, 33, CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL, reports); // Then no invoice chat should be returned because the receiver type does not match expect(invoiceChatReport).toBeUndefined(); From 0afdca3337ddc95690f406a7c38ea14ccbd13ada Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 13:27:58 -0800 Subject: [PATCH 035/110] WIP test getSendMoneyInvoiceInformation --- tests/actions/IOUTest.ts | 229 ++++++++++++++++++++++++++++++++++ tests/unit/ReportUtilsTest.ts | 118 +++++++++--------- 2 files changed, 289 insertions(+), 58 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 11ff821cb240..27c9f302de8a 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -21,6 +21,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/Report'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {convertedInvoiceChat} from '../unit/ReportUtilsTest'; import PusherHelper from '../utils/PusherHelper'; import type {MockFetch} from '../utils/TestHelper'; import * as TestHelper from '../utils/TestHelper'; @@ -3303,4 +3304,232 @@ describe('actions/IOU', () => { ); }); }); + + describe('getSendInvoiceInformation', () => { + // Given a convertedInvoiceReport is stored in Onyx + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${convertedInvoiceChat.reportID}`, convertedInvoiceChat); + + // And data for when a new invoice is sent to a user + const transaction: OnyxEntry = { + amount: 100, + attendees: [ + { + email: 'a1@53019.com', + login: 'a1@53019.com', + displayName: 'a1', + avatarUrl: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_9.png', + accountID: 32, + text: 'a1@53019.com', + selected: true, + reportID: '3634215302663162', + }, + ], + comment: { + customUnit: { + customUnitRateID: '_FAKE_P2P_ID_', + }, + }, + created: '2024-12-13', + currency: 'USD', + iouRequestType: 'manual', + reportID: '3634215302663162', + transactionID: '1', + isFromGlobalCreate: true, + merchant: '(none)', + splitPayerAccountIDs: [32], + shouldShowOriginalAmount: true, + participants: [ + { + accountID: 33, + login: 'b1@53019.com', + isPolicyExpenseChat: false, + reportID: '', + selected: true, + iouType: 'invoice', + }, + { + policyID: 'CC048FA711B35B1F', + isSender: true, + selected: false, + iouType: 'invoice', + }, + ], + tag: '', + category: '', + billable: false, + }; + const currentUserAccountID = 32; + const policy: OnyxEntry = { + id: 'CC048FA711B35B1F', + type: 'team', + name: "53019's Workspace", + role: 'admin', + owner: 'a1@53019.com', + ownerAccountID: 32, + isPolicyExpenseChatEnabled: true, + outputCurrency: 'USD', + autoReporting: true, + autoReportingFrequency: 'instant', + approvalMode: 'OPTIONAL', + harvesting: { + enabled: true, + jobID: 7206965285807173000, + }, + customUnits: { + '39C3FF491F559': { + customUnitID: '39C3FF491F559', + name: 'Distance', + attributes: { + unit: 'mi', + }, + rates: { + '928A74633831E': { + customUnitRateID: '928A74633831E', + name: 'Default Rate', + rate: 67, + enabled: true, + currency: 'USD', + }, + }, + defaultCategory: 'Car', + enabled: true, + }, + }, + areCategoriesEnabled: true, + areTagsEnabled: false, + areDistanceRatesEnabled: false, + areWorkflowsEnabled: false, + areReportFieldsEnabled: false, + areConnectionsEnabled: false, + employeeList: { + 'a1@53019.com': { + role: 'admin', + errors: {}, + email: 'a1@53019.com', + forwardsTo: '', + submitsTo: 'a1@53019.com', + }, + }, + pendingFields: {}, + chatReportIDAnnounce: 0, + chatReportIDAdmins: 1811331783036078, + address: [], + approver: 'a1@53019.com', + areCompanyCardsEnabled: false, + areExpensifyCardsEnabled: false, + areInvoicesEnabled: true, + arePerDiemRatesEnabled: false, + areRulesEnabled: false, + autoReimbursement: { + limit: 0, + }, + autoReimbursementLimit: 0, + autoReportingOffset: 1, + avatarURL: '', + defaultBillable: false, + description: '', + disabledFields: { + defaultBillable: true, + reimbursable: false, + }, + fieldList: { + text_title: { + defaultValue: '{report:type} {report:startdate}', + deletable: true, + disabledOptions: [], + externalIDs: [], + fieldID: 'text_title', + isTax: false, + keys: [], + name: 'title', + orderWeight: 0, + target: 'expense', + type: 'formula', + values: [], + }, + }, + hasMultipleTagLists: false, + invoice: { + markUp: 0, + companyName: 'b1-53019', + companyWebsite: 'https://www.53019.com', + pendingFields: {}, + bankAccount: { + stripeConnectAccountBalance: 0, + stripeConnectAccountID: 'acct_1QVeO7S7tHTCCfyY', + transferBankAccountID: 29, + }, + }, + preventSelfApproval: false, + reimbursementChoice: 'reimburseManual', + requiresCategory: false, + requiresTag: false, + tax: { + trackingEnabled: false, + }, + mccGroup: { + airlines: { + category: 'Travel', + groupID: 'airlines', + }, + commuter: { + category: 'Car', + groupID: 'commuter', + }, + gas: { + category: 'Car', + groupID: 'gas', + }, + goods: { + category: 'Materials', + groupID: 'goods', + }, + groceries: { + category: 'Meals and Entertainment', + groupID: 'groceries', + }, + hotel: { + category: 'Travel', + groupID: 'hotel', + }, + mail: { + category: 'Office Supplies', + groupID: 'mail', + }, + meals: { + category: 'Meals and Entertainment', + groupID: 'meals', + }, + rental: { + category: 'Travel', + groupID: 'rental', + }, + services: { + category: 'Professional Services', + groupID: 'services', + }, + taxi: { + category: 'Travel', + groupID: 'taxi', + }, + uncategorized: { + category: 'Other', + groupID: 'uncategorized', + }, + utilities: { + category: 'Utilities', + groupID: 'utilities', + }, + }, + rules: [], + }; + const companyName = 'b1-53019'; + const companyWebsite = 'https://www.53019.com'; + + // When getSendInvoiceInformation is called + const information = IOU.getSendInvoiceInformation(transaction, currentUserAccountID, undefined, undefined, policy, undefined, undefined, companyName, companyWebsite); + + // Then a new invoice chat/room is created + expect(information?.invoiceRoom?.reportID).not.toEqual(convertedInvoiceChat.reportID); + }); }); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 6addefe96c23..918115f3756d 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -62,6 +62,63 @@ const policy: Policy = { isPolicyExpenseChatEnabled: false, }; +const convertedInvoiceChat = { + chatType: CONST.REPORT.CHAT_TYPE.INVOICE, + currency: 'USD', + description: '', + errorFields: null, + hasOutstandingChildRequest: false, + hasOutstandingChildTask: false, + invoiceReceiver: { + accountID: 33, + policyID: '5F2F82F98C848CAA', + type: 'policy', + }, + isCancelledIOU: false, + isOwnPolicyExpenseChat: false, + isPinned: false, + isWaitingOnBankAccount: false, + lastActionType: 'REPORTPREVIEW', + lastActorAccountID: '32', + lastMessageHtml: 'paid $1.00', + lastMessageText: 'paid $1.00', + lastMessageTranslationKey: '', + lastReadSequenceNumber: 0, + lastReadTime: '2024-12-13 19:45:28.942', + lastVisibleActionCreated: '2024-12-13 19:19:01.794', + lastVisibleActionLastModified: '2024-12-13 19:19:01.794', + managerID: 0, + nonReimbursableTotal: 0, + oldPolicyName: '', + ownerAccountID: 0, + participants: { + '32': { + notificationPreference: 'always', + role: 'admin', + }, + '33': { + notificationPreference: 'always', + permissions: ['read', 'write', 'share', 'own'], + }, + }, + policyAvatar: '', + policyID: 'CC048FA711B35B1F', + policyName: "53019's Workspace", + private_isArchived: '', + reportID: '7605647250932303', + reportName: 'Chat Report', + state: 'OPEN', + stateNum: 0, + statusNum: 0, + total: 0, + type: 'chat', + unheldNonReimbursableTotal: 0, + unheldTotal: 0, + visibility: 'private', + welcomeMessage: '', + writeCapability: 'all', +}; + Onyx.init({keys: ONYXKEYS}); describe('ReportUtils', () => { @@ -1418,64 +1475,6 @@ describe('ReportUtils', () => { describe('getInvoiceChatByParticipants', () => { it('only returns an invoice chat if the receiver type matches', () => { // Given an invoice chat that has been converted from an individual to policy receiver type - const convertedInvoiceChat = { - chatType: CONST.REPORT.CHAT_TYPE.INVOICE, - currency: 'USD', - description: '', - errorFields: null, - hasOutstandingChildRequest: false, - hasOutstandingChildTask: false, - invoiceReceiver: { - accountID: 33, - policyID: '5F2F82F98C848CAA', - type: 'policy', - }, - isCancelledIOU: false, - isOwnPolicyExpenseChat: false, - isPinned: false, - isWaitingOnBankAccount: false, - lastActionType: 'REPORTPREVIEW', - lastActorAccountID: '32', - lastMessageHtml: 'paid $1.00', - lastMessageText: 'paid $1.00', - lastMessageTranslationKey: '', - lastReadSequenceNumber: 0, - lastReadTime: '2024-12-13 19:45:28.942', - lastVisibleActionCreated: '2024-12-13 19:19:01.794', - lastVisibleActionLastModified: '2024-12-13 19:19:01.794', - managerID: 0, - nonReimbursableTotal: 0, - oldPolicyName: '', - ownerAccountID: 0, - participants: { - '32': { - notificationPreference: 'always', - role: 'admin', - }, - '33': { - notificationPreference: 'always', - permissions: ['read', 'write', 'share', 'own'], - }, - }, - policyAvatar: '', - policyID: 'CC048FA711B35B1F', - policyName: "53019's Workspace", - private_isArchived: '', - reportID: '7605647250932303', - reportName: 'Chat Report', - state: 'OPEN', - stateNum: 0, - statusNum: 0, - total: 0, - type: 'chat', - unheldNonReimbursableTotal: 0, - unheldTotal: 0, - visibility: 'private', - welcomeMessage: '', - writeCapability: 'all', - }; - - // Get an Onyx collection of reports const reports: OnyxCollection = { [convertedInvoiceChat.reportID]: convertedInvoiceChat, }; @@ -1488,3 +1487,6 @@ describe('ReportUtils', () => { }); }); }); + +// eslint-disable-next-line import/prefer-default-export +export {convertedInvoiceChat}; From 3054897047fa07ccd1e1cfae66fbac61c6a595e0 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 14:02:42 -0800 Subject: [PATCH 036/110] WIP test via sendInvoice --- tests/actions/IOUTest.ts | 437 ++++++++++++++++++++------------------- 1 file changed, 226 insertions(+), 211 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 27c9f302de8a..1d92855ceb77 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -9,6 +9,7 @@ import * as PolicyActions from '@src/libs/actions/Policy/Policy'; import * as Report from '@src/libs/actions/Report'; import * as ReportActions from '@src/libs/actions/ReportActions'; import * as User from '@src/libs/actions/User'; +import * as API from '@src/libs/API'; import DateUtils from '@src/libs/DateUtils'; import * as Localize from '@src/libs/Localize'; import * as NumberUtils from '@src/libs/NumberUtils'; @@ -3305,231 +3306,245 @@ describe('actions/IOU', () => { }); }); - describe('getSendInvoiceInformation', () => { - // Given a convertedInvoiceReport is stored in Onyx - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${convertedInvoiceChat.reportID}`, convertedInvoiceChat); - - // And data for when a new invoice is sent to a user - const transaction: OnyxEntry = { - amount: 100, - attendees: [ - { - email: 'a1@53019.com', - login: 'a1@53019.com', - displayName: 'a1', - avatarUrl: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_9.png', - accountID: 32, - text: 'a1@53019.com', - selected: true, - reportID: '3634215302663162', - }, - ], - comment: { - customUnit: { - customUnitRateID: '_FAKE_P2P_ID_', - }, - }, - created: '2024-12-13', - currency: 'USD', - iouRequestType: 'manual', - reportID: '3634215302663162', - transactionID: '1', - isFromGlobalCreate: true, - merchant: '(none)', - splitPayerAccountIDs: [32], - shouldShowOriginalAmount: true, - participants: [ - { - accountID: 33, - login: 'b1@53019.com', - isPolicyExpenseChat: false, - reportID: '', - selected: true, - iouType: 'invoice', - }, - { - policyID: 'CC048FA711B35B1F', - isSender: true, - selected: false, - iouType: 'invoice', + describe('sendInvoice', () => { + it('creates a new invoice chat when one has been converted from individual to business', async () => { + // Mock API.write for this test + const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); + + // Given a convertedInvoiceReport is stored in Onyx + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${convertedInvoiceChat.reportID}`, convertedInvoiceChat); + + // And data for when a new invoice is sent to a user + const transaction: OnyxEntry = { + amount: 100, + attendees: [ + { + email: 'a1@53019.com', + login: 'a1@53019.com', + displayName: 'a1', + avatarUrl: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_9.png', + accountID: 32, + text: 'a1@53019.com', + selected: true, + reportID: '3634215302663162', + }, + ], + comment: { + customUnit: { + customUnitRateID: '_FAKE_P2P_ID_', + }, }, - ], - tag: '', - category: '', - billable: false, - }; - const currentUserAccountID = 32; - const policy: OnyxEntry = { - id: 'CC048FA711B35B1F', - type: 'team', - name: "53019's Workspace", - role: 'admin', - owner: 'a1@53019.com', - ownerAccountID: 32, - isPolicyExpenseChatEnabled: true, - outputCurrency: 'USD', - autoReporting: true, - autoReportingFrequency: 'instant', - approvalMode: 'OPTIONAL', - harvesting: { - enabled: true, - jobID: 7206965285807173000, - }, - customUnits: { - '39C3FF491F559': { - customUnitID: '39C3FF491F559', - name: 'Distance', - attributes: { - unit: 'mi', + created: '2024-12-13', + currency: 'USD', + iouRequestType: 'manual', + reportID: '3634215302663162', + transactionID: '1', + isFromGlobalCreate: true, + merchant: '(none)', + splitPayerAccountIDs: [32], + shouldShowOriginalAmount: true, + participants: [ + { + accountID: 33, + login: 'b1@53019.com', + isPolicyExpenseChat: false, + reportID: '', + selected: true, + iouType: 'invoice', }, - rates: { - '928A74633831E': { - customUnitRateID: '928A74633831E', - name: 'Default Rate', - rate: 67, - enabled: true, - currency: 'USD', - }, + { + policyID: 'CC048FA711B35B1F', + isSender: true, + selected: false, + iouType: 'invoice', }, - defaultCategory: 'Car', + ], + tag: '', + category: '', + billable: false, + }; + const currentUserAccountID = 32; + const policy: OnyxEntry = { + id: 'CC048FA711B35B1F', + type: 'team', + name: "53019's Workspace", + role: 'admin', + owner: 'a1@53019.com', + ownerAccountID: 32, + isPolicyExpenseChatEnabled: true, + outputCurrency: 'USD', + autoReporting: true, + autoReportingFrequency: 'instant', + approvalMode: 'OPTIONAL', + harvesting: { enabled: true, + jobID: 7206965285807173000, }, - }, - areCategoriesEnabled: true, - areTagsEnabled: false, - areDistanceRatesEnabled: false, - areWorkflowsEnabled: false, - areReportFieldsEnabled: false, - areConnectionsEnabled: false, - employeeList: { - 'a1@53019.com': { - role: 'admin', - errors: {}, - email: 'a1@53019.com', - forwardsTo: '', - submitsTo: 'a1@53019.com', + customUnits: { + '39C3FF491F559': { + customUnitID: '39C3FF491F559', + name: 'Distance', + attributes: { + unit: 'mi', + }, + rates: { + '928A74633831E': { + customUnitRateID: '928A74633831E', + name: 'Default Rate', + rate: 67, + enabled: true, + currency: 'USD', + }, + }, + defaultCategory: 'Car', + enabled: true, + }, }, - }, - pendingFields: {}, - chatReportIDAnnounce: 0, - chatReportIDAdmins: 1811331783036078, - address: [], - approver: 'a1@53019.com', - areCompanyCardsEnabled: false, - areExpensifyCardsEnabled: false, - areInvoicesEnabled: true, - arePerDiemRatesEnabled: false, - areRulesEnabled: false, - autoReimbursement: { - limit: 0, - }, - autoReimbursementLimit: 0, - autoReportingOffset: 1, - avatarURL: '', - defaultBillable: false, - description: '', - disabledFields: { - defaultBillable: true, - reimbursable: false, - }, - fieldList: { - text_title: { - defaultValue: '{report:type} {report:startdate}', - deletable: true, - disabledOptions: [], - externalIDs: [], - fieldID: 'text_title', - isTax: false, - keys: [], - name: 'title', - orderWeight: 0, - target: 'expense', - type: 'formula', - values: [], + areCategoriesEnabled: true, + areTagsEnabled: false, + areDistanceRatesEnabled: false, + areWorkflowsEnabled: false, + areReportFieldsEnabled: false, + areConnectionsEnabled: false, + employeeList: { + 'a1@53019.com': { + role: 'admin', + errors: {}, + email: 'a1@53019.com', + forwardsTo: '', + submitsTo: 'a1@53019.com', + }, }, - }, - hasMultipleTagLists: false, - invoice: { - markUp: 0, - companyName: 'b1-53019', - companyWebsite: 'https://www.53019.com', pendingFields: {}, - bankAccount: { - stripeConnectAccountBalance: 0, - stripeConnectAccountID: 'acct_1QVeO7S7tHTCCfyY', - transferBankAccountID: 29, + chatReportIDAnnounce: 0, + chatReportIDAdmins: 1811331783036078, + address: [], + approver: 'a1@53019.com', + areCompanyCardsEnabled: false, + areExpensifyCardsEnabled: false, + areInvoicesEnabled: true, + arePerDiemRatesEnabled: false, + areRulesEnabled: false, + autoReimbursement: { + limit: 0, }, - }, - preventSelfApproval: false, - reimbursementChoice: 'reimburseManual', - requiresCategory: false, - requiresTag: false, - tax: { - trackingEnabled: false, - }, - mccGroup: { - airlines: { - category: 'Travel', - groupID: 'airlines', + autoReimbursementLimit: 0, + autoReportingOffset: 1, + avatarURL: '', + defaultBillable: false, + description: '', + disabledFields: { + defaultBillable: true, + reimbursable: false, }, - commuter: { - category: 'Car', - groupID: 'commuter', - }, - gas: { - category: 'Car', - groupID: 'gas', - }, - goods: { - category: 'Materials', - groupID: 'goods', - }, - groceries: { - category: 'Meals and Entertainment', - groupID: 'groceries', - }, - hotel: { - category: 'Travel', - groupID: 'hotel', - }, - mail: { - category: 'Office Supplies', - groupID: 'mail', - }, - meals: { - category: 'Meals and Entertainment', - groupID: 'meals', - }, - rental: { - category: 'Travel', - groupID: 'rental', - }, - services: { - category: 'Professional Services', - groupID: 'services', + fieldList: { + text_title: { + defaultValue: '{report:type} {report:startdate}', + deletable: true, + disabledOptions: [], + externalIDs: [], + fieldID: 'text_title', + isTax: false, + keys: [], + name: 'title', + orderWeight: 0, + target: 'expense', + type: 'formula', + values: [], + }, }, - taxi: { - category: 'Travel', - groupID: 'taxi', + hasMultipleTagLists: false, + invoice: { + markUp: 0, + companyName: 'b1-53019', + companyWebsite: 'https://www.53019.com', + pendingFields: {}, + bankAccount: { + stripeConnectAccountBalance: 0, + stripeConnectAccountID: 'acct_1QVeO7S7tHTCCfyY', + transferBankAccountID: 29, + }, }, - uncategorized: { - category: 'Other', - groupID: 'uncategorized', + preventSelfApproval: false, + reimbursementChoice: 'reimburseManual', + requiresCategory: false, + requiresTag: false, + tax: { + trackingEnabled: false, }, - utilities: { - category: 'Utilities', - groupID: 'utilities', + mccGroup: { + airlines: { + category: 'Travel', + groupID: 'airlines', + }, + commuter: { + category: 'Car', + groupID: 'commuter', + }, + gas: { + category: 'Car', + groupID: 'gas', + }, + goods: { + category: 'Materials', + groupID: 'goods', + }, + groceries: { + category: 'Meals and Entertainment', + groupID: 'groceries', + }, + hotel: { + category: 'Travel', + groupID: 'hotel', + }, + mail: { + category: 'Office Supplies', + groupID: 'mail', + }, + meals: { + category: 'Meals and Entertainment', + groupID: 'meals', + }, + rental: { + category: 'Travel', + groupID: 'rental', + }, + services: { + category: 'Professional Services', + groupID: 'services', + }, + taxi: { + category: 'Travel', + groupID: 'taxi', + }, + uncategorized: { + category: 'Other', + groupID: 'uncategorized', + }, + utilities: { + category: 'Utilities', + groupID: 'utilities', + }, }, - }, - rules: [], - }; - const companyName = 'b1-53019'; - const companyWebsite = 'https://www.53019.com'; - - // When getSendInvoiceInformation is called - const information = IOU.getSendInvoiceInformation(transaction, currentUserAccountID, undefined, undefined, policy, undefined, undefined, companyName, companyWebsite); + rules: [], + }; + const companyName = 'b1-53019'; + const companyWebsite = 'https://www.53019.com'; + + IOU.sendInvoice(currentUserAccountID, transaction, undefined, undefined, policy, undefined, undefined, companyName, companyWebsite); + + // Assert that API.write has been called with parameters where the parameters.invoiceRoomReportID is not the same as the convertedInvoiceChat.reportID + expect(writeSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat.reportID), + }), + expect.anything(), + expect.anything(), + ); - // Then a new invoice chat/room is created - expect(information?.invoiceRoom?.reportID).not.toEqual(convertedInvoiceChat.reportID); + // Restore the original implementation + writeSpy.mockRestore(); + }); }); }); From 401f5debd7cbe83e0ed9378c320930541c21ec72 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 14:32:52 -0800 Subject: [PATCH 037/110] Fix test, remove extra expected param --- tests/actions/IOUTest.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 1d92855ceb77..a28ab0a5b3e8 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3540,7 +3540,6 @@ describe('actions/IOU', () => { invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat.reportID), }), expect.anything(), - expect.anything(), ); // Restore the original implementation From 3d11b6c9adf485ed141c07b4c244ba3b3381878a Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 14:33:03 -0800 Subject: [PATCH 038/110] Pass proper invoice receiver type --- src/libs/actions/IOU.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c094873379b9..1ba31d09438a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -39,6 +39,7 @@ import GoogleTagManager from '@libs/GoogleTagManager'; import * as IOUUtils from '@libs/IOUUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; +import Log from '@libs/Log'; import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane'; import Navigation from '@libs/Navigation/Navigation'; import * as NextStepUtils from '@libs/NextStepUtils'; @@ -65,6 +66,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Attendee, Participant, Split} from '@src/types/onyx/IOU'; import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import {InvoiceReceiver, InvoiceReceiverType} from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; @@ -2037,6 +2039,20 @@ function getDeleteTrackExpenseInformation( return {parameters, optimisticData, successData, failureData, shouldDeleteTransactionThread, chatReport}; } +function getReceiverType(receiverParticipant: Participant | InvoiceReceiver | undefined): InvoiceReceiverType { + if (!receiverParticipant) { + Log.warn('getReceiverType called with no receiverParticipant'); + return CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; + } + if ('type' in receiverParticipant && receiverParticipant.type) { + return receiverParticipant.type; + } + if ('policyID' in receiverParticipant && receiverParticipant.policyID) { + return CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS; + } + return CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL; +} + /** Gathers all the data needed to create an invoice. */ function getSendInvoiceInformation( transaction: OnyxEntry, @@ -2052,17 +2068,18 @@ function getSendInvoiceInformation( const {amount = 0, currency = '', created = '', merchant = '', category = '', tag = '', taxCode = '', taxAmount = 0, billable, comment, participants} = transaction ?? {}; const trimmedComment = (comment?.comment ?? '').trim(); const senderWorkspaceID = participants?.find((participant) => participant?.isSender)?.policyID ?? '-1'; - const receiverParticipant = participants?.find((participant) => participant?.accountID) ?? invoiceChatReport?.invoiceReceiver; + const receiverParticipant: Participant | InvoiceReceiver | undefined = participants?.find((participant) => participant?.accountID) ?? invoiceChatReport?.invoiceReceiver; const receiverAccountID = receiverParticipant && 'accountID' in receiverParticipant && receiverParticipant.accountID ? receiverParticipant.accountID : -1; let receiver = ReportUtils.getPersonalDetailsForAccountID(receiverAccountID); let optimisticPersonalDetailListAction = {}; + const receiverType = getReceiverType(receiverParticipant); // STEP 1: Get existing chat report OR build a new optimistic one let isNewChatReport = false; let chatReport = !isEmptyObject(invoiceChatReport) && invoiceChatReport?.reportID ? invoiceChatReport : null; if (!chatReport) { - chatReport = ReportUtils.getInvoiceChatByParticipants(senderWorkspaceID, receiverAccountID) ?? null; + chatReport = ReportUtils.getInvoiceChatByParticipants(senderWorkspaceID, receiverAccountID, receiverType) ?? null; } if (!chatReport) { @@ -6827,7 +6844,7 @@ function getPayMoneyRequestParams( } if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && activePolicyID) { - const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', activePolicyID); + const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL); if (existingB2BInvoiceRoom) { chatReport = existingB2BInvoiceRoom; } From 03c9bf995709d3a9aa6d59063c9ca9026fd7555c Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 15:02:55 -0800 Subject: [PATCH 039/110] WIP fix some types --- src/types/onyx/Policy.ts | 24 ++++++++++++++++-------- src/types/onyx/Report.ts | 2 +- tests/actions/IOUTest.ts | 4 +++- tests/unit/ReportUtilsTest.ts | 12 ++++++++---- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 672816e1fa6b..86f4a1aaad38 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1491,9 +1491,15 @@ type PolicyInvoicingDetails = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Account balance */ stripeConnectAccountBalance?: number; + /** AccountID */ + stripeConnectAccountID?: string; + /** bankAccountID of selected BBA for payouts */ transferBankAccountID?: number; }; + + /** The markUp */ + markUp?: number; }>; /** Names of policy features */ @@ -1593,7 +1599,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< outputCurrency: string; /** The address of the company */ - address?: CompanyAddress; + address?: CompanyAddress | []; /** The URL for the policy avatar */ avatarURL?: string; @@ -1742,13 +1748,15 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< taxRates?: TaxRatesWithDefault; /** A set of rules related to the workpsace */ - rules?: { - /** A set of rules related to the workpsace approvals */ - approvalRules?: ApprovalRule[]; - - /** A set of rules related to the workpsace expenses */ - expenseRules?: ExpenseRule[]; - }; + rules?: + | { + /** A set of rules related to the workpsace approvals */ + approvalRules?: ApprovalRule[]; + + /** A set of rules related to the workpsace expenses */ + expenseRules?: ExpenseRule[]; + } + | []; /** ReportID of the admins room for this workspace */ chatReportIDAdmins?: number; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 7f652904f0cb..f552c5eeaef4 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -194,7 +194,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< currency?: string; /** Collection of errors that exist in report fields */ - errorFields?: OnyxCommon.ErrorFields; + errorFields?: OnyxCommon.ErrorFields | null; /** Whether the report is waiting on a bank account */ isWaitingOnBankAccount?: boolean; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index a28ab0a5b3e8..9b75a5d9be63 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3407,6 +3407,7 @@ describe('actions/IOU', () => { areReportFieldsEnabled: false, areConnectionsEnabled: false, employeeList: { + // eslint-disable-next-line @typescript-eslint/naming-convention 'a1@53019.com': { role: 'admin', errors: {}, @@ -3438,6 +3439,7 @@ describe('actions/IOU', () => { reimbursable: false, }, fieldList: { + // eslint-disable-next-line @typescript-eslint/naming-convention text_title: { defaultValue: '{report:type} {report:startdate}', deletable: true, @@ -3537,7 +3539,7 @@ describe('actions/IOU', () => { expect(writeSpy).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat.reportID), + invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat?.reportID ?? ''), }), expect.anything(), ); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 918115f3756d..5ff3a73b3089 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -8,6 +8,7 @@ import CONST from '@src/CONST'; import * as TransactionUtils from '@src/libs/TransactionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; +import type {InvoiceReceiver} from '@src/types/onyx/Report'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as NumberUtils from '../../src/libs/NumberUtils'; import * as LHNTestUtils from '../utils/LHNTestUtils'; @@ -62,24 +63,27 @@ const policy: Policy = { isPolicyExpenseChatEnabled: false, }; -const convertedInvoiceChat = { +const convertedInvoiceChat: OnyxEntry = { chatType: CONST.REPORT.CHAT_TYPE.INVOICE, currency: 'USD', description: '', errorFields: null, hasOutstandingChildRequest: false, hasOutstandingChildTask: false, + + // The invoice receiver shouldn't have an accountID when the type is business, + // but this is to test that it still works if the value is present invoiceReceiver: { accountID: 33, policyID: '5F2F82F98C848CAA', type: 'policy', - }, + } as unknown as InvoiceReceiver, isCancelledIOU: false, isOwnPolicyExpenseChat: false, isPinned: false, isWaitingOnBankAccount: false, lastActionType: 'REPORTPREVIEW', - lastActorAccountID: '32', + lastActorAccountID: 32, lastMessageHtml: 'paid $1.00', lastMessageText: 'paid $1.00', lastMessageTranslationKey: '', @@ -98,7 +102,7 @@ const convertedInvoiceChat = { }, '33': { notificationPreference: 'always', - permissions: ['read', 'write', 'share', 'own'], + permissions: [CONST.REPORT.PERMISSIONS.READ, CONST.REPORT.PERMISSIONS.WRITE, CONST.REPORT.PERMISSIONS.SHARE, CONST.REPORT.PERMISSIONS.OWN], }, }, policyAvatar: '', From 0c6e50d49b1ba5caa4a16290042bb2efb38c7d16 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 17:08:39 -0800 Subject: [PATCH 040/110] Add doc comment to make function more clear --- src/libs/actions/IOU.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 1ba31d09438a..8d072ab459aa 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2039,6 +2039,11 @@ function getDeleteTrackExpenseInformation( return {parameters, optimisticData, successData, failureData, shouldDeleteTransactionThread, chatReport}; } +/** + * Get the invoice receiver type based on the receiver participant. + * @param receiverParticipant The participant who will receive the invoice or the invoice receiver object directly. + * @returns The invoice receiver type. + */ function getReceiverType(receiverParticipant: Participant | InvoiceReceiver | undefined): InvoiceReceiverType { if (!receiverParticipant) { Log.warn('getReceiverType called with no receiverParticipant'); From a2bbb544b91ea2e3e62765d98a056778eacacd15 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 17:10:25 -0800 Subject: [PATCH 041/110] Fix receiver type getting B2B invoice room --- 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 8d072ab459aa..04671ac2fa04 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -6849,7 +6849,7 @@ function getPayMoneyRequestParams( } if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && activePolicyID) { - const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL); + const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS); if (existingB2BInvoiceRoom) { chatReport = existingB2BInvoiceRoom; } From 410868e8337526a27f7ab4f83c367edd9f33ae53 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 18:03:54 -0800 Subject: [PATCH 042/110] Major clean up of tests and types and lint --- src/libs/ReportUtils.ts | 2 +- src/libs/actions/IOU.ts | 4 +- src/types/onyx/Policy.ts | 3 + src/types/onyx/Report.ts | 13 +- tests/actions/IOUTest.ts | 225 +------------------------- tests/data/Invoice.ts | 295 ++++++++++++++++++++++++++++++++++ tests/unit/ReportUtilsTest.ts | 67 +------- 7 files changed, 321 insertions(+), 288 deletions(-) create mode 100644 tests/data/Invoice.ts diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 25a8055bc747..59f6effc63d4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6746,7 +6746,7 @@ function getChatByParticipants(newParticipantList: number[], reports: OnyxCollec /** * Attempts to find an invoice chat report in onyx with the provided policyID and receiverID. */ -function getInvoiceChatByParticipants(policyID: string, receiverID: string | number, receiverType: InvoiceReceiverType, reports: OnyxCollection = allReports): OnyxEntry { +function getInvoiceChatByParticipants(receiverID: string | number, receiverType: InvoiceReceiverType, policyID?: string, reports: OnyxCollection = allReports): OnyxEntry { return Object.values(reports ?? {}).find((report) => { if (!report || !isInvoiceRoom(report) || isArchivedRoom(report)) { return false; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 04671ac2fa04..9d4aa640aeb1 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -2084,7 +2084,7 @@ function getSendInvoiceInformation( let chatReport = !isEmptyObject(invoiceChatReport) && invoiceChatReport?.reportID ? invoiceChatReport : null; if (!chatReport) { - chatReport = ReportUtils.getInvoiceChatByParticipants(senderWorkspaceID, receiverAccountID, receiverType) ?? null; + chatReport = ReportUtils.getInvoiceChatByParticipants(receiverAccountID, receiverType, senderWorkspaceID) ?? null; } if (!chatReport) { @@ -6849,7 +6849,7 @@ function getPayMoneyRequestParams( } if (ReportUtils.isIndividualInvoiceRoom(chatReport) && payAsBusiness && activePolicyID) { - const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(chatReport.policyID ?? '', activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS); + const existingB2BInvoiceRoom = ReportUtils.getInvoiceChatByParticipants(activePolicyID, CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS, chatReport.policyID); if (existingB2BInvoiceRoom) { chatReport = existingB2BInvoiceRoom; } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 86f4a1aaad38..2873a80b1d52 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1636,6 +1636,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< harvesting?: { /** Whether the scheduled submit is enabled */ enabled: boolean; + + /** The ID of the Bedrock job that runs harvesting */ + jobID: number; }; /** Whether the self approval or submitting is enabled */ diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index f552c5eeaef4..ea6fa5d33c3a 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -42,6 +42,9 @@ type Participant = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Whether the participant is visible in the report */ notificationPreference: NotificationPreference; + + /** Permissions granted to the participant */ + permissions?: Array>; }>; /** Types of invoice receivers in a report */ @@ -133,10 +136,13 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** ID of the chat report */ chatReportID?: string; - /** The state that the report is currently in */ + /** The state of the report */ + state?: keyof typeof CONST.REPORT.STATE_NUM; + + /** The state number of the report */ stateNum?: ValueOf; - /** The status of the current report */ + /** The state that the report is currently in */ statusNum?: ValueOf; /** Which user role is capable of posting messages on the report */ @@ -238,6 +244,9 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the report is archived */ // eslint-disable-next-line @typescript-eslint/naming-convention private_isArchived?: string; + + /** The report's welcome message */ + welcomeMessage?: string; }, 'addWorkspaceRoom' | 'avatar' | 'createChat' | 'partial' | 'reimbursed' | 'preview' >; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 9b75a5d9be63..e74b380b7b02 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -22,7 +22,8 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/Report'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {convertedInvoiceChat} from '../unit/ReportUtilsTest'; +import * as InvoiceData from '../data/Invoice'; +import type {InvoiceTestData} from '../data/Invoice'; import PusherHelper from '../utils/PusherHelper'; import type {MockFetch} from '../utils/TestHelper'; import * as TestHelper from '../utils/TestHelper'; @@ -3312,234 +3313,22 @@ describe('actions/IOU', () => { const writeSpy = jest.spyOn(API, 'write').mockImplementation(jest.fn()); // Given a convertedInvoiceReport is stored in Onyx - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${convertedInvoiceChat.reportID}`, convertedInvoiceChat); + const {policy, transaction, convertedInvoiceChat}: InvoiceTestData = InvoiceData; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${convertedInvoiceChat?.reportID}`, convertedInvoiceChat ?? {}); // And data for when a new invoice is sent to a user - const transaction: OnyxEntry = { - amount: 100, - attendees: [ - { - email: 'a1@53019.com', - login: 'a1@53019.com', - displayName: 'a1', - avatarUrl: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_9.png', - accountID: 32, - text: 'a1@53019.com', - selected: true, - reportID: '3634215302663162', - }, - ], - comment: { - customUnit: { - customUnitRateID: '_FAKE_P2P_ID_', - }, - }, - created: '2024-12-13', - currency: 'USD', - iouRequestType: 'manual', - reportID: '3634215302663162', - transactionID: '1', - isFromGlobalCreate: true, - merchant: '(none)', - splitPayerAccountIDs: [32], - shouldShowOriginalAmount: true, - participants: [ - { - accountID: 33, - login: 'b1@53019.com', - isPolicyExpenseChat: false, - reportID: '', - selected: true, - iouType: 'invoice', - }, - { - policyID: 'CC048FA711B35B1F', - isSender: true, - selected: false, - iouType: 'invoice', - }, - ], - tag: '', - category: '', - billable: false, - }; const currentUserAccountID = 32; - const policy: OnyxEntry = { - id: 'CC048FA711B35B1F', - type: 'team', - name: "53019's Workspace", - role: 'admin', - owner: 'a1@53019.com', - ownerAccountID: 32, - isPolicyExpenseChatEnabled: true, - outputCurrency: 'USD', - autoReporting: true, - autoReportingFrequency: 'instant', - approvalMode: 'OPTIONAL', - harvesting: { - enabled: true, - jobID: 7206965285807173000, - }, - customUnits: { - '39C3FF491F559': { - customUnitID: '39C3FF491F559', - name: 'Distance', - attributes: { - unit: 'mi', - }, - rates: { - '928A74633831E': { - customUnitRateID: '928A74633831E', - name: 'Default Rate', - rate: 67, - enabled: true, - currency: 'USD', - }, - }, - defaultCategory: 'Car', - enabled: true, - }, - }, - areCategoriesEnabled: true, - areTagsEnabled: false, - areDistanceRatesEnabled: false, - areWorkflowsEnabled: false, - areReportFieldsEnabled: false, - areConnectionsEnabled: false, - employeeList: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'a1@53019.com': { - role: 'admin', - errors: {}, - email: 'a1@53019.com', - forwardsTo: '', - submitsTo: 'a1@53019.com', - }, - }, - pendingFields: {}, - chatReportIDAnnounce: 0, - chatReportIDAdmins: 1811331783036078, - address: [], - approver: 'a1@53019.com', - areCompanyCardsEnabled: false, - areExpensifyCardsEnabled: false, - areInvoicesEnabled: true, - arePerDiemRatesEnabled: false, - areRulesEnabled: false, - autoReimbursement: { - limit: 0, - }, - autoReimbursementLimit: 0, - autoReportingOffset: 1, - avatarURL: '', - defaultBillable: false, - description: '', - disabledFields: { - defaultBillable: true, - reimbursable: false, - }, - fieldList: { - // eslint-disable-next-line @typescript-eslint/naming-convention - text_title: { - defaultValue: '{report:type} {report:startdate}', - deletable: true, - disabledOptions: [], - externalIDs: [], - fieldID: 'text_title', - isTax: false, - keys: [], - name: 'title', - orderWeight: 0, - target: 'expense', - type: 'formula', - values: [], - }, - }, - hasMultipleTagLists: false, - invoice: { - markUp: 0, - companyName: 'b1-53019', - companyWebsite: 'https://www.53019.com', - pendingFields: {}, - bankAccount: { - stripeConnectAccountBalance: 0, - stripeConnectAccountID: 'acct_1QVeO7S7tHTCCfyY', - transferBankAccountID: 29, - }, - }, - preventSelfApproval: false, - reimbursementChoice: 'reimburseManual', - requiresCategory: false, - requiresTag: false, - tax: { - trackingEnabled: false, - }, - mccGroup: { - airlines: { - category: 'Travel', - groupID: 'airlines', - }, - commuter: { - category: 'Car', - groupID: 'commuter', - }, - gas: { - category: 'Car', - groupID: 'gas', - }, - goods: { - category: 'Materials', - groupID: 'goods', - }, - groceries: { - category: 'Meals and Entertainment', - groupID: 'groceries', - }, - hotel: { - category: 'Travel', - groupID: 'hotel', - }, - mail: { - category: 'Office Supplies', - groupID: 'mail', - }, - meals: { - category: 'Meals and Entertainment', - groupID: 'meals', - }, - rental: { - category: 'Travel', - groupID: 'rental', - }, - services: { - category: 'Professional Services', - groupID: 'services', - }, - taxi: { - category: 'Travel', - groupID: 'taxi', - }, - uncategorized: { - category: 'Other', - groupID: 'uncategorized', - }, - utilities: { - category: 'Utilities', - groupID: 'utilities', - }, - }, - rules: [], - }; const companyName = 'b1-53019'; const companyWebsite = 'https://www.53019.com'; + // When the user sends a new invoice to an individual IOU.sendInvoice(currentUserAccountID, transaction, undefined, undefined, policy, undefined, undefined, companyName, companyWebsite); - // Assert that API.write has been called with parameters where the parameters.invoiceRoomReportID is not the same as the convertedInvoiceChat.reportID + // Then a new invoice chat is created instead of incorrectly using the invoice chat which has been converted from individual to business expect(writeSpy).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat?.reportID ?? ''), + invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat.reportID), }), expect.anything(), ); diff --git a/tests/data/Invoice.ts b/tests/data/Invoice.ts new file mode 100644 index 000000000000..24ae87f451fe --- /dev/null +++ b/tests/data/Invoice.ts @@ -0,0 +1,295 @@ +// Test data for Invoices. The values come from the Onyx store in the app while manually testing. +import type {OnyxEntry} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {InvoiceReceiver} from '@src/types/onyx/Report'; + +const policy: OnyxEntry = { + id: 'CC048FA711B35B1F', + type: 'team', + name: "53019's Workspace", + role: 'admin', + owner: 'a1@53019.com', + ownerAccountID: 32, + isPolicyExpenseChatEnabled: true, + outputCurrency: 'USD', + autoReporting: true, + autoReportingFrequency: 'instant', + approvalMode: 'OPTIONAL', + harvesting: { + enabled: true, + jobID: 7206965285807173000, + }, + customUnits: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '39C3FF491F559': { + customUnitID: '39C3FF491F559', + name: 'Distance', + attributes: { + unit: 'mi', + }, + rates: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '928A74633831E': { + customUnitRateID: '928A74633831E', + name: 'Default Rate', + rate: 67, + enabled: true, + currency: 'USD', + }, + }, + defaultCategory: 'Car', + enabled: true, + }, + }, + areCategoriesEnabled: true, + areTagsEnabled: false, + areDistanceRatesEnabled: false, + areWorkflowsEnabled: false, + areReportFieldsEnabled: false, + areConnectionsEnabled: false, + employeeList: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'a1@53019.com': { + role: 'admin', + errors: {}, + email: 'a1@53019.com', + forwardsTo: '', + submitsTo: 'a1@53019.com', + }, + }, + pendingFields: {}, + chatReportIDAnnounce: 0, + chatReportIDAdmins: 1811331783036078, + address: [], + approver: 'a1@53019.com', + areCompanyCardsEnabled: false, + areExpensifyCardsEnabled: false, + areInvoicesEnabled: true, + arePerDiemRatesEnabled: false, + areRulesEnabled: false, + autoReimbursement: { + limit: 0, + }, + autoReimbursementLimit: 0, + autoReportingOffset: 1, + avatarURL: '', + defaultBillable: false, + description: '', + disabledFields: { + defaultBillable: true, + reimbursable: false, + }, + fieldList: { + // eslint-disable-next-line @typescript-eslint/naming-convention + text_title: { + defaultValue: '{report:type} {report:startdate}', + deletable: true, + disabledOptions: [], + externalIDs: [], + fieldID: 'text_title', + isTax: false, + keys: [], + name: 'title', + orderWeight: 0, + target: 'expense', + type: 'formula', + values: [], + }, + }, + hasMultipleTagLists: false, + invoice: { + markUp: 0, + companyName: 'b1-53019', + companyWebsite: 'https://www.53019.com', + pendingFields: {}, + bankAccount: { + stripeConnectAccountBalance: 0, + stripeConnectAccountID: 'acct_1QVeO7S7tHTCCfyY', + transferBankAccountID: 29, + }, + }, + preventSelfApproval: false, + reimbursementChoice: 'reimburseManual', + requiresCategory: false, + requiresTag: false, + tax: { + trackingEnabled: false, + }, + mccGroup: { + airlines: { + category: 'Travel', + groupID: 'airlines', + }, + commuter: { + category: 'Car', + groupID: 'commuter', + }, + gas: { + category: 'Car', + groupID: 'gas', + }, + goods: { + category: 'Materials', + groupID: 'goods', + }, + groceries: { + category: 'Meals and Entertainment', + groupID: 'groceries', + }, + hotel: { + category: 'Travel', + groupID: 'hotel', + }, + mail: { + category: 'Office Supplies', + groupID: 'mail', + }, + meals: { + category: 'Meals and Entertainment', + groupID: 'meals', + }, + rental: { + category: 'Travel', + groupID: 'rental', + }, + services: { + category: 'Professional Services', + groupID: 'services', + }, + taxi: { + category: 'Travel', + groupID: 'taxi', + }, + uncategorized: { + category: 'Other', + groupID: 'uncategorized', + }, + utilities: { + category: 'Utilities', + groupID: 'utilities', + }, + }, + rules: [], +}; + +const transaction: OnyxEntry = { + amount: 100, + attendees: [ + { + email: 'a1@53019.com', + login: 'a1@53019.com', + displayName: 'a1', + avatarUrl: 'https://d2k5nsl2zxldvw.cloudfront.net/images/avatars/default-avatar_9.png', + accountID: 32, + text: 'a1@53019.com', + selected: true, + reportID: '3634215302663162', + }, + ], + comment: { + customUnit: { + customUnitRateID: '_FAKE_P2P_ID_', + }, + }, + created: '2024-12-13', + currency: 'USD', + iouRequestType: 'manual', + reportID: '3634215302663162', + transactionID: '1', + isFromGlobalCreate: true, + merchant: '(none)', + splitPayerAccountIDs: [32], + shouldShowOriginalAmount: true, + participants: [ + { + accountID: 33, + login: 'b1@53019.com', + isPolicyExpenseChat: false, + reportID: '', + selected: true, + iouType: 'invoice', + }, + { + policyID: 'CC048FA711B35B1F', + isSender: true, + selected: false, + iouType: 'invoice', + }, + ], + tag: '', + category: '', + billable: false, +}; + +const convertedInvoiceChat: OnyxTypes.Report = { + chatType: CONST.REPORT.CHAT_TYPE.INVOICE, + currency: 'USD', + description: '', + errorFields: null, + hasOutstandingChildRequest: false, + hasOutstandingChildTask: false, + + // The invoice receiver shouldn't have an accountID when the type is business, + // but this is to test that it still works if the value is present, so cast it to unknown + invoiceReceiver: { + accountID: 33, + policyID: '5F2F82F98C848CAA', + type: 'policy', + } as unknown as InvoiceReceiver, + isCancelledIOU: false, + isOwnPolicyExpenseChat: false, + isPinned: false, + isWaitingOnBankAccount: false, + lastActionType: 'REPORTPREVIEW', + lastActorAccountID: 32, + lastMessageHtml: 'paid $1.00', + lastMessageText: 'paid $1.00', + lastMessageTranslationKey: '', + lastReadSequenceNumber: 0, + lastReadTime: '2024-12-13 19:45:28.942', + lastVisibleActionCreated: '2024-12-13 19:19:01.794', + lastVisibleActionLastModified: '2024-12-13 19:19:01.794', + managerID: 0, + nonReimbursableTotal: 0, + oldPolicyName: '', + ownerAccountID: 0, + participants: { + // eslint-disable-next-line @typescript-eslint/naming-convention + '32': { + notificationPreference: 'always', + role: 'admin', + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + '33': { + notificationPreference: 'always', + permissions: [CONST.REPORT.PERMISSIONS.READ, CONST.REPORT.PERMISSIONS.WRITE, CONST.REPORT.PERMISSIONS.SHARE, CONST.REPORT.PERMISSIONS.OWN], + }, + }, + policyAvatar: '', + policyID: 'CC048FA711B35B1F', + policyName: "53019's Workspace", + // eslint-disable-next-line @typescript-eslint/naming-convention + private_isArchived: '', + reportID: '7605647250932303', + reportName: 'Chat Report', + state: 'OPEN', + stateNum: 0, + statusNum: 0, + total: 0, + type: 'chat', + unheldNonReimbursableTotal: 0, + unheldTotal: 0, + visibility: 'private', + welcomeMessage: '', + writeCapability: 'all', +}; + +type InvoiceTestData = { + policy: OnyxEntry; + transaction: OnyxEntry; + convertedInvoiceChat: OnyxTypes.Report; +}; + +export type {InvoiceTestData}; +export {policy, transaction, convertedInvoiceChat}; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 5ff3a73b3089..751e9e5beb2d 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -2,13 +2,13 @@ import {addDays, format as formatDate} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import {convertedInvoiceChat} from 'tests/data/Invoice'; import DateUtils from '@libs/DateUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import * as TransactionUtils from '@src/libs/TransactionUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; -import type {InvoiceReceiver} from '@src/types/onyx/Report'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as NumberUtils from '../../src/libs/NumberUtils'; import * as LHNTestUtils from '../utils/LHNTestUtils'; @@ -63,66 +63,6 @@ const policy: Policy = { isPolicyExpenseChatEnabled: false, }; -const convertedInvoiceChat: OnyxEntry = { - chatType: CONST.REPORT.CHAT_TYPE.INVOICE, - currency: 'USD', - description: '', - errorFields: null, - hasOutstandingChildRequest: false, - hasOutstandingChildTask: false, - - // The invoice receiver shouldn't have an accountID when the type is business, - // but this is to test that it still works if the value is present - invoiceReceiver: { - accountID: 33, - policyID: '5F2F82F98C848CAA', - type: 'policy', - } as unknown as InvoiceReceiver, - isCancelledIOU: false, - isOwnPolicyExpenseChat: false, - isPinned: false, - isWaitingOnBankAccount: false, - lastActionType: 'REPORTPREVIEW', - lastActorAccountID: 32, - lastMessageHtml: 'paid $1.00', - lastMessageText: 'paid $1.00', - lastMessageTranslationKey: '', - lastReadSequenceNumber: 0, - lastReadTime: '2024-12-13 19:45:28.942', - lastVisibleActionCreated: '2024-12-13 19:19:01.794', - lastVisibleActionLastModified: '2024-12-13 19:19:01.794', - managerID: 0, - nonReimbursableTotal: 0, - oldPolicyName: '', - ownerAccountID: 0, - participants: { - '32': { - notificationPreference: 'always', - role: 'admin', - }, - '33': { - notificationPreference: 'always', - permissions: [CONST.REPORT.PERMISSIONS.READ, CONST.REPORT.PERMISSIONS.WRITE, CONST.REPORT.PERMISSIONS.SHARE, CONST.REPORT.PERMISSIONS.OWN], - }, - }, - policyAvatar: '', - policyID: 'CC048FA711B35B1F', - policyName: "53019's Workspace", - private_isArchived: '', - reportID: '7605647250932303', - reportName: 'Chat Report', - state: 'OPEN', - stateNum: 0, - statusNum: 0, - total: 0, - type: 'chat', - unheldNonReimbursableTotal: 0, - unheldTotal: 0, - visibility: 'private', - welcomeMessage: '', - writeCapability: 'all', -}; - Onyx.init({keys: ONYXKEYS}); describe('ReportUtils', () => { @@ -1484,13 +1424,10 @@ describe('ReportUtils', () => { }; // When we send another invoice to the individual from global create and call getInvoiceChatByParticipants - const invoiceChatReport = ReportUtils.getInvoiceChatByParticipants(convertedInvoiceChat.policyID, 33, CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL, reports); + const invoiceChatReport = ReportUtils.getInvoiceChatByParticipants(33, CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL, convertedInvoiceChat.policyID, reports); // Then no invoice chat should be returned because the receiver type does not match expect(invoiceChatReport).toBeUndefined(); }); }); }); - -// eslint-disable-next-line import/prefer-default-export -export {convertedInvoiceChat}; From 57ba052c310be26fb859c424974ece66a56412ac Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 18:12:50 -0800 Subject: [PATCH 043/110] Jest matcher returns any so cast to fix type error --- tests/actions/IOUTest.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index e74b380b7b02..c70134f64b6e 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3328,12 +3328,10 @@ describe('actions/IOU', () => { expect(writeSpy).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ - invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat.reportID), + invoiceRoomReportID: expect.not.stringMatching(convertedInvoiceChat.reportID) as string, }), expect.anything(), ); - - // Restore the original implementation writeSpy.mockRestore(); }); }); From e6d5a920fee36535f92b4ee571c2d29a165797b8 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 18:46:49 -0800 Subject: [PATCH 044/110] Undo edge case type changes --- src/libs/DebugUtils.ts | 3 +++ src/types/onyx/Policy.ts | 18 ++++++++---------- src/types/onyx/Report.ts | 2 +- tests/data/Invoice.ts | 3 --- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 9343164db1e8..9c27009b036e 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -514,6 +514,7 @@ function validateReportDraftProperty(key: keyof Report, value: string) { pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION, pendingFields: 'object', notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE, + permissions: 'array', }, 'number', ); @@ -630,6 +631,8 @@ function validateReportDraftProperty(key: keyof Report, value: string) { partial: CONST.RED_BRICK_ROAD_PENDING_ACTION, reimbursed: CONST.RED_BRICK_ROAD_PENDING_ACTION, preview: CONST.RED_BRICK_ROAD_PENDING_ACTION, + state: CONST.RED_BRICK_ROAD_PENDING_ACTION, + welcomeMessage: CONST.RED_BRICK_ROAD_PENDING_ACTION, }); } } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 2873a80b1d52..57550592d0bd 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1599,7 +1599,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< outputCurrency: string; /** The address of the company */ - address?: CompanyAddress | []; + address?: CompanyAddress; /** The URL for the policy avatar */ avatarURL?: string; @@ -1751,15 +1751,13 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< taxRates?: TaxRatesWithDefault; /** A set of rules related to the workpsace */ - rules?: - | { - /** A set of rules related to the workpsace approvals */ - approvalRules?: ApprovalRule[]; - - /** A set of rules related to the workpsace expenses */ - expenseRules?: ExpenseRule[]; - } - | []; + rules?: { + /** A set of rules related to the workpsace approvals */ + approvalRules?: ApprovalRule[]; + + /** A set of rules related to the workpsace expenses */ + expenseRules?: ExpenseRule[]; + }; /** ReportID of the admins room for this workspace */ chatReportIDAdmins?: number; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index ea6fa5d33c3a..881c416e01a8 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -200,7 +200,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< currency?: string; /** Collection of errors that exist in report fields */ - errorFields?: OnyxCommon.ErrorFields | null; + errorFields?: OnyxCommon.ErrorFields; /** Whether the report is waiting on a bank account */ isWaitingOnBankAccount?: boolean; diff --git a/tests/data/Invoice.ts b/tests/data/Invoice.ts index 24ae87f451fe..6a5c478a3a94 100644 --- a/tests/data/Invoice.ts +++ b/tests/data/Invoice.ts @@ -61,7 +61,6 @@ const policy: OnyxEntry = { pendingFields: {}, chatReportIDAnnounce: 0, chatReportIDAdmins: 1811331783036078, - address: [], approver: 'a1@53019.com', areCompanyCardsEnabled: false, areExpensifyCardsEnabled: false, @@ -170,7 +169,6 @@ const policy: OnyxEntry = { groupID: 'utilities', }, }, - rules: [], }; const transaction: OnyxEntry = { @@ -226,7 +224,6 @@ const convertedInvoiceChat: OnyxTypes.Report = { chatType: CONST.REPORT.CHAT_TYPE.INVOICE, currency: 'USD', description: '', - errorFields: null, hasOutstandingChildRequest: false, hasOutstandingChildTask: false, From 2ada6b83be4127f2a87741e39a22d117c47fd5fa Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 18:52:46 -0800 Subject: [PATCH 045/110] More type fixes --- src/types/onyx/Policy.ts | 2 +- src/types/utils/whitelistedReportKeys.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 57550592d0bd..3079b3f31f3f 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1638,7 +1638,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< enabled: boolean; /** The ID of the Bedrock job that runs harvesting */ - jobID: number; + jobID?: number; }; /** Whether the self approval or submitting is enabled */ diff --git a/src/types/utils/whitelistedReportKeys.ts b/src/types/utils/whitelistedReportKeys.ts index 015420683925..c3849e3bf83d 100644 --- a/src/types/utils/whitelistedReportKeys.ts +++ b/src/types/utils/whitelistedReportKeys.ts @@ -64,6 +64,8 @@ type WhitelistedReport = OnyxCommon.OnyxValueWithOfflineFeedback< }; // eslint-disable-next-line @typescript-eslint/naming-convention private_isArchived: unknown; + state: unknown; + welcomeMessage: unknown; }, PolicyReportField['fieldID'] >; From 3e4e0339aee2f9178d654e0746e56d2c238e406d Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 19:05:18 -0800 Subject: [PATCH 046/110] Fix lint --- src/libs/DebugUtils.ts | 2 ++ src/libs/actions/IOU.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 9c27009b036e..a8cd91599bff 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -469,6 +469,8 @@ function validateReportDraftProperty(key: keyof Report, value: string) { case 'iouReportID': case 'preexistingReportID': case 'private_isArchived': + case 'state': + case 'welcomeMessage': return validateString(value); case 'hasOutstandingChildRequest': case 'hasOutstandingChildTask': diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9d4aa640aeb1..f803386cb649 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -66,7 +66,7 @@ import type * as OnyxTypes from '@src/types/onyx'; import type {Attendee, Participant, Split} from '@src/types/onyx/IOU'; import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -import {InvoiceReceiver, InvoiceReceiverType} from '@src/types/onyx/Report'; +import type {InvoiceReceiver, InvoiceReceiverType} from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; From 4516d8fb51737529cdf946d6bb0821136ada9269 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Fri, 13 Dec 2024 19:08:07 -0800 Subject: [PATCH 047/110] Fix test, bad import path --- tests/unit/ReportUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 751e9e5beb2d..85cf0125a5eb 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -2,7 +2,6 @@ import {addDays, format as formatDate} from 'date-fns'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import {convertedInvoiceChat} from 'tests/data/Invoice'; import DateUtils from '@libs/DateUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; @@ -11,6 +10,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types/onyx'; import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as NumberUtils from '../../src/libs/NumberUtils'; +import {convertedInvoiceChat} from '../data/Invoice'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import {fakePersonalDetails} from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; From ff4fb89d09308e54a4fea4b6725011e570365066 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Sat, 14 Dec 2024 17:21:31 +0700 Subject: [PATCH 048/110] add listen dimention changes --- src/CONST.ts | 2 + src/components/ScreenWrapper.tsx | 9 +++- src/hooks/useReadyWithDimensions.ts | 31 +++++++++++++ .../step/IOURequestStepConfirmation.tsx | 1 + src/utils/{keyboard/index.ts => keyboard.ts} | 0 src/utils/keyboard/index.website.ts | 46 ------------------- 6 files changed, 42 insertions(+), 47 deletions(-) create mode 100644 src/hooks/useReadyWithDimensions.ts rename src/utils/{keyboard/index.ts => keyboard.ts} (100%) delete mode 100644 src/utils/keyboard/index.website.ts diff --git a/src/CONST.ts b/src/CONST.ts index 204ccaccf394..5e7aa0abc6eb 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -319,6 +319,8 @@ const CONST = { NON_BILLABLE: 'nonBillable', }, + DIMENSIONS_CHANGED_DELAY: 1000, + // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT, chatTypes.INVOICE], ANDROID_PACKAGE_NAME, diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 09315bfb8a8e..500aa505228c 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -9,6 +9,7 @@ import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useKeyboardState from '@hooks/useKeyboardState'; import useNetwork from '@hooks/useNetwork'; +import useReadyWithDimensions from '@hooks/useReadyWithDimensions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useTackInputFocus from '@hooks/useTackInputFocus'; @@ -104,6 +105,8 @@ type ScreenWrapperProps = { /** Overrides the focus trap default settings */ focusTrapSettings?: FocusTrapForScreenProps['focusTrapSettings']; + + shouldListenToDimensionChanges?: boolean; }; type ScreenWrapperStatusContextType = { @@ -136,6 +139,7 @@ function ScreenWrapper( shouldShowOfflineIndicatorInWideScreen = false, shouldUseCachedViewportHeight = false, focusTrapSettings, + shouldListenToDimensionChanges = false, }: ScreenWrapperProps, ref: ForwardedRef, ) { @@ -161,8 +165,11 @@ function ScreenWrapper( const keyboardState = useKeyboardState(); const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); + + const {isReady} = useReadyWithDimensions(Browser.isMobileSafari() && shouldListenToDimensionChanges); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; + + const maxHeight = shouldEnableMaxHeight && isReady ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; const isKeyboardShown = keyboardState?.isKeyboardShown ?? false; diff --git a/src/hooks/useReadyWithDimensions.ts b/src/hooks/useReadyWithDimensions.ts new file mode 100644 index 000000000000..897be8c558df --- /dev/null +++ b/src/hooks/useReadyWithDimensions.ts @@ -0,0 +1,31 @@ +import {useEffect, useState} from 'react'; +import {Dimensions} from 'react-native'; +import CONST from '@src/CONST'; + +const useReadyWithDimensions = (isEnabled = true) => { + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + if (!isEnabled) { + return; + } + const timer = setTimeout(() => { + setIsReady(true); + }, CONST.DIMENSIONS_CHANGED_DELAY); + + const handleDimensionChange = () => { + setIsReady(true); + }; + + const subscription = Dimensions.addEventListener('change', handleDimensionChange); + + return () => { + clearTimeout(timer); + subscription?.remove(); + }; + }, [isEnabled]); + + return {isReady}; +}; + +export default useReadyWithDimensions; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 989277bb5fc1..5fbc536144fa 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -603,6 +603,7 @@ function IOURequestStepConfirmation({ { - const inputTypesToIgnore = ['button', 'submit', 'checkbox', 'file', 'image']; - return (elem.tagName === 'INPUT' && !inputTypesToIgnore.includes((elem as HTMLInputElement).type)) || elem.tagName === 'TEXTAREA' || elem.hasAttribute('contenteditable'); -}; - -const handleFocusIn = (event: FocusEvent): void => { - const target = event.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isVisible = true; - } -}; - -const handleFocusOut = (event: FocusEvent): void => { - const target = event.target as HTMLElement; - if (target && isKeyboardInput(target)) { - isVisible = false; - } -}; - -document.addEventListener('focusin', handleFocusIn); -document.addEventListener('focusout', handleFocusOut); - -const dismiss = (): Promise => { - return new Promise((resolve) => { - if (!isVisible) { - resolve(); - return; - } - - Keyboard.dismiss(); - InteractionManager.runAfterInteractions(() => { - isVisible = false; - resolve(); - }); - }); -}; - -const utils = { - dismiss, -}; - -export default utils; From 5d224960e18182f609c709bfcc9dad37a4e86f07 Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:00:13 +1300 Subject: [PATCH 049/110] Fix IOU amount input flicking on native platforms --- src/components/AmountTextInput.tsx | 2 +- src/components/MoneyRequestAmountInput.tsx | 5 ++++- .../TextInput/BaseTextInput/index.native.tsx | 8 +++----- src/components/TextInput/BaseTextInput/types.ts | 3 +++ src/components/TextInputWithCurrencySymbol/types.ts | 2 +- src/pages/iou/MoneyRequestAmountForm.tsx | 2 ++ src/styles/utils/index.ts | 10 ++++++++++ 7 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/components/AmountTextInput.tsx b/src/components/AmountTextInput.tsx index 6be2b43c09d7..12189d22dba0 100644 --- a/src/components/AmountTextInput.tsx +++ b/src/components/AmountTextInput.tsx @@ -39,7 +39,7 @@ type AmountTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; -} & Pick; +} & Pick; function AmountTextInput( { diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx index 9ef33900bb00..717659c16fd3 100644 --- a/src/components/MoneyRequestAmountInput.tsx +++ b/src/components/MoneyRequestAmountInput.tsx @@ -12,6 +12,7 @@ import CONST from '@src/CONST'; import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused'; import type {BaseTextInputRef} from './TextInput/BaseTextInput/types'; import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; +import type {TextInputWithCurrencySymbolProps} from './TextInputWithCurrencySymbol/types'; type CurrentMoney = {amount: string; currency: string}; @@ -91,7 +92,7 @@ type MoneyRequestAmountInputProps = { /** The width of inner content */ contentWidth?: number; -}; +} & Pick; type Selection = { start: number; @@ -126,6 +127,7 @@ function MoneyRequestAmountInput( hideFocusedState = true, shouldKeepUserInput = false, autoGrow = true, + autoGrowExtraSpace, contentWidth, ...props }: MoneyRequestAmountInputProps, @@ -289,6 +291,7 @@ function MoneyRequestAmountInput( return ( = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, - (autoGrow || !!contentWidth) && StyleUtils.getWidthStyle(textInputWidth), + !!contentWidth && StyleUtils.getWidthStyle(textInputWidth), + autoGrow && StyleUtils.getAutoGrowWidthInputContainerStyles(textInputWidth, autoGrowExtraSpace), !hideFocusedState && isFocused && styles.borderColorFocus, (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxAutoGrowHeight === 'number' ? 2 * maxAutoGrowHeight : undefined}, @@ -441,14 +443,10 @@ function BaseTextInput( )} {/* Text input component doesn't support auto grow by default. - We're using a hidden text input to achieve that. This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} {(!!autoGrow || autoGrowHeight) && !isAutoGrowHeightMarkdown && ( - // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value - // https://github.com/Expensify/App/issues/8158 - // https://github.com/Expensify/App/issues/26628 ; +} & Pick; type TextInputWithCurrencySymbolProps = Omit & { onSelectionChange?: (start: number, end: number) => void; diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index ba406c3ddef6..3b4f66c32738 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -19,6 +19,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; +import variables from '@styles/variables'; import type {BaseTextInputRef} from '@src/components/TextInput/BaseTextInput/types'; import CONST from '@src/CONST'; import type {Route} from '@src/ROUTES'; @@ -259,6 +260,7 @@ function MoneyRequestAmountForm( > ({ }; }, + /* + * Returns styles for the text input container, with extraSpace allowing overflow without affecting the layout. + */ + getAutoGrowWidthInputContainerStyles: (width: number, extraSpace: number): ViewStyle => { + if (!!width && !!extraSpace) { + return {marginRight: -extraSpace, width: width + extraSpace}; + } + return {width}; + }, + /* * Returns the actual maxHeight of the auto-growing markdown text input. */ From e792e873087bfad0dd10f974a96e17f2706a7706 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 16 Dec 2024 15:19:14 +0700 Subject: [PATCH 050/110] add given word --- tests/ui/LHNItemsPresence.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index 67ad9ff0e816..a4a2bb9be88d 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -228,7 +228,7 @@ describe('SidebarLinksData', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`, [transactionViolation]); - // The RBR icon should be shown + // Then the RBR icon should be shown expect(screen.getByTestId('RBR Icon')).toBeOnTheScreen(); }); @@ -326,7 +326,7 @@ describe('SidebarLinksData', () => { lastReadTime: report.lastVisibleActionCreated, }); - // The report should not disappear in the sidebar because we are in the focus mode + // Then the report should not disappear in the sidebar because we are in the focus mode expect(getOptionRows()).toHaveLength(0); }); }); @@ -519,7 +519,7 @@ describe('SidebarLinksData', () => { // When the user is in focus mode await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); - // The report should not disappear in the sidebar because it's read + // Then the report should not disappear in the sidebar because it's read expect(getOptionRows()).toHaveLength(0); }); }); From a1b7179342277fa6f961dd93989aba83a4d691de Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 17 Dec 2024 11:46:54 +0800 Subject: [PATCH 051/110] fix typo --- tests/unit/APITest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts index d34a07a9b16e..7c7182059bec 100644 --- a/tests/unit/APITest.ts +++ b/tests/unit/APITest.ts @@ -581,7 +581,7 @@ describe('APITests', () => { }); }); - test('Read request should not stuck when SequentialQueue is paused an resumed', async () => { + test('Read request should not stuck when SequentialQueue is paused and resumed', async () => { // Given 2 WRITE requests and 1 READ request where the first write request pauses the SequentialQueue const xhr = jest.spyOn(HttpUtils, 'xhr').mockResolvedValueOnce({previousUpdateID: 1}); API.write('MockWriteCommandOne' as WriteCommand, {}); From 5a4018bd9f0364a55b8cbe0c4fa25c7f0e680e63 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 17 Dec 2024 11:54:39 +0800 Subject: [PATCH 052/110] add comment --- src/libs/Network/SequentialQueue.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 223966f26a6f..0a0c5281a3a9 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -198,6 +198,12 @@ function unpause() { const numberOfPersistedRequests = PersistedRequests.getAll().length || 0; Log.info(`[SequentialQueue] Unpausing the queue and flushing ${numberOfPersistedRequests} requests`); isQueuePaused = false; + + // A READ request will wait until all the WRITE requests are done, using isReadyPromise promise. + // When the queue is paused and then unpaused, we call flush which by defaults recreate the isReadyPromise. + // After all the WRITE requests are done, the isReadyPromise is resolved, but since it's a new instance of promise, + // the pending READ request never received the resolved callback. That's why we don't want to recreate + // the promise when unpausing the queue. flush(false); } From 17d298adeff0480b026174c3a7dac7d484530e8c Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 17 Dec 2024 10:20:37 -0500 Subject: [PATCH 053/110] Fix accidentally changed comment --- src/types/onyx/Report.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 881c416e01a8..1e4256c0e8ae 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -1,9 +1,10 @@ -import type {ValueOf} from 'type-fest'; +import type { ValueOf } from 'type-fest'; import type CONST from '@src/CONST'; import type ONYXKEYS from '@src/ONYXKEYS'; import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type * as OnyxCommon from './OnyxCommon'; -import type {PolicyReportField} from './Policy'; +import type { PolicyReportField } from './Policy'; + /** Preference that defines how regular the chat notifications are sent to the user */ type NotificationPreference = ValueOf; @@ -142,7 +143,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** The state number of the report */ stateNum?: ValueOf; - /** The state that the report is currently in */ + /** The status of the current report */ statusNum?: ValueOf; /** Which user role is capable of posting messages on the report */ @@ -256,4 +257,4 @@ type ReportCollectionDataSet = CollectionDataSet Date: Wed, 18 Dec 2024 05:28:25 +0700 Subject: [PATCH 054/110] fix lint --- .../MoneyRequestPreviewContent.tsx | 32 ++++++++----------- .../ReportActionItem/ReportPreview.tsx | 13 ++++---- src/libs/ReportActionsUtils.ts | 4 +-- src/libs/ReportUtils.ts | 2 +- src/libs/TransactionUtils/index.ts | 23 +++++++------ 5 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 4d59a6e2333f..e3f2eb7966e3 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -74,20 +74,20 @@ function MoneyRequestPreviewContent({ const route = useRoute>(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || '-1'}`); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [session] = useOnyx(ONYXKEYS.SESSION); - const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || '-1'}`); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); const policy = PolicyUtils.getPolicy(iouReport?.policyID); const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action); - const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : '-1'; + const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : undefined; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const sessionAccountID = session?.accountID; - const managerID = iouReport?.managerID ?? -1; - const ownerAccountID = iouReport?.ownerAccountID ?? -1; + const managerID = iouReport?.managerID ?? CONST.DEFAULT_NUMBER_ID; + const ownerAccountID = iouReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); const participantAccountIDs = @@ -117,9 +117,9 @@ function MoneyRequestPreviewContent({ const isOnHold = TransactionUtils.isOnHold(transaction); const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; const isPartialHold = isSettlementOrApprovalPartial && isOnHold; - const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID ?? '-1', transactionViolations, true); - const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID ?? '-1', transactionViolations, true) && ReportUtils.isPaidGroupPolicy(iouReport); - const hasWarningTypeViolations = TransactionUtils.hasWarningTypeViolation(transaction?.transactionID ?? '-1', transactionViolations, true); + const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID, transactionViolations, true); + const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID, transactionViolations, true) && ReportUtils.isPaidGroupPolicy(iouReport); + const hasWarningTypeViolations = TransactionUtils.hasWarningTypeViolation(transaction?.transactionID, transactionViolations, true); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); @@ -155,8 +155,8 @@ function MoneyRequestPreviewContent({ const shouldShowHoldMessage = !(isSettled && !isSettlementOrApprovalPartial) && !!transaction?.comment?.hold; const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`); - const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? ''); - const reviewingTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1'; + const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID, report?.parentReportActionID); + const reviewingTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined; /* Show the merchant for IOUs and expenses only if: @@ -253,10 +253,10 @@ function MoneyRequestPreviewContent({ if (TransactionUtils.isPending(transaction)) { return {shouldShow: true, messageIcon: Expensicons.CreditCardHourglass, messageDescription: translate('iou.transactionPending')}; } - if (TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', iouReport, policy)) { + if (TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID, iouReport, policy)) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('violations.brokenConnection530Error')}; } - if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1', transactionViolations))) { + if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID, transactionViolations))) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; } return {shouldShow: false}; @@ -301,12 +301,8 @@ function MoneyRequestPreviewContent({ // Clear the draft before selecting a different expense to prevent merging fields from the previous expense // (e.g., category, tag, tax) that may be not enabled/available in the new expense's policy. Transaction.abandonReviewDuplicateTransactions(); - const comparisonResult = TransactionUtils.compareDuplicateTransactionFields( - reviewingTransactionID, - transaction?.reportID ?? '', - transaction?.transactionID ?? reviewingTransactionID, - ); - Transaction.setReviewDuplicatesKey({...comparisonResult.keep, duplicates, transactionID: transaction?.transactionID ?? '', reportID: transaction?.reportID}); + const comparisonResult = TransactionUtils.compareDuplicateTransactionFields(reviewingTransactionID, transaction?.reportID, transaction?.transactionID ?? reviewingTransactionID); + Transaction.setReviewDuplicatesKey({...comparisonResult.keep, duplicates, transactionID: transaction?.transactionID, reportID: transaction?.reportID}); if ('merchant' in comparisonResult.change) { Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_MERCHANT_PAGE.getRoute(route.params?.threadReportID, backTo)); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index a1e3a1345b49..617f2dc9e099 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -103,7 +103,7 @@ function ReportPreview({ const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET); const [invoiceReceiverPolicy] = useOnyx( - `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : -1}`, + `${ONYXKEYS.COLLECTION.POLICY}${chatReport?.invoiceReceiver && 'policyID' in chatReport.invoiceReceiver ? chatReport.invoiceReceiver.policyID : CONST.DEFAULT_NUMBER_ID}`, ); const theme = useTheme(); const styles = useThemeStyles(); @@ -144,10 +144,10 @@ function ReportPreview({ const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport); const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = ReportUtils.getNonHeldAndFullAmount(iouReport, shouldShowPayButton); - const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID ?? ''); - const hasHeldExpenses = ReportUtils.hasHeldExpenses(iouReport?.reportID ?? ''); + const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID); + const hasHeldExpenses = ReportUtils.hasHeldExpenses(iouReport?.reportID); - const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? 0; + const managerID = iouReport?.managerID ?? action.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport); const iouSettled = ReportUtils.isSettled(iouReportID) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; @@ -189,9 +189,8 @@ function ReportPreview({ const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction})); const showRTERViolationMessage = numberOfRequests === 1 && - TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID ?? '-1', transactionViolations)); - const shouldShowBrokenConnectionViolation = - numberOfRequests === 1 && TransactionUtils.shouldShowBrokenConnectionViolation(allTransactions.at(0)?.transactionID ?? '-1', iouReport, policy); + TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID, transactionViolations)); + const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && TransactionUtils.shouldShowBrokenConnectionViolation(allTransactions.at(0)?.transactionID, iouReport, policy); let formattedMerchant = numberOfRequests === 1 ? TransactionUtils.getMerchant(allTransactions.at(0)) : null; const formattedDescription = numberOfRequests === 1 ? TransactionUtils.getDescription(allTransactions.at(0)) : null; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index dd17adbda338..1153f638ceef 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -929,8 +929,8 @@ function getLinkedTransactionID(reportActionOrID: string | OnyxEntry 0 && !reportTransactions.some((transaction) => !TransactionUtils.isOnHold(transaction)); } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 81a738f724e0..04c5d5519b5f 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -701,7 +701,7 @@ function hasMissingSmartscanFields(transaction: OnyxInputOrEntry): /** * Get all transaction violations of the transaction with given tranactionID. */ -function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection | null): TransactionViolations | null { +function getTransactionViolations(transactionID: string | undefined, transactionViolations: OnyxCollection | null): TransactionViolations | null { return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; } @@ -721,7 +721,7 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | /** * Check if there is broken connection violation. */ -function hasBrokenConnectionViolation(transactionID: string): boolean { +function hasBrokenConnectionViolation(transactionID?: string): boolean { const violations = getTransactionViolations(transactionID, allTransactionViolations); return !!violations?.find( (violation) => @@ -733,7 +733,7 @@ function hasBrokenConnectionViolation(transactionID: string): boolean { /** * Check if user should see broken connection violation warning. */ -function shouldShowBrokenConnectionViolation(transactionID: string, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean { +function shouldShowBrokenConnectionViolation(transactionID: string | undefined, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean { return ( hasBrokenConnectionViolation(transactionID) && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isOpenExpenseReport(report) || (ReportUtils.isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy))) @@ -879,7 +879,7 @@ function isOnHoldByTransactionID(transactionID: string): boolean { /** * Checks if any violations for the provided transaction are of type 'violation' */ -function hasViolation(transactionID: string, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { +function hasViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some( (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION && (showInReview === undefined || showInReview === (violation.showInReview ?? false)), ); @@ -888,7 +888,7 @@ function hasViolation(transactionID: string, transactionViolations: OnyxCollecti /** * Checks if any violations for the provided transaction are of type 'notice' */ -function hasNoticeTypeViolation(transactionID: string, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { +function hasNoticeTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some( (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.NOTICE && (showInReview === undefined || showInReview === (violation.showInReview ?? false)), ); @@ -897,7 +897,7 @@ function hasNoticeTypeViolation(transactionID: string, transactionViolations: On /** * Checks if any violations for the provided transaction are of type 'warning' */ -function hasWarningTypeViolation(transactionID: string, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { +function hasWarningTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { const violations = transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]; const warningTypeViolations = violations?.filter( @@ -1057,14 +1057,17 @@ function removeSettledAndApprovedTransactions(transactionIDs: string[]) { * 6. It returns the 'keep' and 'change' objects. */ -function compareDuplicateTransactionFields(reviewingTransactionID: string, reportID: string, selectedTransactionID?: string): {keep: Partial; change: FieldsToChange} { - const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${reviewingTransactionID}`]; - const duplicates = transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? []; - const transactions = removeSettledAndApprovedTransactions([reviewingTransactionID, ...duplicates]).map((item) => getTransaction(item)); +function compareDuplicateTransactionFields(reviewingTransactionID?: string, reportID?: string, selectedTransactionID?: string): {keep: Partial; change: FieldsToChange} { // eslint-disable-next-line @typescript-eslint/no-explicit-any const keep: Record = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any const change: Record = {}; + if (!reviewingTransactionID || !reportID) { + return {keep, change}; + } + const transactionViolations = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${reviewingTransactionID}`]; + const duplicates = transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? []; + const transactions = removeSettledAndApprovedTransactions([reviewingTransactionID, ...duplicates]).map((item) => getTransaction(item)); const fieldsToCompare: FieldsToCompare = { merchant: ['modifiedMerchant', 'merchant'], From b62ad1b310bd227f9affc6eb6549b0e63fa544ab Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 17 Dec 2024 18:00:32 -0500 Subject: [PATCH 055/110] Fix style --- src/types/onyx/Report.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 1e4256c0e8ae..7075c9f35085 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -1,10 +1,9 @@ -import type { ValueOf } from 'type-fest'; +import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type ONYXKEYS from '@src/ONYXKEYS'; import type CollectionDataSet from '@src/types/utils/CollectionDataSet'; import type * as OnyxCommon from './OnyxCommon'; -import type { PolicyReportField } from './Policy'; - +import type {PolicyReportField} from './Policy'; /** Preference that defines how regular the chat notifications are sent to the user */ type NotificationPreference = ValueOf; @@ -257,4 +256,4 @@ type ReportCollectionDataSet = CollectionDataSet Date: Wed, 18 Dec 2024 14:59:23 +1300 Subject: [PATCH 056/110] Update comment --- src/components/TextInput/BaseTextInput/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 70cfb028cef4..eef2d471a0d7 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -54,7 +54,7 @@ type CustomBaseTextInputProps = { */ autoGrow?: boolean; - /** If autoGrow is enabled, this reserves extra space for incoming characters to prevent flickering. */ + /** If autoGrow is enabled, this reserves extra space for incoming characters to prevent flickering on native platforms. */ autoGrowExtraSpace?: number; /** From 02d8152f633f507376fb0a0e3499810a58bbdae2 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 18 Dec 2024 12:51:57 +0700 Subject: [PATCH 057/110] add given word --- tests/ui/LHNItemsPresence.tsx | 64 +++++++++++++++++------------------ tests/unit/ReportUtilsTest.ts | 30 +++++++++++----- 2 files changed, 53 insertions(+), 41 deletions(-) diff --git a/tests/ui/LHNItemsPresence.tsx b/tests/ui/LHNItemsPresence.tsx index a4a2bb9be88d..162711b85499 100644 --- a/tests/ui/LHNItemsPresence.tsx +++ b/tests/ui/LHNItemsPresence.tsx @@ -126,11 +126,11 @@ describe('SidebarLinksData', () => { describe('Report that should be included in the LHN', () => { it('should display the current active report', async () => { - // When the SidebarLinks are rendered without a specified report ID. + // Given the SidebarLinks are rendered without a specified report ID. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report = createReport(); - // And the Onyx state is initialized with a report. + // When the Onyx state is initialized with a report. await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -149,14 +149,14 @@ describe('SidebarLinksData', () => { }); it('should display draft report', async () => { - // When SidebarLinks are rendered initially. + // Given SidebarLinks are rendered initially. LHNTestUtils.getDefaultRenderedSidebarLinks(); const draftReport = { ...createReport(false, [1, 2], 0), writeCapability: CONST.REPORT.WRITE_CAPABILITIES.ALL, }; - // And Onyx state is initialized with a draft report. + // When Onyx state is initialized with a draft report. await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${draftReport.reportID}`]: draftReport, }); @@ -174,11 +174,11 @@ describe('SidebarLinksData', () => { }); it('should display pinned report', async () => { - // When the SidebarLinks are rendered. + // Given the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report = createReport(false); - // And the report is initialized in Onyx. + // When the report is initialized in Onyx. await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -198,10 +198,10 @@ describe('SidebarLinksData', () => { }); it('should display the report with violations', async () => { - // When the SidebarLinks are rendered. + // Given the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); - // And the report is initialized in Onyx. + // When the report is initialized in Onyx. const report: Report = { ...createReport(true, undefined, undefined, CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, TEST_POLICY_ID), ownerAccountID: TEST_USER_ACCOUNT_ID, @@ -233,14 +233,14 @@ describe('SidebarLinksData', () => { }); it('should display the report awaiting user action', async () => { - // When the SidebarLinks are rendered. + // Given the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { ...createReport(false), hasOutstandingChildRequest: true, }; - // And the report is initialized in Onyx. + // When the report is initialized in Onyx. await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -253,7 +253,7 @@ describe('SidebarLinksData', () => { }); it('should display the archived report in the default mode', async () => { - // When the SidebarLinks are rendered. + // Given the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const archivedReport: Report = { ...createReport(false), @@ -272,7 +272,7 @@ describe('SidebarLinksData', () => { await waitForBatchedUpdatesWithAct(); - // And the user is in the default mode + // When the user is in the default mode await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.DEFAULT); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${archivedReport.reportID}`, reportNameValuePairs); @@ -281,11 +281,11 @@ describe('SidebarLinksData', () => { }); it('should display the selfDM report by default', async () => { - // When the SidebarLinks are rendered. + // Given the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report = createReport(true, undefined, undefined, undefined, CONST.REPORT.CHAT_TYPE.SELF_DM, undefined); - // And the selfDM is initialized in Onyx + // When the selfDM is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -295,7 +295,7 @@ describe('SidebarLinksData', () => { }); it('should display the unread report in the focus mode with the bold text', async () => { - // When the SidebarLinks are rendered. + // Given the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { ...createReport(undefined, undefined, undefined, undefined, undefined, true), @@ -309,7 +309,7 @@ describe('SidebarLinksData', () => { await waitForBatchedUpdatesWithAct(); - // And the user is in focus mode + // When the user is in focus mode await Onyx.merge(ONYXKEYS.NVP_PRIORITY_MODE, CONST.PRIORITY_MODE.GSD); // Then the report should appear in the sidebar because it's unread @@ -333,11 +333,11 @@ describe('SidebarLinksData', () => { describe('Report that should NOT be included in the LHN', () => { it('should not display report with no participants', async () => { - // When the SidebarLinks are rendered. + // Given the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report = LHNTestUtils.getFakeReport([]); - // And a report with no participants is initialized in Onyx. + // When a report with no participants is initialized in Onyx. await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -347,11 +347,11 @@ describe('SidebarLinksData', () => { }); it('should not display empty chat', async () => { - // When the SidebarLinks are rendered. + // Given the SidebarLinks are rendered. LHNTestUtils.getDefaultRenderedSidebarLinks(); const report = LHNTestUtils.getFakeReport([1, 2], 0); - // And a report with no messages is initialized in Onyx + // When a report with no messages is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -361,7 +361,7 @@ describe('SidebarLinksData', () => { }); it('should not display the report marked as hidden', async () => { - // When the SidebarLinks are rendered + // Given the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { ...createReport(), @@ -372,7 +372,7 @@ describe('SidebarLinksData', () => { }, }; - // And a report with notification preference set as hidden is initialized in Onyx + // When a report with notification preference set as hidden is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -382,11 +382,11 @@ describe('SidebarLinksData', () => { }); it('should not display the report has empty notification preference', async () => { - // When the SidebarLinks are rendered + // Given the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const report = createReport(false, [2]); - // And a report with empty notification preference is initialized in Onyx + // When a report with empty notification preference is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -396,7 +396,7 @@ describe('SidebarLinksData', () => { }); it('should not display the report the user cannot access due to policy restrictions', async () => { - // When the SidebarLinks are rendered + // Given the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { ...createReport(), @@ -404,7 +404,7 @@ describe('SidebarLinksData', () => { lastMessageText: 'fake last message', }; - // And a default room is initialized in Onyx + // When a default room is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -417,7 +417,7 @@ describe('SidebarLinksData', () => { }); it('should not display the single transaction thread', async () => { - // When the SidebarLinks are rendered + // Given the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const expenseReport = ReportUtils.buildOptimisticExpenseReport('212', '123', 100, 122, 'USD'); const expenseTransaction = TransactionUtils.buildOptimisticTransaction(100, 'USD', expenseReport.reportID); @@ -439,7 +439,7 @@ describe('SidebarLinksData', () => { const transactionThreadReport = ReportUtils.buildTransactionThread(expenseCreatedAction, expenseReport); expenseCreatedAction.childReportID = transactionThreadReport.reportID; - // And a single transaction thread is initialized in Onyx + // When a single transaction thread is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport.reportID}`]: transactionThreadReport, }); @@ -455,7 +455,7 @@ describe('SidebarLinksData', () => { }); it('should not display the report with parent message is pending removal', async () => { - // When the SidebarLinks are rendered + // Given the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const parentReport = createReport(); const report = createReport(); @@ -479,7 +479,7 @@ describe('SidebarLinksData', () => { report.parentReportID = parentReport.reportID; report.parentReportActionID = parentReportAction.reportActionID; - // And a report with parent message is pending removal is initialized in Onyx + // When a report with parent message is pending removal is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); @@ -493,7 +493,7 @@ describe('SidebarLinksData', () => { }); it('should not display the read report in the focus mode', async () => { - // When the SidebarLinks are rendered + // Given the SidebarLinks are rendered LHNTestUtils.getDefaultRenderedSidebarLinks(); const report: Report = { ...createReport(), @@ -501,7 +501,7 @@ describe('SidebarLinksData', () => { lastActorAccountID: TEST_USER_ACCOUNT_ID, }; - // And a read report that isn't empty is initialized in Onyx + // When a read report that isn't empty is initialized in Onyx await initializeState({ [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, }); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 9e3b177502cb..46035c0b8c83 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -545,7 +545,10 @@ describe('ReportUtils', () => { parentReportID: '101', policyID: paidPolicy.id, }; - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]); + const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [ + currentUserAccountID, + participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID, + ]); expect(moneyRequestOptions.length).toBe(0); }); }); @@ -558,7 +561,10 @@ describe('ReportUtils', () => { ...LHNTestUtils.getFakeReport(), chatType, }; - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]); + const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [ + currentUserAccountID, + participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID, + ]); return moneyRequestOptions.length === 1 && moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT); }); expect(onlyHaveSplitOption).toBe(true); @@ -605,7 +611,7 @@ describe('ReportUtils', () => { statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, managerID: currentUserAccountID, }; - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]); + const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true); }); @@ -618,7 +624,7 @@ describe('ReportUtils', () => { statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, managerID: currentUserAccountID, }; - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]); + const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true); }); @@ -667,7 +673,10 @@ describe('ReportUtils', () => { outputCurrency: '', isPolicyExpenseChatEnabled: false, } as const; - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]); + const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [ + currentUserAccountID, + participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID, + ]); expect(moneyRequestOptions.length).toBe(2); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK)).toBe(true); @@ -682,7 +691,7 @@ describe('ReportUtils', () => { statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, managerID: currentUserAccountID, }; - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]); + const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true); }); @@ -695,7 +704,7 @@ describe('ReportUtils', () => { statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED, managerID: currentUserAccountID, }; - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]); + const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]); expect(moneyRequestOptions.length).toBe(1); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true); }); @@ -738,7 +747,10 @@ describe('ReportUtils', () => { managerID: currentUserAccountID, ownerAccountID: currentUserAccountID, }; - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]); + const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, paidPolicy, [ + currentUserAccountID, + participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID, + ]); expect(moneyRequestOptions.length).toBe(2); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.TRACK)).toBe(true); @@ -752,7 +764,7 @@ describe('ReportUtils', () => { ...LHNTestUtils.getFakeReport(), type: CONST.REPORT.TYPE.CHAT, }; - const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? -1]); + const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, undefined, [currentUserAccountID, participantsAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID]); expect(moneyRequestOptions.length).toBe(3); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SPLIT)).toBe(true); expect(moneyRequestOptions.includes(CONST.IOU.TYPE.SUBMIT)).toBe(true); From 71d9b233271dc4f9949eea47ea9f008b4e9ff068 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Wed, 18 Dec 2024 13:34:38 +0700 Subject: [PATCH 058/110] fix lint --- tests/unit/ReportUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 46035c0b8c83..359beac3929d 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -914,7 +914,7 @@ describe('ReportUtils', () => { const reportActionCollectionDataSet = toCollectionDataSet( ONYXKEYS.COLLECTION.REPORT_ACTIONS, reportActions.map((reportAction) => ({[reportAction.reportActionID]: reportAction})), - (actions) => Object.values(actions).at(0)?.reportActionID ?? '', + (actions) => Object.values(actions).at(0)?.reportActionID ?? `${CONST.DEFAULT_NUMBER_ID}`, ); Onyx.multiSet({ ...reportCollectionDataSet, From e47e03614d759f0bcff058c47f6a2dd108c49505 Mon Sep 17 00:00:00 2001 From: Kevin Brian Bader Date: Wed, 18 Dec 2024 11:39:54 -0800 Subject: [PATCH 059/110] Bump react-native-live-markdown to 0.1.210 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index d887076adff1..2fb303c3eab4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ ], "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.209", + "@expensify/react-native-live-markdown": "0.1.210", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -3607,9 +3607,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.209", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.209.tgz", - "integrity": "sha512-u+RRY+Jog/llEu9T1v0okSLgRhG5jGlX9H1Je0A8HWv0439XFLnAWSvN2eQ2T7bvT8Yjdj5CcC0hkgJiB9oCQw==", + "version": "0.1.210", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.210.tgz", + "integrity": "sha512-CW9DY2yN/QJrqkD6+74s+kWQ9bhWQwd2jT+x5RCgyy5N2SdcoE8G8DGQQvmo6q94KcRkHIr/HsTVOyzACQ/nrw==", "hasInstallScript": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 4c4a16d7da71..83a821b2aea8 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.209", + "@expensify/react-native-live-markdown": "0.1.210", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", From 396f062efe55d1feb66b54800018a50d2ee821ca Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:16:27 +1300 Subject: [PATCH 060/110] Remove default IDs --- src/pages/iou/MoneyRequestAmountForm.tsx | 2 +- src/styles/utils/index.ts | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/MoneyRequestAmountForm.tsx b/src/pages/iou/MoneyRequestAmountForm.tsx index 3b4f66c32738..533c113c2a86 100644 --- a/src/pages/iou/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/MoneyRequestAmountForm.tsx @@ -314,7 +314,7 @@ function MoneyRequestAmountForm( addBankAccountRoute={bankAccountRoute} addDebitCardRoute={ROUTES.IOU_SEND_ADD_DEBIT_CARD} currency={currency ?? CONST.CURRENCY.USD} - policyID={policyID ?? '-1'} + policyID={policyID} style={[styles.w100, canUseTouchScreen ? styles.mt5 : styles.mt3]} buttonSize={CONST.DROPDOWN_BUTTON_SIZE.LARGE} kycWallAnchorAlignment={{ diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 0747fcb83132..8a41e6882365 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -290,7 +290,11 @@ function getBackgroundColorAndFill(backgroundColor: string, fill: string): SVGAv */ function getEReceiptColorCode(transaction: OnyxEntry): EReceiptColorName { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const transactionID = transaction?.parentTransactionID || transaction?.transactionID || ''; + const transactionID = transaction?.parentTransactionID || transaction?.transactionID; + + if (!transactionID) { + return CONST.ERECEIPT_COLORS.YELLOW; + } const colorHash = UserUtils.hashText(transactionID.trim(), eReceiptColors.length); From 65345c428110422bee470eefd817345f16fb4e43 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:17:09 +0700 Subject: [PATCH 061/110] Check visual viewport before setting maxHeight --- src/CONST.ts | 2 - src/components/ScreenWrapper.tsx | 37 +++++++++++++++---- src/hooks/useReadyWithDimensions.ts | 31 ---------------- .../step/IOURequestStepConfirmation.tsx | 1 - 4 files changed, 30 insertions(+), 41 deletions(-) delete mode 100644 src/hooks/useReadyWithDimensions.ts diff --git a/src/CONST.ts b/src/CONST.ts index 06e34b518c5c..e5c070676e49 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -319,8 +319,6 @@ const CONST = { NON_BILLABLE: 'nonBillable', }, - DIMENSIONS_CHANGED_DELAY: 1000, - // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT, chatTypes.INVOICE], ANDROID_PACKAGE_NAME, diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index d4f86b26c81f..20e914ef6b5a 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -8,7 +8,6 @@ import type {EdgeInsets} from 'react-native-safe-area-context'; import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useNetwork from '@hooks/useNetwork'; -import useReadyWithDimensions from '@hooks/useReadyWithDimensions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyledSafeAreaInsets from '@hooks/useStyledSafeAreaInsets'; import useTackInputFocus from '@hooks/useTackInputFocus'; @@ -17,6 +16,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types'; +import addViewportResizeListener from '@libs/VisualViewport'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; @@ -104,8 +104,6 @@ type ScreenWrapperProps = { /** Overrides the focus trap default settings */ focusTrapSettings?: FocusTrapForScreenProps['focusTrapSettings']; - - shouldListenToDimensionChanges?: boolean; }; type ScreenWrapperStatusContextType = { @@ -138,7 +136,6 @@ function ScreenWrapper( shouldShowOfflineIndicatorInWideScreen = false, shouldUseCachedViewportHeight = false, focusTrapSettings, - shouldListenToDimensionChanges = false, }: ScreenWrapperProps, ref: ForwardedRef, ) { @@ -164,10 +161,9 @@ function ScreenWrapper( const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); - const {isReady} = useReadyWithDimensions(Browser.isMobileSafari() && shouldListenToDimensionChanges); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const maxHeight = shouldEnableMaxHeight && isReady ? windowHeight : undefined; + const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; const route = useRoute(); @@ -175,6 +171,8 @@ function ScreenWrapper( return !!route?.params && 'singleNewDotEntry' in route.params && route.params.singleNewDotEntry === 'true'; }, [route?.params]); + const initVisualViewport = Browser.isSafari() && window.visualViewport ? window.visualViewport.height : undefined; + const [isMaxHeightReady, setIsMaxHeightReady] = useState(!Browser.isSafari()); UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => { NativeModules.HybridAppModule?.closeReactNativeApp(false, false); }); @@ -198,6 +196,26 @@ function ScreenWrapper( }), ).current; + useEffect(() => { + if (!Browser.isMobileSafari()) { + return; + } + + const handleViewportResize = () => { + if (!window.visualViewport) { + return; + } + + setIsMaxHeightReady(window.visualViewport.height === initVisualViewport); + }; + + const removeViewportResizeListener = addViewportResizeListener(handleViewportResize); + + return () => { + removeViewportResizeListener(); + }; + }, [initVisualViewport]); + useEffect(() => { // On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout const timeout = setTimeout(() => { @@ -281,7 +299,12 @@ function ScreenWrapper( {...keyboardDismissPanResponder.panHandlers} > diff --git a/src/hooks/useReadyWithDimensions.ts b/src/hooks/useReadyWithDimensions.ts deleted file mode 100644 index 897be8c558df..000000000000 --- a/src/hooks/useReadyWithDimensions.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {useEffect, useState} from 'react'; -import {Dimensions} from 'react-native'; -import CONST from '@src/CONST'; - -const useReadyWithDimensions = (isEnabled = true) => { - const [isReady, setIsReady] = useState(false); - - useEffect(() => { - if (!isEnabled) { - return; - } - const timer = setTimeout(() => { - setIsReady(true); - }, CONST.DIMENSIONS_CHANGED_DELAY); - - const handleDimensionChange = () => { - setIsReady(true); - }; - - const subscription = Dimensions.addEventListener('change', handleDimensionChange); - - return () => { - clearTimeout(timer); - subscription?.remove(); - }; - }, [isEnabled]); - - return {isReady}; -}; - -export default useReadyWithDimensions; diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index ac335f90dd79..15aef60bf3d3 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -607,7 +607,6 @@ function IOURequestStepConfirmation({ Date: Thu, 19 Dec 2024 13:35:58 +0700 Subject: [PATCH 062/110] Use visual viewport to detect keyboard show/hide --- src/components/ScreenWrapper.tsx | 30 +----------- src/utils/{keyboard.ts => keyboard/index.ts} | 0 src/utils/keyboard/index.website.ts | 48 ++++++++++++++++++++ 3 files changed, 49 insertions(+), 29 deletions(-) rename src/utils/{keyboard.ts => keyboard/index.ts} (100%) create mode 100644 src/utils/keyboard/index.website.ts diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 20e914ef6b5a..b57b12117736 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -16,7 +16,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList, RootStackParamList} from '@libs/Navigation/types'; -import addViewportResizeListener from '@libs/VisualViewport'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; @@ -171,8 +170,6 @@ function ScreenWrapper( return !!route?.params && 'singleNewDotEntry' in route.params && route.params.singleNewDotEntry === 'true'; }, [route?.params]); - const initVisualViewport = Browser.isSafari() && window.visualViewport ? window.visualViewport.height : undefined; - const [isMaxHeightReady, setIsMaxHeightReady] = useState(!Browser.isSafari()); UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => { NativeModules.HybridAppModule?.closeReactNativeApp(false, false); }); @@ -196,26 +193,6 @@ function ScreenWrapper( }), ).current; - useEffect(() => { - if (!Browser.isMobileSafari()) { - return; - } - - const handleViewportResize = () => { - if (!window.visualViewport) { - return; - } - - setIsMaxHeightReady(window.visualViewport.height === initVisualViewport); - }; - - const removeViewportResizeListener = addViewportResizeListener(handleViewportResize); - - return () => { - removeViewportResizeListener(); - }; - }, [initVisualViewport]); - useEffect(() => { // On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout const timeout = setTimeout(() => { @@ -299,12 +276,7 @@ function ScreenWrapper( {...keyboardDismissPanResponder.panHandlers} > diff --git a/src/utils/keyboard.ts b/src/utils/keyboard/index.ts similarity index 100% rename from src/utils/keyboard.ts rename to src/utils/keyboard/index.ts diff --git a/src/utils/keyboard/index.website.ts b/src/utils/keyboard/index.website.ts new file mode 100644 index 000000000000..f2ea9c673fdf --- /dev/null +++ b/src/utils/keyboard/index.website.ts @@ -0,0 +1,48 @@ +import {Keyboard} from 'react-native'; + +let isVisible = false; +const initialViewportHeight = window?.visualViewport?.height; + +const handleResize = () => { + const currentHeight = window?.visualViewport?.height; + + if (!currentHeight || !initialViewportHeight) { + return; + } + + if (currentHeight < initialViewportHeight) { + isVisible = true; + return; + } + + if (currentHeight === initialViewportHeight) { + isVisible = false; + } +}; + +window.visualViewport?.addEventListener('resize', handleResize); + +const dismiss = (): Promise => { + return new Promise((resolve) => { + if (!isVisible) { + resolve(); + return; + } + + const handleDismissResize = () => { + if (window.visualViewport?.height !== initialViewportHeight) { + return; + } + + window.visualViewport?.removeEventListener('resize', handleDismissResize); + return resolve(); + }; + + window.visualViewport?.addEventListener('resize', handleDismissResize); + Keyboard.dismiss(); + }); +}; + +const utils = {dismiss}; + +export default utils; From 9917a1baafb56a65c423c25b4b12f8987c61c3fe Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:37:29 +0700 Subject: [PATCH 063/110] Remove unused break line --- src/components/ScreenWrapper.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index b57b12117736..bb20b4abae11 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -159,9 +159,7 @@ function ScreenWrapper( const styles = useThemeStyles(); const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); - const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); - const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; const minHeight = shouldEnableMinHeight && !Browser.isSafari() ? initialHeight : undefined; From daf0d1bb5467b909f137cb0556c839c1476f69d9 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 20 Dec 2024 17:40:52 +0100 Subject: [PATCH 064/110] Improve feed removal offline --- src/libs/CardUtils.ts | 17 +++-- src/libs/actions/CompanyCards.ts | 66 +++++++++++++++++-- .../WorkspaceCompanyCardFeedSelectorPage.tsx | 2 + .../WorkspaceCompanyCardsPage.tsx | 2 +- .../WorkspaceCompanyCardsSettingsPage.tsx | 5 +- .../members/WorkspaceMemberNewCardPage.tsx | 3 + src/types/onyx/CardFeeds.ts | 8 +-- 7 files changed, 89 insertions(+), 14 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 9a71480019a6..2785bb294405 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -252,16 +252,25 @@ function isCustomFeed(feed: CompanyCardFeed): boolean { } function getCompanyFeeds(cardFeeds: OnyxEntry): CompanyFeeds { - return {...cardFeeds?.settings?.companyCards, ...cardFeeds?.settings?.oAuthAccountDetails}; + const allFeeds = {...cardFeeds?.settings?.companyCards, ...cardFeeds?.settings?.oAuthAccountDetails}; + const {[CONST.EXPENSIFY_CARD.BANK as CompanyCardFeed]: expensifyFeed, ...companyFeeds} = allFeeds; + return companyFeeds; } -function removeExpensifyCardFromCompanyCards(cardFeeds: OnyxEntry): CompanyFeeds { +function removeExpensifyCardFromCompanyCards(cardFeeds: OnyxEntry, shouldFilterOutRemovedFeeds = false): CompanyFeeds { if (!cardFeeds) { return {}; } const companyCards = getCompanyFeeds(cardFeeds); - return Object.fromEntries(Object.entries(companyCards).filter(([key]) => key !== CONST.EXPENSIFY_CARD.BANK)); + return Object.fromEntries( + Object.entries(companyCards).filter(([key, value]) => { + if (shouldFilterOutRemovedFeeds && value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return false; + } + return key !== CONST.EXPENSIFY_CARD.BANK; + }), + ); } function getCardFeedName(feedType: CompanyCardFeed): string { @@ -348,7 +357,7 @@ const getCorrectStepForSelectedBank = (selectedBank: ValueOf, cardFeeds: OnyxEntry): CompanyCardFeed | undefined { - const defaultFeed = Object.keys(removeExpensifyCardFromCompanyCards(cardFeeds)).at(0) as CompanyCardFeed | undefined; + const defaultFeed = Object.keys(removeExpensifyCardFromCompanyCards(cardFeeds, true)).at(0) as CompanyCardFeed | undefined; return lastSelectedFeed ?? defaultFeed; } diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index 8e83b9192a71..ee059086adde 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -151,10 +151,14 @@ function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, API.write(WRITE_COMMANDS.SET_COMPANY_CARD_TRANSACTION_LIABILITY, parameters, onyxData); } -function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: number, bankName: CompanyCardFeed, feedToOpen?: CompanyCardFeed) { +function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: number, bankName: CompanyCardFeed, cardIDs: string[], feedToOpen?: CompanyCardFeed) { const authToken = NetworkStore.getAuthToken(); const isCustomFeed = CardUtils.isCustomFeed(bankName); - const feedUpdates = {[bankName]: null}; + const optimisticFeedUpdates = {[bankName]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}}; + const successFeedUpdates = {[bankName]: null}; + const failureFeedUpdates = {[bankName]: {pendingAction: null, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')}}; + const optimisticCardUpdates = Object.fromEntries(cardIDs.map((cardID) => [cardID, {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}])); + const successAndFailureCardUpdates = Object.fromEntries(cardIDs.map((cardID) => [cardID, {pendingAction: null}])); const optimisticData: OnyxUpdate[] = [ { @@ -162,13 +166,67 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { settings: { - ...(isCustomFeed ? {companyCards: feedUpdates} : {oAuthAccountDetails: feedUpdates}), + ...(isCustomFeed ? {companyCards: optimisticFeedUpdates} : {oAuthAccountDetails: optimisticFeedUpdates}), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`, + value: optimisticCardUpdates, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: optimisticCardUpdates, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, + value: { + settings: { + ...(isCustomFeed ? {companyCards: successFeedUpdates} : {oAuthAccountDetails: successFeedUpdates, companyCards: successFeedUpdates}), companyCardNicknames: { [bankName]: null, }, }, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`, + value: successAndFailureCardUpdates, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: successAndFailureCardUpdates, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, + value: { + settings: { + ...(isCustomFeed ? {companyCards: failureFeedUpdates} : {oAuthAccountDetails: failureFeedUpdates}), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${bankName}`, + value: successAndFailureCardUpdates, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.CARD_LIST, + value: successAndFailureCardUpdates, + }, ]; if (feedToOpen) { @@ -185,7 +243,7 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu bankName, }; - API.write(WRITE_COMMANDS.DELETE_COMPANY_CARD_FEED, parameters, {optimisticData}); + API.write(WRITE_COMMANDS.DELETE_COMPANY_CARD_FEED, parameters, {optimisticData, successData, failureData}); } function assignWorkspaceCompanyCard(policyID: string, data?: Partial) { diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx index 723242c55494..d8109d7b7caf 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx @@ -49,6 +49,8 @@ function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedS text: CardUtils.getCustomOrFormattedFeedName(feed, cardFeeds?.settings?.companyCardNicknames), keyForList: feed, isSelected: feed === selectedFeed, + isDisabled: companyFeeds[feed]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + pendingAction: companyFeeds[feed]?.pendingAction, brickRoadIndicator: companyFeeds[feed]?.errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, canShowSeveralIndicators: !!companyFeeds[feed]?.errors, leftElement: ( diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 392138a2d8d1..b424c69a9d41 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -51,7 +51,7 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) { const companyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); const selectedFeedData = selectedFeed && companyCards[selectedFeed]; - const isNoFeed = isEmptyObject(companyCards) && !selectedFeedData; + const isNoFeed = !selectedFeedData; const isPending = !!selectedFeedData?.pending; const isFeedAdded = !isPending && !isNoFeed; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx index 58c79d41d3c9..13df27f84f83 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx @@ -42,6 +42,7 @@ function WorkspaceCompanyCardsSettingsPage({ const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we want to run the hook only once to escape unexpected feed change const selectedFeed = useMemo(() => CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds), []); + const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`); const feedName = CardUtils.getCustomOrFormattedFeedName(selectedFeed, cardFeeds?.settings?.companyCardNicknames); const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds); const liabilityType = selectedFeed && companyFeeds[selectedFeed]?.liabilityType; @@ -53,8 +54,10 @@ function WorkspaceCompanyCardsSettingsPage({ const deleteCompanyCardFeed = () => { if (selectedFeed) { + const {cardList, ...cards} = cardsList ?? {}; + const cardIDs = Object.keys(cards); const feedToOpen = (Object.keys(companyFeeds) as CompanyCardFeed[]).filter((feed) => feed !== selectedFeed).at(0); - CompanyCards.deleteWorkspaceCompanyCardFeed(policyID, workspaceAccountID, selectedFeed, feedToOpen); + CompanyCards.deleteWorkspaceCompanyCardFeed(policyID, workspaceAccountID, selectedFeed, cardIDs, feedToOpen); } setDeleteCompanyCardConfirmModalVisible(false); Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx index d53d8a558276..3600963b455e 100644 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx @@ -50,6 +50,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew const memberLogin = personalDetails?.[accountID]?.login ?? ''; const memberName = personalDetails?.[accountID]?.firstName ? personalDetails?.[accountID]?.firstName : personalDetails?.[accountID]?.login; const availableCompanyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); + const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds); const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`); const filteredCardList = CardUtils.getFilteredCardList(list, cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed as CompanyCardFeed]); @@ -101,6 +102,8 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew value: key, text: CardUtils.getCustomOrFormattedFeedName(key, cardFeeds?.settings?.companyCardNicknames), keyForList: key, + isDisabled: companyFeeds[key]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + pendingAction: companyFeeds[key]?.pendingAction, isSelected: selectedFeed === key, leftElement: ( ; /** Direct card feed data */ -type DirectCardFeedData = { +type DirectCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** List of accounts */ accountList: string[]; @@ -58,7 +58,7 @@ type DirectCardFeedData = { /** Broken connection errors */ errors?: OnyxCommon.Errors; -}; +}>; /** Card feed data */ type CardFeedData = CustomCardFeedData | DirectCardFeedData; From c3accb3f17732be79046f2df625ef5c616341102 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 23 Dec 2024 10:38:49 +0100 Subject: [PATCH 065/110] Fix lint errors --- src/libs/CardUtils.ts | 4 +- src/libs/actions/CompanyCards.ts | 134 +++++++++--------- .../WorkspaceCompanyCardsSettingsPage.tsx | 2 +- 3 files changed, 71 insertions(+), 69 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 2785bb294405..c2e8744e0d5d 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -203,8 +203,8 @@ function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry, personalDetails: OnyxEntry): Card[] { const {cardList, ...cards} = cardsList ?? {}; return Object.values(cards).sort((cardA: Card, cardB: Card) => { - const userA = personalDetails?.[cardA.accountID ?? '-1'] ?? {}; - const userB = personalDetails?.[cardB.accountID ?? '-1'] ?? {}; + const userA = cardA.accountID ? personalDetails?.[cardA.accountID] ?? {} : {}; + const userB = cardB.accountID ? personalDetails?.[cardB.accountID] ?? {} : {}; const aName = PersonalDetailsUtils.getDisplayNameOrDefault(userA); const bName = PersonalDetailsUtils.getDisplayNameOrDefault(userB); diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index ee059086adde..f145bfd54afa 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -252,7 +252,7 @@ function assignWorkspaceCompanyCard(policyID: string, data?: Partial Date: Mon, 23 Dec 2024 10:48:37 +0100 Subject: [PATCH 066/110] Fix tests --- tests/unit/CardUtilsTest.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index c294b068a62d..4bd3ba28a88d 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -2,7 +2,6 @@ import type {OnyxCollection} from 'react-native-onyx'; import CONST from '@src/CONST'; import * as CardUtils from '@src/libs/CardUtils'; import type * as OnyxTypes from '@src/types/onyx'; -import type {CompanyFeeds} from '@src/types/onyx/CardFeeds'; const shortDate = '0924'; const shortDateSlashed = '09/24'; @@ -115,7 +114,6 @@ const customFeedCardsList = { '480801XXXXXX2566': 'ENCRYPTED_CARD_NUMBER', }, } as unknown as OnyxTypes.WorkspaceCardsList; -const allFeeds: CompanyFeeds = {...customFeeds, ...directFeeds}; const customFeedName = 'Custom feed name'; const cardFeedsCollection: OnyxCollection = { @@ -200,7 +198,7 @@ describe('CardUtils', () => { describe('getCompanyFeeds', () => { it('Should return both custom and direct feeds if exists', () => { const companyFeeds = CardUtils.getCompanyFeeds(cardFeedsCollection.FAKE_ID_1); - expect(companyFeeds).toStrictEqual(allFeeds); + expect(companyFeeds).toStrictEqual({...directFeeds, ...customFeedsWithoutExpensifyBank}); }); it('Should return direct feeds only since custom feeds are not exist', () => { @@ -210,7 +208,7 @@ describe('CardUtils', () => { it('Should return custom feeds only since direct feeds are not exist', () => { const companyFeeds = CardUtils.getCompanyFeeds(cardFeedsCollection.FAKE_ID_3); - expect(companyFeeds).toStrictEqual(customFeeds); + expect(companyFeeds).toStrictEqual(customFeedsWithoutExpensifyBank); }); it('Should return empty object if undefined is passed', () => { From 470b8350d3590ba28168c693e1d004af68db4b24 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 23 Dec 2024 11:41:59 +0100 Subject: [PATCH 067/110] Code improvement --- src/libs/actions/CompanyCards.ts | 132 +++++++++--------- .../WorkspaceCompanyCardDetailsPage.tsx | 4 +- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index f145bfd54afa..6d25d0135a83 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -300,74 +300,72 @@ function assignWorkspaceCompanyCard(policyID: string, data?: Partial { setIsUnassignModalVisible(false); - CompanyCards.unassignWorkspaceCompanyCard(workspaceAccountID, bank, card); + if (card) { + CompanyCards.unassignWorkspaceCompanyCard(workspaceAccountID, bank, card); + } Navigation.goBack(); }; From b75a4cfe2e1ffc3311b4fbdefe7399d4c1b00d2f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 23 Dec 2024 13:33:31 +0100 Subject: [PATCH 068/110] Fix lint --- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 85a5d2372ee9..55cc6e5e2e16 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -81,7 +81,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const isSelectedMemberCurrentUser = accountID === currentUserPersonalDetails?.accountID; const isCurrentUserAdmin = policy?.employeeList?.[personalDetails?.[currentUserPersonalDetails?.accountID]?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN; const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login; - const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? -1] ?? ({} as PersonalDetails); + const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails); const policyOwnerDisplayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? ''; const hasMultipleFeeds = Object.values(CardUtils.getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0; const paymentAccountID = cardSettings?.paymentBankAccountID ?? 0; From 785eccd15268f0fbefb3638fb216b2e29061aca9 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 23 Dec 2024 14:01:09 +0100 Subject: [PATCH 069/110] Improve getCompanyFeeds function and get rid of removeExpensifyCardFromCompanyCards --- src/libs/CardUtils.ts | 18 +---- src/libs/actions/CompanyCards.ts | 12 ++-- .../workspace/WorkspaceMoreFeaturesPage.tsx | 2 +- .../WorkspaceCompanyCardFeedSelectorPage.tsx | 3 +- .../WorkspaceCompanyCardsPage.tsx | 2 +- .../members/WorkspaceMemberNewCardPage.tsx | 3 +- tests/unit/CardUtilsTest.ts | 67 ++++++++++--------- 7 files changed, 48 insertions(+), 59 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index c2e8744e0d5d..e26618fd3a6d 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -251,20 +251,9 @@ function isCustomFeed(feed: CompanyCardFeed): boolean { return [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD, CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX].some((value) => value === feed); } -function getCompanyFeeds(cardFeeds: OnyxEntry): CompanyFeeds { - const allFeeds = {...cardFeeds?.settings?.companyCards, ...cardFeeds?.settings?.oAuthAccountDetails}; - const {[CONST.EXPENSIFY_CARD.BANK as CompanyCardFeed]: expensifyFeed, ...companyFeeds} = allFeeds; - return companyFeeds; -} - -function removeExpensifyCardFromCompanyCards(cardFeeds: OnyxEntry, shouldFilterOutRemovedFeeds = false): CompanyFeeds { - if (!cardFeeds) { - return {}; - } - - const companyCards = getCompanyFeeds(cardFeeds); +function getCompanyFeeds(cardFeeds: OnyxEntry, shouldFilterOutRemovedFeeds = false): CompanyFeeds { return Object.fromEntries( - Object.entries(companyCards).filter(([key, value]) => { + Object.entries(cardFeeds?.settings?.companyCards ?? {}).filter(([key, value]) => { if (shouldFilterOutRemovedFeeds && value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { return false; } @@ -357,7 +346,7 @@ const getCorrectStepForSelectedBank = (selectedBank: ValueOf, cardFeeds: OnyxEntry): CompanyCardFeed | undefined { - const defaultFeed = Object.keys(removeExpensifyCardFromCompanyCards(cardFeeds, true)).at(0) as CompanyCardFeed | undefined; + const defaultFeed = Object.keys(getCompanyFeeds(cardFeeds, true)).at(0) as CompanyCardFeed | undefined; return lastSelectedFeed ?? defaultFeed; } @@ -419,7 +408,6 @@ export { getSelectedFeed, getCorrectStepForSelectedBank, getCustomOrFormattedFeedName, - removeExpensifyCardFromCompanyCards, getFilteredCardList, hasOnlyOneCardToAssign, checkIfNewFeedConnected, diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index 6d25d0135a83..4fba1e89be2f 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -124,7 +124,6 @@ function setWorkspaceCompanyCardFeedName(policyID: string, workspaceAccountID: n function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, policyID: string, bankName: CompanyCardFeed, liabilityType: string) { const authToken = NetworkStore.getAuthToken(); - const isCustomFeed = CardUtils.isCustomFeed(bankName); const feedUpdates = { [bankName]: {liabilityType}, }; @@ -135,7 +134,7 @@ function setWorkspaceCompanyCardTransactionLiability(workspaceAccountID: number, onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { - settings: isCustomFeed ? {companyCards: feedUpdates} : {oAuthAccountDetails: feedUpdates}, + settings: {companyCards: feedUpdates}, }, }, ], @@ -166,7 +165,7 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { settings: { - ...(isCustomFeed ? {companyCards: optimisticFeedUpdates} : {oAuthAccountDetails: optimisticFeedUpdates}), + companyCards: optimisticFeedUpdates, }, }, }, @@ -213,7 +212,7 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { settings: { - ...(isCustomFeed ? {companyCards: failureFeedUpdates} : {oAuthAccountDetails: failureFeedUpdates}), + companyCards: failureFeedUpdates, }, }, }, @@ -377,7 +376,6 @@ function unassignWorkspaceCompanyCard(workspaceAccountID: number, bankName: stri function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, bankName: CompanyCardFeed) { const authToken = NetworkStore.getAuthToken(); - const isCustomFeed = CardUtils.isCustomFeed(bankName); const optimisticFeedUpdates = {[bankName]: {errors: null}}; const failureFeedUpdates = {[bankName]: {errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR}}}; @@ -416,7 +414,7 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { - settings: isCustomFeed ? {companyCards: optimisticFeedUpdates} : {oAuthAccountDetails: optimisticFeedUpdates}, + settings: {companyCards: optimisticFeedUpdates}, }, }, ]; @@ -483,7 +481,7 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, value: { - settings: isCustomFeed ? {companyCards: failureFeedUpdates} : {oAuthAccountDetails: failureFeedUpdates}, + settings: {companyCards: failureFeedUpdates}, }, }, ]; diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index ddcb89064c7e..53ef127193ed 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -131,7 +131,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro subtitleTranslationKey: 'workspace.moreFeatures.companyCards.subtitle', isActive: policy?.areCompanyCardsEnabled ?? false, pendingAction: policy?.pendingFields?.areCompanyCardsEnabled, - disabled: !isEmptyObject(CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds)), + disabled: !isEmptyObject(CardUtils.getCompanyFeeds(cardFeeds)), action: (isEnabled: boolean) => { if (!policyID) { return; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx index d8109d7b7caf..29d0dfb0c6af 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx @@ -42,9 +42,8 @@ function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedS const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`); const selectedFeed = CardUtils.getSelectedFeed(lastSelectedFeed, cardFeeds); const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds); - const availableCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); - const feeds: CardFeedListItem[] = (Object.keys(availableCards) as CompanyCardFeed[]).map((feed) => ({ + const feeds: CardFeedListItem[] = (Object.keys(companyFeeds) as CompanyCardFeed[]).map((feed) => ({ value: feed, text: CardUtils.getCustomOrFormattedFeedName(feed, cardFeeds?.settings?.companyCardNicknames), keyForList: feed, diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index b424c69a9d41..ea3a0e0f7071 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -49,7 +49,7 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) { const filteredCardList = CardUtils.getFilteredCardList(cardsList, selectedFeed ? cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed] : undefined); - const companyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); + const companyCards = CardUtils.getCompanyFeeds(cardFeeds); const selectedFeedData = selectedFeed && companyCards[selectedFeed]; const isNoFeed = !selectedFeedData; const isPending = !!selectedFeedData?.pending; diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx index 3600963b455e..afa76e63a815 100644 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx @@ -49,7 +49,6 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew const accountID = Number(route.params.accountID); const memberLogin = personalDetails?.[accountID]?.login ?? ''; const memberName = personalDetails?.[accountID]?.firstName ? personalDetails?.[accountID]?.firstName : personalDetails?.[accountID]?.login; - const availableCompanyCards = CardUtils.removeExpensifyCardFromCompanyCards(cardFeeds); const companyFeeds = CardUtils.getCompanyFeeds(cardFeeds); const [list] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`); @@ -98,7 +97,7 @@ function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNew setShouldShowError(false); }; - const companyCardFeeds: CardFeedListItem[] = (Object.keys(availableCompanyCards) as CompanyCardFeed[]).map((key) => ({ + const companyCardFeeds: CardFeedListItem[] = (Object.keys(companyFeeds) as CompanyCardFeed[]).map((key) => ({ value: key, text: CardUtils.getCustomOrFormattedFeedName(key, cardFeeds?.settings?.companyCardNicknames), keyForList: key, diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 4bd3ba28a88d..2685a77836b3 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -12,7 +12,7 @@ const longDateHyphen = '09-2024'; const expectedMonth = '09'; const expectedYear = '2024'; -const customFeeds = { +const companyCardsCustomFeedSettings = { [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: { pending: true, }, @@ -23,7 +23,7 @@ const customFeeds = { liabilityType: 'personal', }, }; -const customFeedsWithoutExpensifyBank = { +const companyCardsCustomFeedSettingsWithoutExpensifyBank = { [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: { pending: true, }, @@ -31,7 +31,25 @@ const customFeedsWithoutExpensifyBank = { liabilityType: 'personal', }, }; -const directFeeds = { +const companyCardsDirectFeedSettings = { + [CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]: { + liabilityType: 'personal', + }, + [CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]: { + liabilityType: 'personal', + }, +}; +const companyCardsSettingsWithoutExpensifyBank = { + [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: { + pending: true, + }, + [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: { + liabilityType: 'personal', + }, + ...companyCardsDirectFeedSettings, +}; + +const oAuthAccountDetails = { [CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]: { accountList: ['CREDIT CARD...6607', 'CREDIT CARD...5501'], credentials: 'xxxxx', @@ -117,23 +135,27 @@ const customFeedCardsList = { const customFeedName = 'Custom feed name'; const cardFeedsCollection: OnyxCollection = { + // Policy with both custom and direct feeds FAKE_ID_1: { settings: { companyCardNicknames: { [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: customFeedName, }, - companyCards: customFeeds, - oAuthAccountDetails: directFeeds, + companyCards: {...companyCardsCustomFeedSettings, ...companyCardsDirectFeedSettings}, + oAuthAccountDetails, }, }, + // Policy with direct feeds only FAKE_ID_2: { settings: { - oAuthAccountDetails: directFeeds, + companyCards: companyCardsDirectFeedSettings, + oAuthAccountDetails, }, }, + // Policy with custom feeds only FAKE_ID_3: { settings: { - companyCards: customFeeds, + companyCards: companyCardsCustomFeedSettings, }, }, }; @@ -196,19 +218,19 @@ describe('CardUtils', () => { }); describe('getCompanyFeeds', () => { - it('Should return both custom and direct feeds if exists', () => { + it('Should return both custom and direct feeds with filtered out "Expensify Card" bank', () => { const companyFeeds = CardUtils.getCompanyFeeds(cardFeedsCollection.FAKE_ID_1); - expect(companyFeeds).toStrictEqual({...directFeeds, ...customFeedsWithoutExpensifyBank}); + expect(companyFeeds).toStrictEqual(companyCardsSettingsWithoutExpensifyBank); }); it('Should return direct feeds only since custom feeds are not exist', () => { const companyFeeds = CardUtils.getCompanyFeeds(cardFeedsCollection.FAKE_ID_2); - expect(companyFeeds).toStrictEqual(directFeeds); + expect(companyFeeds).toStrictEqual(companyCardsDirectFeedSettings); }); - it('Should return custom feeds only since direct feeds are not exist', () => { + it('Should return custom feeds only with filtered out "Expensify Card" bank since direct feeds are not exist', () => { const companyFeeds = CardUtils.getCompanyFeeds(cardFeedsCollection.FAKE_ID_3); - expect(companyFeeds).toStrictEqual(customFeedsWithoutExpensifyBank); + expect(companyFeeds).toStrictEqual(companyCardsCustomFeedSettingsWithoutExpensifyBank); }); it('Should return empty object if undefined is passed', () => { @@ -217,23 +239,6 @@ describe('CardUtils', () => { }); }); - describe('removeExpensifyCardFromCompanyCards', () => { - it('Should return custom feeds without filtered out "Expensify Card" bank', () => { - const companyFeeds = CardUtils.removeExpensifyCardFromCompanyCards(cardFeedsCollection.FAKE_ID_3); - expect(companyFeeds).toStrictEqual(customFeedsWithoutExpensifyBank); - }); - - it('Should return direct feeds without any updates, since there were no "Expensify Card" bank', () => { - const companyFeeds = CardUtils.removeExpensifyCardFromCompanyCards(cardFeedsCollection.FAKE_ID_2); - expect(companyFeeds).toStrictEqual(directFeeds); - }); - - it('Should return empty object if undefined is passed', () => { - const companyFeeds = CardUtils.removeExpensifyCardFromCompanyCards(undefined); - expect(companyFeeds).toStrictEqual({}); - }); - }); - describe('getSelectedFeed', () => { it('Should return last selected custom feed', () => { const lastSelectedCustomFeed = CONST.COMPANY_CARD.FEED_BANK_NAME.VISA; @@ -355,13 +360,13 @@ describe('CardUtils', () => { }); it('Should return filtered direct feed cards list with a single card', () => { - const cardsList = CardUtils.getFilteredCardList(directFeedCardsSingleList, directFeeds[CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]); + const cardsList = CardUtils.getFilteredCardList(directFeedCardsSingleList, oAuthAccountDetails[CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE]); // eslint-disable-next-line @typescript-eslint/naming-convention expect(cardsList).toStrictEqual({'CREDIT CARD...6607': 'CREDIT CARD...6607'}); }); it('Should return filtered direct feed cards list with multiple cards', () => { - const cardsList = CardUtils.getFilteredCardList(directFeedCardsMultipleList, directFeeds[CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]); + const cardsList = CardUtils.getFilteredCardList(directFeedCardsMultipleList, oAuthAccountDetails[CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]); // eslint-disable-next-line @typescript-eslint/naming-convention expect(cardsList).toStrictEqual({'CREDIT CARD...1233': 'CREDIT CARD...1233', 'CREDIT CARD...3333': 'CREDIT CARD...3333', 'CREDIT CARD...7788': 'CREDIT CARD...7788'}); }); From 75299dcacb2bc92385df15b24a1066165fa2488c Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 23 Dec 2024 14:03:57 +0100 Subject: [PATCH 070/110] Lint fixes --- src/pages/workspace/WorkspaceMoreFeaturesPage.tsx | 2 +- .../workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx | 2 +- src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 53ef127193ed..6c6e70b8cc28 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -74,7 +74,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro !!policy?.connections?.xero?.config?.importTaxRates || !!policy?.connections?.netsuite?.options?.config?.syncOptions?.syncTax; const policyID = policy?.id; - const workspaceAccountID = policy?.workspaceAccountID ?? -1; + const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID.toString()}_${CONST.EXPENSIFY_CARD.BANK}`); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID.toString()}`); const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx index b016c86c112a..fb49ece0feb0 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx @@ -58,7 +58,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag const card = allBankCards?.[cardID]; const cardBank = card?.bank ?? ''; - const cardholder = personalDetails?.[card?.accountID ?? -1]; + const cardholder = personalDetails?.[card?.accountID ?? CONST.DEFAULT_NUMBER_ID]; const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(cardholder); const exportMenuItem = getExportMenuItem(connectedIntegration, policyID, translate, policy, card); diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 55cc6e5e2e16..c53fe1fb2e7f 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -84,7 +84,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const ownerDetails = personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails); const policyOwnerDisplayName = formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? ''; const hasMultipleFeeds = Object.values(CardUtils.getCompanyFeeds(cardFeeds)).filter((feed) => !feed.pending).length > 0; - const paymentAccountID = cardSettings?.paymentBankAccountID ?? 0; + const paymentAccountID = cardSettings?.paymentBankAccountID ?? CONST.DEFAULT_NUMBER_ID; useEffect(() => { CompanyCards.openPolicyCompanyCardsPage(policyID, workspaceAccountID); From d11c081836b588e49d98f67a1405d2d0a110aa81 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 23 Dec 2024 14:37:44 +0100 Subject: [PATCH 071/110] Buf fix --- src/libs/actions/CompanyCards.ts | 12 +++++------- .../WorkspaceCompanyCardsSettingsPage.tsx | 4 +++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index 4fba1e89be2f..d0e19398c257 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -228,13 +228,11 @@ function deleteWorkspaceCompanyCardFeed(policyID: string, workspaceAccountID: nu }, ]; - if (feedToOpen) { - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, - value: feedToOpen, - }); - } + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, + value: feedToOpen ?? null, + }); const parameters = { authToken, diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx index 5abd95f7ad9c..60774c448546 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx @@ -56,7 +56,9 @@ function WorkspaceCompanyCardsSettingsPage({ if (selectedFeed) { const {cardList, ...cards} = cardsList ?? {}; const cardIDs = Object.keys(cards); - const feedToOpen = (Object.keys(companyFeeds) as CompanyCardFeed[]).filter((feed) => feed !== selectedFeed).at(0); + const feedToOpen = (Object.keys(companyFeeds) as CompanyCardFeed[]) + .filter((feed) => feed !== selectedFeed && companyFeeds[feed]?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + .at(0); CompanyCards.deleteWorkspaceCompanyCardFeed(policyID, workspaceAccountID, selectedFeed, cardIDs, feedToOpen); } setDeleteCompanyCardConfirmModalVisible(false); From ad960d3dbc601f334eda15bd053436c972d8b014 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Mon, 23 Dec 2024 22:22:46 +0700 Subject: [PATCH 072/110] Update tests/unit/ReportUtilsTest.ts Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- tests/unit/ReportUtilsTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 359beac3929d..351e9e470f11 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -914,7 +914,7 @@ describe('ReportUtils', () => { const reportActionCollectionDataSet = toCollectionDataSet( ONYXKEYS.COLLECTION.REPORT_ACTIONS, reportActions.map((reportAction) => ({[reportAction.reportActionID]: reportAction})), - (actions) => Object.values(actions).at(0)?.reportActionID ?? `${CONST.DEFAULT_NUMBER_ID}`, + (actions) => Object.values(actions).at(0)?.reportActionID, ); Onyx.multiSet({ ...reportCollectionDataSet, From 8286cc316b441506cccb27f9869d2308116480d1 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 24 Dec 2024 13:55:11 +0700 Subject: [PATCH 073/110] fix style offline indicattor --- src/components/ScreenWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 24dbf482cdc7..464509b0a947 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -298,7 +298,7 @@ function ScreenWrapper( style={[offlineIndicatorStyle]} containerStyles={ includeSafeAreaPaddingBottom - ? [] + ? [styles.offlineIndicatorMobile] : [styles.offlineIndicatorMobile, {paddingBottom: paddingBottom + styles.offlineIndicatorMobile.paddingBottom}] } /> From b89a319bf4238f799ffde0c0242b1306822c4a79 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 24 Dec 2024 07:12:49 -0800 Subject: [PATCH 074/110] Remove field no longer present on type --- tests/data/Invoice.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/data/Invoice.ts b/tests/data/Invoice.ts index 6a5c478a3a94..650b9afab832 100644 --- a/tests/data/Invoice.ts +++ b/tests/data/Invoice.ts @@ -242,7 +242,6 @@ const convertedInvoiceChat: OnyxTypes.Report = { lastActorAccountID: 32, lastMessageHtml: 'paid $1.00', lastMessageText: 'paid $1.00', - lastMessageTranslationKey: '', lastReadSequenceNumber: 0, lastReadTime: '2024-12-13 19:45:28.942', lastVisibleActionCreated: '2024-12-13 19:19:01.794', From c25b46012ad22983cae775e5bc5bc0f0448fe4b8 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 24 Dec 2024 22:14:11 +0700 Subject: [PATCH 075/110] fix eslint --- src/libs/DistanceRequestUtils.ts | 5 ++++- src/pages/iou/request/step/IOURequestStepParticipants.tsx | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index c41b33873a8a..e69b6754aeb2 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -289,7 +289,10 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number { /** * Returns custom unit rate ID for the distance transaction */ -function getCustomUnitRateID(reportID: string) { +function getCustomUnitRateID(reportID?: string) { + if (!reportID) { + return ''; + } const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index af4d674f08d7..6a67a1040f1b 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -41,7 +41,7 @@ function IOURequestStepParticipants({ const {translate} = useLocalize(); const styles = useThemeStyles(); const isFocused = useIsFocused(); - const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? -1}`); + const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? CONST.DEFAULT_NUMBER_ID}`); // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? participants.at(0)?.reportID ?? reportID : reportID); @@ -92,7 +92,7 @@ function IOURequestStepParticipants({ (val: Participant[]) => { HttpUtils.cancelPendingRequests(READ_COMMANDS.SEARCH_FOR_REPORTS); - const firstParticipantReportID = val.at(0)?.reportID ?? ''; + const firstParticipantReportID = val.at(0)?.reportID; const rateID = DistanceRequestUtils.getCustomUnitRateID(firstParticipantReportID); const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && ReportUtils.isInvoiceRoomWithID(firstParticipantReportID); numberOfParticipants.current = val.length; @@ -108,7 +108,7 @@ function IOURequestStepParticipants({ } // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. - selectedReportID.current = firstParticipantReportID || reportID; + selectedReportID.current = firstParticipantReportID ?? reportID; }, [iouType, reportID, transactionID], ); From 670440364067251e855d0a9cd7a7aad5c5a3a666 Mon Sep 17 00:00:00 2001 From: Huu Le <20178761+huult@users.noreply.github.com> Date: Tue, 24 Dec 2024 23:52:45 +0700 Subject: [PATCH 076/110] Update return default customUnitRateID when report ID is undefined --- src/libs/DistanceRequestUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index e69b6754aeb2..94167b382d49 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -290,13 +290,14 @@ function convertToDistanceInMeters(distance: number, unit: Unit): number { * Returns custom unit rate ID for the distance transaction */ function getCustomUnitRateID(reportID?: string) { + let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; + if (!reportID) { - return ''; + return customUnitRateID; } const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`]; const policy = PolicyUtils.getPolicy(report?.policyID ?? parentReport?.policyID); - let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; if (isEmptyObject(policy)) { return customUnitRateID; From 55572c31dee16d950538b0fdb8c52c234d0e661c Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 24 Dec 2024 11:18:13 -0800 Subject: [PATCH 077/110] Remove accidentally added client only Report key state --- src/libs/DebugUtils.ts | 2 -- src/types/onyx/Report.ts | 5 +---- src/types/utils/whitelistedReportKeys.ts | 1 - tests/data/Invoice.ts | 1 - 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 9660a95d999d..109dbf2820f3 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -468,7 +468,6 @@ function validateReportDraftProperty(key: keyof Report, value: string) { case 'iouReportID': case 'preexistingReportID': case 'private_isArchived': - case 'state': case 'welcomeMessage': return validateString(value); case 'hasOutstandingChildRequest': @@ -631,7 +630,6 @@ function validateReportDraftProperty(key: keyof Report, value: string) { partial: CONST.RED_BRICK_ROAD_PENDING_ACTION, reimbursed: CONST.RED_BRICK_ROAD_PENDING_ACTION, preview: CONST.RED_BRICK_ROAD_PENDING_ACTION, - state: CONST.RED_BRICK_ROAD_PENDING_ACTION, welcomeMessage: CONST.RED_BRICK_ROAD_PENDING_ACTION, }); } diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index a864c369aab9..613f524b8c08 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -136,10 +136,7 @@ type Report = OnyxCommon.OnyxValueWithOfflineFeedback< /** ID of the chat report */ chatReportID?: string; - /** The state of the report */ - state?: keyof typeof CONST.REPORT.STATE_NUM; - - /** The state number of the report */ + /** The state that the report is currently in */ stateNum?: ValueOf; /** The status of the current report */ diff --git a/src/types/utils/whitelistedReportKeys.ts b/src/types/utils/whitelistedReportKeys.ts index 367da53d3122..7e27d49de1d6 100644 --- a/src/types/utils/whitelistedReportKeys.ts +++ b/src/types/utils/whitelistedReportKeys.ts @@ -63,7 +63,6 @@ type WhitelistedReport = OnyxCommon.OnyxValueWithOfflineFeedback< }; // eslint-disable-next-line @typescript-eslint/naming-convention private_isArchived: unknown; - state: unknown; welcomeMessage: unknown; }, PolicyReportField['fieldID'] diff --git a/tests/data/Invoice.ts b/tests/data/Invoice.ts index 650b9afab832..c94c7ce816be 100644 --- a/tests/data/Invoice.ts +++ b/tests/data/Invoice.ts @@ -269,7 +269,6 @@ const convertedInvoiceChat: OnyxTypes.Report = { private_isArchived: '', reportID: '7605647250932303', reportName: 'Chat Report', - state: 'OPEN', stateNum: 0, statusNum: 0, total: 0, From 061dea7c6d8c561844c3dff6796101b8342ec7d0 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 25 Dec 2024 09:48:39 +0700 Subject: [PATCH 078/110] update comment --- src/libs/Network/SequentialQueue.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 0a0c5281a3a9..1af1337f845b 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -199,8 +199,8 @@ function unpause() { Log.info(`[SequentialQueue] Unpausing the queue and flushing ${numberOfPersistedRequests} requests`); isQueuePaused = false; - // A READ request will wait until all the WRITE requests are done, using isReadyPromise promise. - // When the queue is paused and then unpaused, we call flush which by defaults recreate the isReadyPromise. + // A READ request will wait until all the WRITE requests are done, using the isReadyPromise promise. + // When the queue is paused and then unpaused, we call flush which by defaults recreates the isReadyPromise. // After all the WRITE requests are done, the isReadyPromise is resolved, but since it's a new instance of promise, // the pending READ request never received the resolved callback. That's why we don't want to recreate // the promise when unpausing the queue. From 8ff9230f95364c30c19f7810557265d2c58b6035 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 25 Dec 2024 15:15:29 +0700 Subject: [PATCH 079/110] fix: add dismiss modal when cancel payment --- src/libs/actions/IOU.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 8b786071e89d..97b8e421f4fb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7845,6 +7845,7 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O }, {optimisticData, successData, failureData}, ); + Navigation.dismissModal(); Report.notifyNewAction(expenseReport.reportID, userAccountID); } From 85fe866907fe9d6603a037e26b752d29251e2cdc Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 25 Dec 2024 15:22:21 +0700 Subject: [PATCH 080/110] fix to scroll bottom when cancel payment --- src/libs/actions/IOU.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 97b8e421f4fb..992b3c8c7e59 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7742,6 +7742,7 @@ function cancelPayment(expenseReport: OnyxEntry, chatReport: O key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, value: { ...expenseReport, + lastVisibleActionCreated: optimisticReportAction?.created, lastMessageText: ReportActionsUtils.getReportActionText(optimisticReportAction), lastMessageHtml: ReportActionsUtils.getReportActionHtml(optimisticReportAction), stateNum, From 6b3af86f53dc31fcdb4e898d2c882ed7a16c0b1b Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Wed, 25 Dec 2024 17:39:06 -0500 Subject: [PATCH 081/110] Apply styling to parent view --- src/components/EmptySelectionListContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/EmptySelectionListContent.tsx b/src/components/EmptySelectionListContent.tsx index 5281b1c33b4b..8207e7c23fcf 100644 --- a/src/components/EmptySelectionListContent.tsx +++ b/src/components/EmptySelectionListContent.tsx @@ -48,8 +48,8 @@ function EmptySelectionListContent({contentType}: EmptySelectionListContentProps ); return ( - - + + Date: Thu, 26 Dec 2024 12:24:08 +1300 Subject: [PATCH 082/110] Remove an empty line --- src/styles/utils/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 8a41e6882365..a2ca4c508320 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -291,7 +291,6 @@ function getBackgroundColorAndFill(backgroundColor: string, fill: string): SVGAv function getEReceiptColorCode(transaction: OnyxEntry): EReceiptColorName { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const transactionID = transaction?.parentTransactionID || transaction?.transactionID; - if (!transactionID) { return CONST.ERECEIPT_COLORS.YELLOW; } From 6734a5acaf65fd64d8b551e3b3691759b00ec7a2 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 26 Dec 2024 18:15:07 +0700 Subject: [PATCH 083/110] don't apply the category tax rules for distance request --- src/libs/TransactionUtils/index.ts | 2 +- tests/actions/IOUTest.ts | 201 +++++++++++++++++++++-------- 2 files changed, 146 insertions(+), 57 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 21c346f613b5..bb7ea0dc5c42 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1258,7 +1258,7 @@ function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry, policy: OnyxEntry) { const taxRules = policy?.rules?.expenseRules?.filter((rule) => rule.tax); - if (!taxRules || taxRules?.length === 0) { + if (!taxRules || taxRules?.length === 0 || isDistanceRequest(transaction)) { return {categoryTaxCode: undefined, categoryTaxAmount: undefined}; } diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 49e1150f1a57..b0f42838df72 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3520,40 +3520,82 @@ describe('actions/IOU', () => { }); }); - it('should not change the tax if there are no tax expense rules', async () => { - // Given a policy without tax expense rules - const transactionID = '1'; - const category = 'Advertising'; - const policyID = '2'; - const taxCode = 'id_TAX_EXEMPT'; - const taxAmount = 0; - const fakePolicy: OnyxTypes.Policy = { - ...createRandomPolicy(Number(policyID)), - taxRates: CONST.DEFAULT_TAX, - rules: {}, - }; - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { - taxCode, - taxAmount, - amount: 100, + describe('should not change the tax', () => { + it('if the transaction type is distance', async () => { + // Given a policy with tax expense rules associated with category and a distance transaction + const transactionID = '1'; + const category = 'Advertising'; + const policyID = '2'; + const taxCode = 'id_TAX_EXEMPT'; + const ruleTaxCode = 'id_TAX_RATE_1'; + const taxAmount = 0; + const fakePolicy: OnyxTypes.Policy = { + ...createRandomPolicy(Number(policyID)), + taxRates: CONST.DEFAULT_TAX, + rules: {expenseRules: createCategoryTaxExpenseRules(category, ruleTaxCode)}, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { + taxCode, + taxAmount, + amount: 100, + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + + // When setting the money request category + IOU.setMoneyRequestCategory(transactionID, category, policyID); + + await waitForBatchedUpdates(); + + // Then the transaction tax rate and amount shouldn't be updated + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, + callback: (transaction) => { + Onyx.disconnect(connection); + expect(transaction?.taxCode).toBe(taxCode); + expect(transaction?.taxAmount).toBe(taxAmount); + resolve(); + }, + }); + }); }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); - // When setting the money request category - IOU.setMoneyRequestCategory(transactionID, category, policyID); + it('if there are no tax expense rules', async () => { + // Given a policy without tax expense rules + const transactionID = '1'; + const category = 'Advertising'; + const policyID = '2'; + const taxCode = 'id_TAX_EXEMPT'; + const taxAmount = 0; + const fakePolicy: OnyxTypes.Policy = { + ...createRandomPolicy(Number(policyID)), + taxRates: CONST.DEFAULT_TAX, + rules: {}, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { + taxCode, + taxAmount, + amount: 100, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); - await waitForBatchedUpdates(); + // When setting the money request category + IOU.setMoneyRequestCategory(transactionID, category, policyID); - // Then the transaction tax rate and amount shouldn't be updated - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, - callback: (transaction) => { - Onyx.disconnect(connection); - expect(transaction?.taxCode).toBe(taxCode); - expect(transaction?.taxAmount).toBe(taxAmount); - resolve(); - }, + await waitForBatchedUpdates(); + + // Then the transaction tax rate and amount shouldn't be updated + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, + callback: (transaction) => { + Onyx.disconnect(connection); + expect(transaction?.taxCode).toBe(taxCode); + expect(transaction?.taxAmount).toBe(taxAmount); + resolve(); + }, + }); }); }); }); @@ -3627,34 +3669,81 @@ describe('actions/IOU', () => { }); }); - it('should not update the tax when there are no tax expense rules', async () => { - // Given a policy without tax expense rules - const transactionID = '1'; - const policyID = '2'; - const category = 'Advertising'; - const fakePolicy: OnyxTypes.Policy = { - ...createRandomPolicy(Number(policyID)), - taxRates: CONST.DEFAULT_TAX, - rules: {}, - }; - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {amount: 100}); - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + describe('should not update the tax', () => { + it('if the transaction type is distance', async () => { + // Given a policy with tax expense rules associated with category and a distance transaction + const transactionID = '1'; + const policyID = '2'; + const category = 'Advertising'; + const taxCode = 'id_TAX_EXEMPT'; + const taxAmount = 0; + const ruleTaxCode = 'id_TAX_RATE_1'; + const fakePolicy: OnyxTypes.Policy = { + ...createRandomPolicy(Number(policyID)), + taxRates: CONST.DEFAULT_TAX, + rules: {expenseRules: createCategoryTaxExpenseRules(category, ruleTaxCode)}, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { + taxCode, + taxAmount, + amount: 100, + comment: { + type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT, + customUnit: { + name: CONST.CUSTOM_UNITS.NAME_DISTANCE, + }, + }, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); - // When updating the money request category - IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined); + // When updating a money request category + IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined); - await waitForBatchedUpdates(); + await waitForBatchedUpdates(); - // Then the transaction tax rate and amount shouldn't be updated - await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - callback: (transaction) => { - Onyx.disconnect(connection); - expect(transaction?.taxCode).toBeUndefined(); - expect(transaction?.taxAmount).toBeUndefined(); - resolve(); - }, + // Then the transaction tax rate and amount shouldn't be updated + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + callback: (transaction) => { + Onyx.disconnect(connection); + expect(transaction?.taxCode).toBe(taxCode); + expect(transaction?.taxAmount).toBe(taxAmount); + resolve(); + }, + }); + }); + }); + + it('if there are no tax expense rules', async () => { + // Given a policy without tax expense rules + const transactionID = '1'; + const policyID = '2'; + const category = 'Advertising'; + const fakePolicy: OnyxTypes.Policy = { + ...createRandomPolicy(Number(policyID)), + taxRates: CONST.DEFAULT_TAX, + rules: {}, + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {amount: 100}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + + // When updating the money request category + IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined); + + await waitForBatchedUpdates(); + + // Then the transaction tax rate and amount shouldn't be updated + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + callback: (transaction) => { + Onyx.disconnect(connection); + expect(transaction?.taxCode).toBeUndefined(); + expect(transaction?.taxAmount).toBeUndefined(); + resolve(); + }, + }); }); }); }); @@ -3700,7 +3789,7 @@ describe('actions/IOU', () => { }); describe('should not change the tax', () => { - it('if there is no tax expense rules', async () => { + it('if there are no tax expense rules', async () => { // Given a policy without tax expense rules const transactionID = '1'; const category = 'Advertising'; From b7ce3b7938e596e3cd01ce1798ac23432b186ea1 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 26 Dec 2024 18:27:57 +0700 Subject: [PATCH 084/110] add test --- tests/unit/TransactionUtilsTest.ts | 53 +++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index a6c847e5f7f4..fa91210dc6c4 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -133,22 +133,45 @@ describe('TransactionUtils', () => { expect(categoryTaxAmount).toBe(0); }); - it('should return and undefined tax when there are no policy tax expense rules', () => { - // Given a policy without tax expense rules - const category = 'Advertising'; - const fakePolicy: Policy = { - ...createRandomPolicy(0), - taxRates: CONST.DEFAULT_TAX, - rules: {}, - }; - - // When retrieving the tax from a category - const transaction = generateTransaction(); - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy); + describe('should return undefined tax', () => { + it('if the transaction type is distance', () => { + // Given a policy with tax expense rules associated with a category + const category = 'Advertising'; + const fakePolicy: Policy = { + ...createRandomPolicy(0), + taxRates: CONST.DEFAULT_TAX, + rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')}, + }; + + // When retrieving the tax from the associated category + const transaction: Transaction = { + ...generateTransaction(), + iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE, + }; + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy); + + // Then it should return undefined for both the tax code and the tax amount + expect(categoryTaxCode).toBe(undefined); + expect(categoryTaxAmount).toBe(undefined); + }); - // Then it should return undefined for both the tax code and the tax amount - expect(categoryTaxCode).toBe(undefined); - expect(categoryTaxAmount).toBe(undefined); + it('if there are no policy tax expense rules', () => { + // Given a policy without tax expense rules + const category = 'Advertising'; + const fakePolicy: Policy = { + ...createRandomPolicy(0), + taxRates: CONST.DEFAULT_TAX, + rules: {}, + }; + + // When retrieving the tax from a category + const transaction = generateTransaction(); + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy); + + // Then it should return undefined for both the tax code and the tax amount + expect(categoryTaxCode).toBe(undefined); + expect(categoryTaxAmount).toBe(undefined); + }); }); }); }); From 1e792b85f8d0be0e75455ca826ffc31966bc685b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 26 Dec 2024 19:41:24 +0700 Subject: [PATCH 085/110] get the correct default tax rate --- src/libs/TransactionUtils/index.ts | 3 ++- tests/unit/TransactionUtilsTest.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index bb7ea0dc5c42..379f68009d3c 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1262,7 +1262,8 @@ function getCategoryTaxCodeAndAmount(category: string, transaction: OnyxEntry { expect(categoryTaxAmount).toBe(0); }); + it("should return the foreign default tax when the category doesn't match the tax expense rules and using a foreign currency", () => { + // Given a policy with tax expense rules associated with a category + const ruleCategory = 'Advertising'; + const selectedCategory = 'Benefits'; + const fakePolicy: Policy = { + ...createRandomPolicy(0), + taxRates: { + ...CONST.DEFAULT_TAX, + foreignTaxDefault: 'id_TAX_RATE_2', + taxes: { + ...CONST.DEFAULT_TAX.taxes, + id_TAX_RATE_2: { + name: 'Tax rate 2', + value: '10%', + }, + }, + }, + outputCurrency: 'IDR', + rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')}, + }; + + // When retrieving the tax from a category that is not associated with the tax expense rules + const transaction = generateTransaction(); + const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(selectedCategory, transaction, fakePolicy); + + // Then it should return the default tax code and amount + expect(categoryTaxCode).toBe('id_TAX_RATE_2'); + expect(categoryTaxAmount).toBe(9); + }); + describe('should return undefined tax', () => { it('if the transaction type is distance', () => { // Given a policy with tax expense rules associated with a category From 6552ca8a1d6db538450a58a78c0e9ad1edb99c82 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 26 Dec 2024 19:45:06 +0700 Subject: [PATCH 086/110] lint --- tests/unit/TransactionUtilsTest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 42c0534c16ed..27b0a2d90605 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -144,6 +144,7 @@ describe('TransactionUtils', () => { foreignTaxDefault: 'id_TAX_RATE_2', taxes: { ...CONST.DEFAULT_TAX.taxes, + // eslint-disable-next-line @typescript-eslint/naming-convention id_TAX_RATE_2: { name: 'Tax rate 2', value: '10%', From 296948e49625994549b0461f0857920bbb159111 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 26 Dec 2024 19:45:26 +0700 Subject: [PATCH 087/110] update comment --- tests/unit/TransactionUtilsTest.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 27b0a2d90605..0ed8d0f0461b 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -104,9 +104,9 @@ describe('TransactionUtils', () => { taxRates: CONST.DEFAULT_TAX, rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')}, }; + const transaction = generateTransaction(); // When retrieving the tax from the associated category - const transaction = generateTransaction(); const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy); // Then it should return the associated tax code and amount @@ -123,9 +123,9 @@ describe('TransactionUtils', () => { taxRates: CONST.DEFAULT_TAX, rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')}, }; + const transaction = generateTransaction(); // When retrieving the tax from a category that is not associated with the tax expense rules - const transaction = generateTransaction(); const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(selectedCategory, transaction, fakePolicy); // Then it should return the default tax code and amount @@ -134,7 +134,7 @@ describe('TransactionUtils', () => { }); it("should return the foreign default tax when the category doesn't match the tax expense rules and using a foreign currency", () => { - // Given a policy with tax expense rules associated with a category + // Given a policy with tax expense rules associated with a category and a transaction with a foreign currency const ruleCategory = 'Advertising'; const selectedCategory = 'Benefits'; const fakePolicy: Policy = { @@ -154,9 +154,9 @@ describe('TransactionUtils', () => { outputCurrency: 'IDR', rules: {expenseRules: createCategoryTaxExpenseRules(ruleCategory, 'id_TAX_RATE_1')}, }; + const transaction = generateTransaction(); // When retrieving the tax from a category that is not associated with the tax expense rules - const transaction = generateTransaction(); const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(selectedCategory, transaction, fakePolicy); // Then it should return the default tax code and amount @@ -173,12 +173,12 @@ describe('TransactionUtils', () => { taxRates: CONST.DEFAULT_TAX, rules: {expenseRules: createCategoryTaxExpenseRules(category, 'id_TAX_RATE_1')}, }; - - // When retrieving the tax from the associated category const transaction: Transaction = { ...generateTransaction(), iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE, }; + + // When retrieving the tax from the associated category const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy); // Then it should return undefined for both the tax code and the tax amount @@ -194,9 +194,9 @@ describe('TransactionUtils', () => { taxRates: CONST.DEFAULT_TAX, rules: {}, }; + const transaction = generateTransaction(); // When retrieving the tax from a category - const transaction = generateTransaction(); const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, fakePolicy); // Then it should return undefined for both the tax code and the tax amount From c35bddc90ed186898f32f2700535ac5a678b9837 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 26 Dec 2024 20:59:49 +0700 Subject: [PATCH 088/110] fix system message shows tax updates too when changing category with tax rules --- src/libs/TransactionUtils/index.ts | 5 +++++ src/libs/actions/IOU.ts | 19 ++----------------- tests/actions/IOUTest.ts | 23 ++++++++++++++++++++++- tests/unit/TransactionUtilsTest.ts | 27 +++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 379f68009d3c..e40facb21c48 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -376,6 +376,11 @@ function getUpdatedTransaction({ if (Object.hasOwn(transactionChanges, 'category') && typeof transactionChanges.category === 'string') { updatedTransaction.category = transactionChanges.category; + const {categoryTaxCode, categoryTaxAmount} = getCategoryTaxCodeAndAmount(transactionChanges.category, transaction, policy); + if (categoryTaxCode && categoryTaxAmount !== undefined) { + updatedTransaction.taxCode = categoryTaxCode; + updatedTransaction.taxAmount = categoryTaxAmount; + } } if (Object.hasOwn(transactionChanges, 'tag') && typeof transactionChanges.tag === 'string') { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c0906f77850a..78546c6636f3 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3441,15 +3441,8 @@ function updateMoneyRequestCategory( policyTagList: OnyxEntry, policyCategories: OnyxEntry, ) { - const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(category, transaction, policy); const transactionChanges: TransactionChanges = { category, - ...(categoryTaxCode && - categoryTaxAmount !== undefined && { - taxCode: categoryTaxCode, - taxAmount: categoryTaxAmount, - }), }; const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories); @@ -5377,27 +5370,19 @@ function completeSplitBill(chatReportID: string, reportAction: OnyxTypes.ReportA } function setDraftSplitTransaction(transactionID: string, transactionChanges: TransactionChanges = {}, policy?: OnyxEntry) { - const newTransactionChanges = {...transactionChanges}; let draftSplitTransaction = allDraftSplitTransactions[`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`]; if (!draftSplitTransaction) { draftSplitTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; } - if (transactionChanges.category) { - const {categoryTaxCode, categoryTaxAmount} = TransactionUtils.getCategoryTaxCodeAndAmount(transactionChanges.category, draftSplitTransaction, policy); - if (categoryTaxCode && categoryTaxAmount !== undefined) { - newTransactionChanges.taxCode = categoryTaxCode; - newTransactionChanges.taxAmount = categoryTaxAmount; - } - } - const updatedTransaction = draftSplitTransaction ? TransactionUtils.getUpdatedTransaction({ transaction: draftSplitTransaction, - transactionChanges: newTransactionChanges, + transactionChanges, isFromExpenseReport: false, shouldUpdateReceiptState: false, + policy, }) : null; diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index b0f42838df72..59d3ec9f2f0c 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -3635,6 +3635,7 @@ describe('actions/IOU', () => { // Given a policy with tax expense rules associated with category const transactionID = '1'; const policyID = '2'; + const transactionThreadReportID = '3'; const category = 'Advertising'; const taxCode = 'id_TAX_EXEMPT'; const ruleTaxCode = 'id_TAX_RATE_1'; @@ -3649,9 +3650,10 @@ describe('actions/IOU', () => { amount: 100, }); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, fakePolicy); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {reportID: transactionThreadReportID}); // When updating a money request category - IOU.updateMoneyRequestCategory(transactionID, '3', category, fakePolicy, undefined, undefined); + IOU.updateMoneyRequestCategory(transactionID, transactionThreadReportID, category, fakePolicy, undefined, undefined); await waitForBatchedUpdates(); @@ -3667,6 +3669,25 @@ describe('actions/IOU', () => { }, }); }); + + // But the original message should only contains the old and new category data + await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`, + callback: (reportActions) => { + Onyx.disconnect(connection); + const reportAction = Object.values(reportActions ?? {}).at(0); + if (ReportActionsUtils.isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE)) { + const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction); + expect(originalMessage?.oldCategory).toBe(''); + expect(originalMessage?.category).toBe(category); + expect(originalMessage?.oldTaxRate).toBeUndefined(); + expect(originalMessage?.oldTaxAmount).toBeUndefined(); + resolve(); + } + }, + }); + }); }); describe('should not update the tax', () => { diff --git a/tests/unit/TransactionUtilsTest.ts b/tests/unit/TransactionUtilsTest.ts index 0ed8d0f0461b..558d09e6a0fa 100644 --- a/tests/unit/TransactionUtilsTest.ts +++ b/tests/unit/TransactionUtilsTest.ts @@ -205,4 +205,31 @@ describe('TransactionUtils', () => { }); }); }); + + describe('getUpdatedTransaction', () => { + it('should return updated category and tax when updating category with a category tax rules', () => { + // Given a policy with tax expense rules associated with a category + const category = 'Advertising'; + const taxCode = 'id_TAX_RATE_1'; + const fakePolicy: Policy = { + ...createRandomPolicy(0), + taxRates: CONST.DEFAULT_TAX, + rules: {expenseRules: createCategoryTaxExpenseRules(category, taxCode)}, + }; + const transaction = generateTransaction(); + + // When updating the transaction category to the category associated with the rule + const updatedTransaction = TransactionUtils.getUpdatedTransaction({ + transaction, + isFromExpenseReport: true, + policy: fakePolicy, + transactionChanges: {category}, + }); + + // Then the updated transaction should contain the tax from the matched rule + expect(updatedTransaction.category).toBe(category); + expect(updatedTransaction.taxCode).toBe(taxCode); + expect(updatedTransaction.taxAmount).toBe(5); + }); + }); }); From 2e9ca097fbb13ff7da4ae69a74ca274361a40bdc Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 26 Dec 2024 16:30:30 +0000 Subject: [PATCH 089/110] Update version to 9.0.78-3 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7fd4fd9dd59d..aa06e157e303 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007802 - versionName "9.0.78-2" + versionCode 1009007803 + versionName "9.0.78-3" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 3374f9c36b3f..52508250f300 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.78.2 + 9.0.78.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 6f72c68b009d..ee2052c35ef6 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.78.2 + 9.0.78.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 328278e16cf3..be5564604833 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.78 CFBundleVersion - 9.0.78.2 + 9.0.78.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index f0ee3f539fcd..f76638d9cb19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.78-2", + "version": "9.0.78-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.78-2", + "version": "9.0.78-3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index a84f3df0b259..64914c311429 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.78-2", + "version": "9.0.78-3", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 0b2a2231bb4ffee3fb7df9865c34cd7562a2415a Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 26 Dec 2024 16:31:00 +0000 Subject: [PATCH 090/110] Update Mobile-Expensify to 9.0.78-3 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 7ffe8a7f1b47..fe4b53acb9ec 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 7ffe8a7f1b471c697f9823b8cd4a2c19b200fa6f +Subproject commit fe4b53acb9ec71954b34e91abe40f351fe2788b5 From d715ed874ca0df7aa7110cb9f25ee9ed04b0954e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 26 Dec 2024 23:44:56 +0700 Subject: [PATCH 091/110] fix lhn filter is resets to all when sending invoice for the first time on a workspace --- .../home/sidebar/SidebarScreen/BaseSidebarScreen.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index a1e4f0e4a22a..580f94e0f2f4 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -15,13 +15,15 @@ import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; function BaseSidebarScreen() { const styles = useThemeStyles(); const activeWorkspaceID = useActiveWorkspaceFromNavigationState(); const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [activeWorkspace] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID ?? CONST.DEFAULT_NUMBER_ID}`); + const [activeWorkspace, activeWorkspaceResult] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID ?? CONST.DEFAULT_NUMBER_ID}`); + const isLoading = isLoadingOnyxValue(activeWorkspaceResult); useEffect(() => { Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); @@ -29,13 +31,13 @@ function BaseSidebarScreen() { }, []); useEffect(() => { - if (!!activeWorkspace || activeWorkspaceID === undefined) { + if (!!activeWorkspace || activeWorkspaceID === undefined || isLoading) { return; } Navigation.navigateWithSwitchPolicyID({policyID: undefined}); updateLastAccessedWorkspace(undefined); - }, [activeWorkspace, activeWorkspaceID]); + }, [activeWorkspace, activeWorkspaceID, isLoading]); const shouldDisplaySearch = shouldUseNarrowLayout; From 7f723ed63f6db185ff7d2f2bc10471dccef0193e Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Thu, 26 Dec 2024 09:02:26 -0800 Subject: [PATCH 092/110] Update src/components/EmptySelectionListContent.tsx Co-authored-by: Pujan Shah --- src/components/EmptySelectionListContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/EmptySelectionListContent.tsx b/src/components/EmptySelectionListContent.tsx index 8207e7c23fcf..313b5d620f42 100644 --- a/src/components/EmptySelectionListContent.tsx +++ b/src/components/EmptySelectionListContent.tsx @@ -48,8 +48,8 @@ function EmptySelectionListContent({contentType}: EmptySelectionListContentProps ); return ( - - + + Date: Thu, 26 Dec 2024 17:20:31 +0000 Subject: [PATCH 093/110] Update version to 9.0.78-4 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index aa06e157e303..44c83c772624 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007803 - versionName "9.0.78-3" + versionCode 1009007804 + versionName "9.0.78-4" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 52508250f300..b6e0072080a4 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.78.3 + 9.0.78.4 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index ee2052c35ef6..5a3bd5a9998a 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.78.3 + 9.0.78.4 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index be5564604833..e6818adba50b 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.78 CFBundleVersion - 9.0.78.3 + 9.0.78.4 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index f76638d9cb19..08f7f96d3fba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.78-3", + "version": "9.0.78-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.78-3", + "version": "9.0.78-4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 64914c311429..31a304b5336b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.78-3", + "version": "9.0.78-4", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 5037479d824632ae955dda50b056e614095ac4c8 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 26 Dec 2024 17:21:03 +0000 Subject: [PATCH 094/110] Update Mobile-Expensify to 9.0.78-4 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index fe4b53acb9ec..bc1871df1eb6 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit fe4b53acb9ec71954b34e91abe40f351fe2788b5 +Subproject commit bc1871df1eb6b5c766caa38212305c73f2865761 From e9d5f82b7de04e26071ef5ef550ff63ecbc25e9f Mon Sep 17 00:00:00 2001 From: Neil Marcellini Date: Thu, 26 Dec 2024 09:43:19 -0800 Subject: [PATCH 095/110] Revert "fix: when selecting categories, the selected categories get reset" --- src/hooks/useCleanupSelectedOptions/index.ts | 21 ------------------- .../categories/WorkspaceCategoriesPage.tsx | 16 +++++++------- .../workspace/tags/WorkspaceTagsPage.tsx | 18 ++++++++-------- .../workspace/taxes/WorkspaceTaxesPage.tsx | 21 +++++++++---------- 4 files changed, 27 insertions(+), 49 deletions(-) delete mode 100644 src/hooks/useCleanupSelectedOptions/index.ts diff --git a/src/hooks/useCleanupSelectedOptions/index.ts b/src/hooks/useCleanupSelectedOptions/index.ts deleted file mode 100644 index 7451e85aef23..000000000000 --- a/src/hooks/useCleanupSelectedOptions/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {NavigationContainerRefContext, useIsFocused} from '@react-navigation/native'; -import {useContext, useEffect} from 'react'; -import NAVIGATORS from '@src/NAVIGATORS'; - -const useCleanupSelectedOptions = (cleanupFunction?: () => void) => { - const navigationContainerRef = useContext(NavigationContainerRefContext); - const state = navigationContainerRef?.getState(); - const lastRoute = state?.routes.at(-1); - const isRightModalOpening = lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR; - - const isFocused = useIsFocused(); - - useEffect(() => { - if (isFocused || isRightModalOpening) { - return; - } - cleanupFunction?.(); - }, [isFocused, cleanupFunction, isRightModalOpening]); -}; - -export default useCleanupSelectedOptions; diff --git a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx index a8a37638f87e..737fbc2972c1 100644 --- a/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx +++ b/src/pages/workspace/categories/WorkspaceCategoriesPage.tsx @@ -1,4 +1,4 @@ -import {useFocusEffect} from '@react-navigation/native'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import lodashSortBy from 'lodash/sortBy'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; @@ -23,7 +23,6 @@ import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useAutoTurnSelectionModeOffWhenHasNoActiveOption from '@hooks/useAutoTurnSelectionModeOffWhenHasNoActiveOption'; -import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -71,6 +70,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { const [selectedCategories, setSelectedCategories] = useState>({}); const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false); const [deleteCategoriesConfirmModalVisible, setDeleteCategoriesConfirmModalVisible] = useState(false); + const isFocused = useIsFocused(); const {environmentURL} = useEnvironment(); const policyId = route.params.policyID ?? '-1'; const backTo = route.params?.backTo; @@ -98,8 +98,12 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { }, [fetchCategories]), ); - const cleanupSelectedOption = useCallback(() => setSelectedCategories({}), []); - useCleanupSelectedOptions(cleanupSelectedOption); + useEffect(() => { + if (isFocused) { + return; + } + setSelectedCategories({}); + }, [isFocused]); const categoryList = useMemo( () => @@ -147,10 +151,6 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) { }; const navigateToCategorySettings = (category: PolicyOption) => { - if (isSmallScreenWidth && selectionMode?.isEnabled) { - toggleCategory(category); - return; - } Navigation.navigate( isQuickSettingsFlow ? ROUTES.SETTINGS_CATEGORY_SETTINGS.getRoute(policyId, category.keyForList, backTo) diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index b86a35fa6fca..61bd2e3aa42f 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -1,6 +1,6 @@ -import {useFocusEffect} from '@react-navigation/native'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import lodashSortBy from 'lodash/sortBy'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -22,7 +22,6 @@ import CustomListHeader from '@components/SelectionListWithModal/CustomListHeade import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -65,6 +64,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false); const [isDeleteTagsConfirmModalVisible, setIsDeleteTagsConfirmModalVisible] = useState(false); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); + const isFocused = useIsFocused(); const policyID = route.params.policyID ?? '-1'; const backTo = route.params.backTo; const policy = usePolicy(policyID); @@ -87,8 +87,12 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { useFocusEffect(fetchTags); - const cleanupSelectedOption = useCallback(() => setSelectedTags({}), []); - useCleanupSelectedOptions(cleanupSelectedOption); + useEffect(() => { + if (isFocused) { + return; + } + setSelectedTags({}); + }, [isFocused]); const getPendingAction = (policyTagList: PolicyTagList): PendingAction | undefined => { if (!policyTagList) { @@ -172,10 +176,6 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { }; const navigateToTagSettings = (tag: TagListItem) => { - if (isSmallScreenWidth && selectionMode?.isEnabled) { - toggleTag(tag); - return; - } if (tag.orderWeight !== undefined) { Navigation.navigate( isQuickSettingsFlow ? ROUTES.SETTINGS_TAG_LIST_VIEW.getRoute(policyID, tag.orderWeight, backTo) : ROUTES.WORKSPACE_TAG_LIST_VIEW.getRoute(policyID, tag.orderWeight), diff --git a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx index e588a1ecb313..e064c04878a1 100644 --- a/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx +++ b/src/pages/workspace/taxes/WorkspaceTaxesPage.tsx @@ -1,5 +1,5 @@ -import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useMemo, useState} from 'react'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; @@ -17,7 +17,6 @@ import SelectionListWithModal from '@components/SelectionListWithModal'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import useCleanupSelectedOptions from '@hooks/useCleanupSelectedOptions'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; @@ -52,8 +51,7 @@ function WorkspaceTaxesPage({ params: {policyID}, }, }: WorkspaceTaxesPageProps) { - // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -63,6 +61,7 @@ function WorkspaceTaxesPage({ const {selectionMode} = useMobileSelectionMode(); const defaultExternalID = policy?.taxRates?.defaultExternalID; const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; + const isFocused = useIsFocused(); const hasAccountingConnections = PolicyUtils.hasAccountingConnections(policy); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); @@ -87,8 +86,12 @@ function WorkspaceTaxesPage({ }, [fetchTaxes]), ); - const cleanupSelectedOption = useCallback(() => setSelectedTaxesIDs([]), []); - useCleanupSelectedOptions(cleanupSelectedOption); + useEffect(() => { + if (isFocused) { + return; + } + setSelectedTaxesIDs([]); + }, [isFocused]); const textForDefault = useCallback( (taxID: string, taxRate: TaxRate): string => { @@ -189,10 +192,6 @@ function WorkspaceTaxesPage({ if (!taxRate.keyForList) { return; } - if (isSmallScreenWidth && selectionMode?.isEnabled) { - toggleTax(taxRate); - return; - } Navigation.navigate(ROUTES.WORKSPACE_TAX_EDIT.getRoute(policyID, taxRate.keyForList)); }; From 1fca5d5a43c1f62375908ea625607bd60d8d3262 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 26 Dec 2024 17:51:59 +0000 Subject: [PATCH 096/110] Update version to 9.0.78-5 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 44c83c772624..bc0b8bd4bcd8 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007804 - versionName "9.0.78-4" + versionCode 1009007805 + versionName "9.0.78-5" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index b6e0072080a4..317b42b3cb96 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.78.4 + 9.0.78.5 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 5a3bd5a9998a..3bbb30e64775 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.78.4 + 9.0.78.5 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index e6818adba50b..ee6c4d444b1e 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.78 CFBundleVersion - 9.0.78.4 + 9.0.78.5 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 08f7f96d3fba..86a5ca885d81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.78-4", + "version": "9.0.78-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.78-4", + "version": "9.0.78-5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 31a304b5336b..2b2b9047b4fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.78-4", + "version": "9.0.78-5", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 704008d00f2de0d93c5b15ccd6beff83c4bb7249 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 26 Dec 2024 17:52:29 +0000 Subject: [PATCH 097/110] Update Mobile-Expensify to 9.0.78-5 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index bc1871df1eb6..5c96902b2f11 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit bc1871df1eb6b5c766caa38212305c73f2865761 +Subproject commit 5c96902b2f112987207b68a97340707489073c41 From 626ef41b373a8f7e66e2b41d7d83df3ed082c489 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Thu, 26 Dec 2024 09:53:13 -0800 Subject: [PATCH 098/110] Revert "fix wallet phone validation page" --- src/libs/GetPhysicalCardUtils.ts | 6 +----- .../settings/Wallet/Card/GetPhysicalCardPhone.tsx | 14 +++----------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index 9ed192b09233..8dc46204db3c 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -1,4 +1,3 @@ -import {Str} from 'expensify-common'; import type {OnyxEntry} from 'react-native-onyx'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; @@ -7,19 +6,16 @@ import type {LoginList, PrivatePersonalDetails} from '@src/types/onyx'; import * as LoginUtils from './LoginUtils'; import Navigation from './Navigation/Navigation'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; -import * as PhoneNumberUtils from './PhoneNumber'; import * as UserUtils from './UserUtils'; function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry): Route { const {legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; const address = PersonalDetailsUtils.getCurrentAddress(privatePersonalDetails); - const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumber ?? ''); - const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode); if (!legalFirstName && !legalLastName) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain); } - if (!phoneNumber || !parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) { + if (!phoneNumber || !LoginUtils.validateNumber(phoneNumber)) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain); } if (!(address?.street && address?.city && address?.state && address?.country && address?.zip)) { diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx index 075bc4a3ff5c..bcb3fe646fff 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx @@ -1,4 +1,3 @@ -import {Str} from 'expensify-common'; import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -9,8 +8,6 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as LoginUtils from '@libs/LoginUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import * as PhoneNumberUtils from '@libs/PhoneNumber'; -import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -43,17 +40,12 @@ function GetPhysicalCardPhone({ const errors: OnValidateResult = {}; - if (!ValidationUtils.isRequiredFulfilled(phoneNumberToValidate)) { + if (!LoginUtils.validateNumber(phoneNumberToValidate)) { + errors.phoneNumber = translate('common.error.phoneNumber'); + } else if (!phoneNumberToValidate) { errors.phoneNumber = translate('common.error.fieldRequired'); } - const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumberToValidate); - const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode); - - if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) { - errors.phoneNumber = translate('bankAccount.error.phoneNumber'); - } - return errors; }; From 321a4bce3ef01b38ecbd8c98913568d76902e7e9 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 26 Dec 2024 18:54:40 +0000 Subject: [PATCH 099/110] Update version to 9.0.78-6 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index bc0b8bd4bcd8..6e4e7f6297b1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007805 - versionName "9.0.78-5" + versionCode 1009007806 + versionName "9.0.78-6" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 317b42b3cb96..64f8ca108c2e 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.78.5 + 9.0.78.6 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 3bbb30e64775..6b2ed6fb1dc8 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.78.5 + 9.0.78.6 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index ee6c4d444b1e..1986bea48095 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.78 CFBundleVersion - 9.0.78.5 + 9.0.78.6 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 86a5ca885d81..2cc4dae5450d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.78-5", + "version": "9.0.78-6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.78-5", + "version": "9.0.78-6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2b2b9047b4fa..82f3cb9eb571 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.78-5", + "version": "9.0.78-6", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 58539c5f38dff838c3029af4a7b89f58a7213442 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 26 Dec 2024 18:55:09 +0000 Subject: [PATCH 100/110] Update Mobile-Expensify to 9.0.78-6 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 5c96902b2f11..9fd006acd6b0 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 5c96902b2f112987207b68a97340707489073c41 +Subproject commit 9fd006acd6b08d74507c827b88ebd19fa4044485 From ab40c1aef2ec5899e8f9733ddefdfb697b91efdc Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 26 Dec 2024 20:11:25 +0000 Subject: [PATCH 101/110] Update version to 9.0.79-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- ios/NotificationServiceExtension/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6e4e7f6297b1..572ee82f81f5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007806 - versionName "9.0.78-6" + versionCode 1009007900 + versionName "9.0.79-0" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 64f8ca108c2e..56ab019515a6 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.78 + 9.0.79 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.78.6 + 9.0.79.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 6b2ed6fb1dc8..7e06db7b7308 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.78 + 9.0.79 CFBundleSignature ???? CFBundleVersion - 9.0.78.6 + 9.0.79.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 1986bea48095..12f833b773b8 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.78 + 9.0.79 CFBundleVersion - 9.0.78.6 + 9.0.79.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 2cc4dae5450d..18ce878fe9ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.78-6", + "version": "9.0.79-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.78-6", + "version": "9.0.79-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 82f3cb9eb571..34975e0eced7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.78-6", + "version": "9.0.79-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 7a21b1205332fb82e3aebfb8fe85899d9bf4a3e0 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Thu, 26 Dec 2024 20:11:55 +0000 Subject: [PATCH 102/110] Update Mobile-Expensify to 9.0.79-0 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 9fd006acd6b0..1c05373664da 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 9fd006acd6b08d74507c827b88ebd19fa4044485 +Subproject commit 1c05373664dacc1072f4d97ead7912a79983af22 From 6db58742ac3ed4a3437df3f300fbe38bc42bae9e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 27 Dec 2024 14:56:47 +0700 Subject: [PATCH 103/110] add comment --- src/libs/Network/SequentialQueue.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 1af1337f845b..2ddc6c5ae4cc 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -128,6 +128,10 @@ function process(): Promise { return currentRequestPromise; } +/** + * @param shouldResetPromise Determines whether the isReadyPromise should be reset. Resetting can cause unresolved READ requests + * to hang if tied to the old promise, so some cases (e.g., unpausing) require skipping the reset to maintain proper behavior. + */ function flush(shouldResetPromise = true) { // When the queue is paused, return early. This will keep an requests in the queue and they will get flushed again when the queue is unpaused if (isQueuePaused) { From 010c488699c33df3a5bc65d7d01ce149d497edb9 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 27 Dec 2024 14:59:02 +0700 Subject: [PATCH 104/110] update comment --- src/libs/Network/SequentialQueue.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 2ddc6c5ae4cc..c1c2b0946935 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -129,8 +129,10 @@ function process(): Promise { } /** - * @param shouldResetPromise Determines whether the isReadyPromise should be reset. Resetting can cause unresolved READ requests - * to hang if tied to the old promise, so some cases (e.g., unpausing) require skipping the reset to maintain proper behavior. + * @param shouldResetPromise Determines whether the isReadyPromise should be reset. + * A READ request will wait until all the WRITE requests are done, using the isReadyPromise promise. + * Resetting can cause unresolved READ requests to hang if tied to the old promise, + * so some cases (e.g., unpausing) require skipping the reset to maintain proper behavior. */ function flush(shouldResetPromise = true) { // When the queue is paused, return early. This will keep an requests in the queue and they will get flushed again when the queue is unpaused @@ -203,7 +205,6 @@ function unpause() { Log.info(`[SequentialQueue] Unpausing the queue and flushing ${numberOfPersistedRequests} requests`); isQueuePaused = false; - // A READ request will wait until all the WRITE requests are done, using the isReadyPromise promise. // When the queue is paused and then unpaused, we call flush which by defaults recreates the isReadyPromise. // After all the WRITE requests are done, the isReadyPromise is resolved, but since it's a new instance of promise, // the pending READ request never received the resolved callback. That's why we don't want to recreate From e69df76081dd6a620c2fa63cc29f69c254bf61e7 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 27 Dec 2024 21:01:14 +0700 Subject: [PATCH 105/110] fix app crashes when creating track expense --- .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index e3f2eb7966e3..86196f13d662 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -74,9 +74,9 @@ function MoneyRequestPreviewContent({ const route = useRoute>(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || CONST.DEFAULT_NUMBER_ID}`); const [session] = useOnyx(ONYXKEYS.SESSION); - const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || CONST.DEFAULT_NUMBER_ID}`); const policy = PolicyUtils.getPolicy(iouReport?.policyID); const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action); From 87907f969fb70e7bb1b16dcf4db25ec0430b7053 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 27 Dec 2024 22:44:30 +0530 Subject: [PATCH 106/110] fixes issue --- src/components/ProductTrainingContext/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx index 7cfcf4d3bfa7..5d964f5f5671 100644 --- a/src/components/ProductTrainingContext/index.tsx +++ b/src/components/ProductTrainingContext/index.tsx @@ -205,8 +205,8 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou ]); const shouldShowProductTrainingTooltip = useMemo(() => { - return shouldRenderTooltip(tooltipName); - }, [shouldRenderTooltip, tooltipName]); + return shouldShow && shouldRenderTooltip(tooltipName); + }, [shouldRenderTooltip, tooltipName, shouldShow]); const hideProductTrainingTooltip = useCallback(() => { const tooltip = TOOLTIPS[tooltipName]; From a6e888e406fd94408d0b5f3801108fc5f45703a1 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 27 Dec 2024 18:34:22 +0000 Subject: [PATCH 107/110] Update version to 9.0.79-1 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 572ee82f81f5..2758804469a4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007900 - versionName "9.0.79-0" + versionCode 1009007901 + versionName "9.0.79-1" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 56ab019515a6..cea219fb54d5 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.79.0 + 9.0.79.1 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 7e06db7b7308..cd3532bdd1aa 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.79.0 + 9.0.79.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 12f833b773b8..d21333dbec4f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.79 CFBundleVersion - 9.0.79.0 + 9.0.79.1 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 18ce878fe9ff..a81d2dd4d581 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.79-0", + "version": "9.0.79-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.79-0", + "version": "9.0.79-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 34975e0eced7..ee510b969f71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.79-0", + "version": "9.0.79-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 1ba5d4d5017d061f83ad4009c3d04588ea241ca5 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 27 Dec 2024 18:34:53 +0000 Subject: [PATCH 108/110] Update Mobile-Expensify to 9.0.79-1 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 1c05373664da..0194fbea020a 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 1c05373664dacc1072f4d97ead7912a79983af22 +Subproject commit 0194fbea020a026b8207cbd11f3ef600ebde0dc5 From b8e4d47354865458047ac17ad59f494774a5a4b3 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 27 Dec 2024 19:48:22 +0000 Subject: [PATCH 109/110] Update version to 9.0.79-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2758804469a4..faf2c2221b7d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009007901 - versionName "9.0.79-1" + versionCode 1009007902 + versionName "9.0.79-2" // Supported language variants must be declared here to avoid from being removed during the compilation. // This also helps us to not include unnecessary language variants in the APK. resConfigs "en", "es" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index cea219fb54d5..750a68e41fb9 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 9.0.79.1 + 9.0.79.2 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index cd3532bdd1aa..dfe29bdcf8e8 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.79.1 + 9.0.79.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index d21333dbec4f..b840c6d54ed6 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.79 CFBundleVersion - 9.0.79.1 + 9.0.79.2 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index a81d2dd4d581..d93fa91f928f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.79-1", + "version": "9.0.79-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.79-1", + "version": "9.0.79-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ee510b969f71..f40db686a0db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.79-1", + "version": "9.0.79-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 9c6d3d96c9425cd173a70de7c0a13ff7b91705be Mon Sep 17 00:00:00 2001 From: OSBotify Date: Fri, 27 Dec 2024 19:48:53 +0000 Subject: [PATCH 110/110] Update Mobile-Expensify to 9.0.79-2 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 0194fbea020a..ddda89a75246 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 0194fbea020a026b8207cbd11f3ef600ebde0dc5 +Subproject commit ddda89a75246cb1e2706d67f03a1485f481d1c7d