From 6463ecf8598bfd8f07989c3a3938058c1242f5ea Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Mon, 23 Dec 2024 09:33:32 +0530 Subject: [PATCH 001/145] feat: [Auth Violations] Support isDismissed on newDot to hide violations. Signed-off-by: krishna2323 --- .../BrokenConnectionDescription.tsx | 5 ++- src/components/MoneyRequestHeader.tsx | 3 +- .../MoneyRequestPreviewContent.tsx | 20 +++++------ .../ReportActionItem/MoneyRequestView.tsx | 4 +-- .../ReportActionItem/ReportPreview.tsx | 3 +- src/libs/TransactionUtils/index.ts | 33 +++++++++++++++---- src/libs/actions/IOU.ts | 2 +- .../DebugTransactionViolations.tsx | 6 ++-- .../DebugTransactionViolationCreatePage.tsx | 4 +-- .../DebugTransactionViolationPage.tsx | 4 +-- src/pages/TransactionDuplicate/Review.tsx | 3 +- .../request/step/IOURequestStepAttendees.tsx | 4 +-- 12 files changed, 53 insertions(+), 38 deletions(-) diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index c54bd0058f99..12573c042418 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -1,13 +1,12 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import * as TransactionUtils from '@libs/TransactionUtils'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report} from '@src/types/onyx'; import TextLink from './TextLink'; @@ -26,7 +25,7 @@ type BrokenConnectionDescriptionProps = { function BrokenConnectionDescription({transactionID, policy, report}: BrokenConnectionDescriptionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); const brokenConnection530Error = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530); const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index f253c757050f..13c9880f65b3 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -56,7 +56,6 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? -1 : -1 }`, ); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true}); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const styles = useThemeStyles(); @@ -112,7 +111,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre ), }; } - if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1', transactionViolations))) { + if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1'))) { return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')}; } if (isScanning) { diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index ba0cda25d59e..d25eed67739a 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -83,7 +83,8 @@ function MoneyRequestPreviewContent({ const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : '-1'; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); - const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const [allViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const transactionViolations = TransactionUtils.getTransactionViolations(transaction?.transactionID); const sessionAccountID = session?.accountID; const managerID = iouReport?.managerID ?? -1; @@ -117,9 +118,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 ?? '-1', allViolations, true); + const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID ?? '-1', allViolations, true) && ReportUtils.isPaidGroupPolicy(iouReport); + const hasWarningTypeViolations = TransactionUtils.hasWarningTypeViolation(transaction?.transactionID ?? '-1', allViolations, true); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); @@ -134,11 +135,8 @@ function MoneyRequestPreviewContent({ // Get transaction violations for given transaction id from onyx, find duplicated transactions violations and get duplicates const allDuplicates = useMemo( - () => - transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction?.transactionID}`]?.find( - (violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION, - )?.data?.duplicates ?? [], - [transaction?.transactionID, transactionViolations], + () => transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [], + [transactionViolations], ); // Remove settled transactions from duplicates @@ -209,7 +207,7 @@ function MoneyRequestPreviewContent({ } if (shouldShowRBR && transaction) { - const violations = TransactionUtils.getTransactionViolations(transaction.transactionID, transactionViolations); + const violations = TransactionUtils.getTransactionViolations(transaction.transactionID); if (shouldShowHoldMessage) { return `${message} ${CONST.DOT_SEPARATOR} ${translate('violations.hold')}`; } @@ -256,7 +254,7 @@ function MoneyRequestPreviewContent({ if (TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', 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 ?? '-1'))) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; } return {shouldShow: false}; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 18750bfc7a29..19e3e8feca19 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -73,7 +73,7 @@ const receiptFieldViolationNames: OnyxTypes.ViolationName[] = [CONST.VIOLATIONS. const getTransactionID = (report: OnyxEntry, parentReportActions: OnyxEntry) => { const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; const originalMessage = parentReportAction && ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction) : undefined; - return originalMessage?.IOUTransactionID ?? -1; + return originalMessage?.IOUTransactionID ?? undefined; }; function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { @@ -95,7 +95,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { canEvict: false, }); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${getTransactionID(report, parentReportActions)}`); + const transactionViolations = TransactionUtils.getTransactionViolations(getTransactionID(report, parentReportActions) ?? undefined); const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; const isTrackExpense = ReportUtils.isTrackExpenseReport(report); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 79497e5fab88..e9f4978e6fe1 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -188,8 +188,7 @@ function ReportPreview({ const lastThreeTransactions = allTransactions.slice(-3); 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)); + numberOfRequests === 1 && TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID ?? '-1')); const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && TransactionUtils.shouldShowBrokenConnectionViolation(allTransactions.at(0)?.transactionID ?? '-1', iouReport, policy); let formattedMerchant = numberOfRequests === 1 ? TransactionUtils.getMerchant(allTransactions.at(0)) : null; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 6643cd721d45..62f2007e01d6 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -703,8 +703,11 @@ function hasMissingSmartscanFields(transaction: OnyxInputOrEntry): /** * Get all transaction violations of the transaction with given tranactionID. */ -function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection | null): TransactionViolations | null { - return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; +function getTransactionViolations(transactionID: string | undefined): TransactionViolations | null { + if (!transactionID) { + return null; + } + return allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((v) => !isViolationDismissed(transactionID, v)) ?? null; } /** @@ -724,7 +727,7 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | * Check if there is broken connection violation. */ function hasBrokenConnectionViolation(transactionID: string): boolean { - const violations = getTransactionViolations(transactionID, allTransactionViolations); + const violations = getTransactionViolations(transactionID); return !!violations?.find( (violation) => violation.name === CONST.VIOLATIONS.RTER && @@ -747,7 +750,7 @@ function shouldShowBrokenConnectionViolation(transactionID: string, report: Onyx */ function allHavePendingRTERViolation(transactionIds: string[]): boolean { const transactionsWithRTERViolations = transactionIds.map((transactionId) => { - const transactionViolations = getTransactionViolations(transactionId, allTransactionViolations); + const transactionViolations = getTransactionViolations(transactionId); return hasPendingRTERViolation(transactionViolations); }); return transactionsWithRTERViolations.length > 0 && transactionsWithRTERViolations.every((value) => value === true); @@ -878,12 +881,22 @@ function isOnHoldByTransactionID(transactionID: string): boolean { return isOnHold(allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]); } +/** + * Checks if a violation is dismissed for the given transaction + */ +function isViolationDismissed(transactionID: string, violation: TransactionViolation): boolean { + return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]?.comment?.dismissedViolations?.[violation.name]?.[currentUserEmail] === `${currentUserAccountID}`; +} + /** * Checks if any violations for the provided transaction are of type 'violation' */ function hasViolation(transactionID: string, 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)), + (violation: TransactionViolation) => + violation.type === CONST.VIOLATION_TYPES.VIOLATION && + (showInReview === undefined || showInReview === (violation.showInReview ?? false)) && + !isViolationDismissed(transactionID, violation), ); } @@ -892,7 +905,10 @@ function hasViolation(transactionID: string, transactionViolations: OnyxCollecti */ function hasNoticeTypeViolation(transactionID: string, 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)), + (violation: TransactionViolation) => + violation.type === CONST.VIOLATION_TYPES.NOTICE && + (showInReview === undefined || showInReview === (violation.showInReview ?? false)) && + !isViolationDismissed(transactionID, violation), ); } @@ -903,7 +919,10 @@ function hasWarningTypeViolation(transactionID: string, transactionViolations: O const violations = transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]; const warningTypeViolations = violations?.filter( - (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.WARNING && (showInReview === undefined || showInReview === (violation.showInReview ?? false)), + (violation: TransactionViolation) => + violation.type === CONST.VIOLATION_TYPES.WARNING && + (showInReview === undefined || showInReview === (violation.showInReview ?? false)) && + !isViolationDismissed(transactionID, violation), ) ?? []; const hasOnlyDupeDetectionViolation = warningTypeViolations?.every((violation: TransactionViolation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c8d6fb36f60d..5a3bb14fb527 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3293,7 +3293,7 @@ function updateMoneyRequestAttendees( policy: OnyxEntry, policyTagList: OnyxEntry, policyCategories: OnyxEntry, - violations: OnyxEntry, + violations?: OnyxEntry, ) { const transactionChanges: TransactionChanges = { attendees, diff --git a/src/pages/Debug/Transaction/DebugTransactionViolations.tsx b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx index d3e37f726a96..a7cf469ffa0a 100644 --- a/src/pages/Debug/Transaction/DebugTransactionViolations.tsx +++ b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx @@ -1,6 +1,5 @@ import React from 'react'; import type {ListRenderItemInfo} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import FlatList from '@components/FlatList'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; @@ -9,7 +8,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import ONYXKEYS from '@src/ONYXKEYS'; +import * as TransactionUtils from '@libs/TransactionUtils'; import ROUTES from '@src/ROUTES'; import type {TransactionViolation} from '@src/types/onyx'; @@ -19,7 +18,8 @@ type DebugTransactionViolationsProps = { }; function DebugTransactionViolations({transactionID}: DebugTransactionViolationsProps) { - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); + const styles = useThemeStyles(); const {translate} = useLocalize(); diff --git a/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx b/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx index b5f3d0d603d5..8d9df99a7dfa 100644 --- a/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx +++ b/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -14,6 +13,7 @@ import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; +import * as TransactionUtils from '@libs/TransactionUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import Debug from '@userActions/Debug'; import CONST from '@src/CONST'; @@ -62,7 +62,7 @@ function DebugTransactionViolationCreatePage({ }: DebugTransactionViolationCreatePageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); const [draftTransactionViolation, setDraftTransactionViolation] = useState(() => getInitialTransactionViolation()); const [error, setError] = useState(); diff --git a/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx b/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx index f615060ab6df..a9293592eee5 100644 --- a/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx +++ b/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useMemo} from 'react'; import {InteractionManager, View} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import TabSelector from '@components/TabSelector/TabSelector'; @@ -13,6 +12,7 @@ import Navigation from '@libs/Navigation/Navigation'; import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; +import * as TransactionUtils from '@libs/TransactionUtils'; import DebugDetails from '@pages/Debug/DebugDetails'; import DebugJSON from '@pages/Debug/DebugJSON'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -29,7 +29,7 @@ function DebugTransactionViolationPage({ }, }: DebugTransactionViolationPageProps) { const {translate} = useLocalize(); - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); const transactionViolation = useMemo(() => transactionViolations?.[Number(index)], [index, transactionViolations]); const styles = useThemeStyles(); diff --git a/src/pages/TransactionDuplicate/Review.tsx b/src/pages/TransactionDuplicate/Review.tsx index cb27ecfcbb3c..e7f1ec355d57 100644 --- a/src/pages/TransactionDuplicate/Review.tsx +++ b/src/pages/TransactionDuplicate/Review.tsx @@ -30,7 +30,8 @@ function TransactionDuplicateReview() { const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`); const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); const transactionID = ReportActionsUtils.getLinkedTransactionID(reportAction, report?.reportID ?? '-1') ?? '-1'; - const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); + const duplicateTransactionIDs = useMemo( () => transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [], [transactionViolations], diff --git a/src/pages/iou/request/step/IOURequestStepAttendees.tsx b/src/pages/iou/request/step/IOURequestStepAttendees.tsx index 409ef1cfe02d..105cedd62dae 100644 --- a/src/pages/iou/request/step/IOURequestStepAttendees.tsx +++ b/src/pages/iou/request/step/IOURequestStepAttendees.tsx @@ -43,7 +43,7 @@ function IOURequestStepAttendees({ const [attendees, setAttendees] = useState(() => TransactionUtils.getAttendees(transaction)); const previousAttendees = usePrevious(attendees); const {translate} = useLocalize(); - const [violations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const violations = TransactionUtils.getTransactionViolations(transactionID); const saveAttendees = useCallback(() => { if (attendees.length <= 0) { @@ -52,7 +52,7 @@ function IOURequestStepAttendees({ if (!lodashIsEqual(previousAttendees, attendees)) { IOU.setMoneyRequestAttendees(transactionID, attendees, !isEditing); if (isEditing) { - IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations); + IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations ?? undefined); } } From 2a179a29c1d374f88eae29c3ff34031e0fb67054 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 5 Jan 2025 18:00:39 +0530 Subject: [PATCH 002/145] fix eslint. Signed-off-by: krishna2323 --- src/libs/TransactionUtils/index.ts | 14 ++++++++++---- .../Transaction/DebugTransactionViolations.tsx | 2 -- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 1b09260f7e79..30e4e507a12b 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -750,8 +750,11 @@ function hasMissingSmartscanFields(transaction: OnyxInputOrEntry): /** * Get all transaction violations of the transaction with given tranactionID. */ -function getTransactionViolations(transactionID: string | undefined, transactionViolations: OnyxCollection | null): TransactionViolations | null { - return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; +function getTransactionViolations(transactionID: string | undefined): TransactionViolations | null { + if (transactionID) { + return null; + } + return allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((v) => !isViolationDismissed(transactionID, v)) ?? null; } /** @@ -771,7 +774,7 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | * Check if there is broken connection violation. */ function hasBrokenConnectionViolation(transactionID?: string): boolean { - const violations = getTransactionViolations(transactionID, allTransactionViolations); + const violations = getTransactionViolations(transactionID); return !!violations?.find( (violation) => violation.name === CONST.VIOLATIONS.RTER && @@ -928,7 +931,10 @@ function isOnHoldByTransactionID(transactionID: string): boolean { /** * Checks if a violation is dismissed for the given transaction */ -function isViolationDismissed(transactionID: string, violation: TransactionViolation): boolean { +function isViolationDismissed(transactionID: string | undefined, violation: TransactionViolation): boolean { + if (!transactionID) { + return false; + } return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]?.comment?.dismissedViolations?.[violation.name]?.[currentUserEmail] === `${currentUserAccountID}`; } diff --git a/src/pages/Debug/Transaction/DebugTransactionViolations.tsx b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx index ea01e8746b58..5d0c846b40d6 100644 --- a/src/pages/Debug/Transaction/DebugTransactionViolations.tsx +++ b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx @@ -1,6 +1,4 @@ import React from 'react'; -import type {ListRenderItemInfo} from 'react-native'; -import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import ScrollView from '@components/ScrollView'; From cd59a56006c08747160b281cbb23f717a081cf10 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 5 Jan 2025 18:18:29 +0530 Subject: [PATCH 003/145] fix eslint. Signed-off-by: krishna2323 --- src/components/BrokenConnectionDescription.tsx | 9 +++++++-- src/components/MoneyRequestHeader.tsx | 17 ++++++++++------- .../MoneyRequestPreviewContent.tsx | 2 +- src/libs/TransactionUtils/index.ts | 5 ++++- src/libs/actions/Transaction.ts | 5 ++++- 5 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index 12573c042418..6567a4269275 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -39,13 +39,18 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn return translate('violations.brokenConnection530Error'); } - if (isPolicyAdmin && !ReportUtils.isCurrentUserSubmitter(report?.reportID ?? '')) { + if (isPolicyAdmin && !ReportUtils.isCurrentUserSubmitter(report?.reportID)) { return ( <> {`${translate('violations.adminBrokenConnectionError')}`} Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id ?? '-1'))} + onPress={() => { + if (!policy?.id) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id)); + }} >{`${translate('workspace.common.companyCards')}`} . diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 13c9880f65b3..97ff01a57463 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -16,6 +16,7 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import * as TransactionActions from '@userActions/Transaction'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -50,10 +51,12 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const route = useRoute(); - const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? CONST.DEFAULT_NUMBER_ID}`); const [transaction] = useOnyx( `${ONYXKEYS.COLLECTION.TRANSACTION}${ - ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? -1 : -1 + ReportActionsUtils.isMoneyRequestAction(parentReportAction) + ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID + : CONST.DEFAULT_NUMBER_ID }`, ); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true}); @@ -63,21 +66,21 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const {translate} = useLocalize(); const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); const isOnHold = TransactionUtils.isOnHold(transaction); - const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? ''); + const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID); const reportID = report?.reportID; const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; - const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']); + const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation(transaction?.transactionID ? [transaction?.transactionID] : []); - const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', parentReport, policy); + const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID, parentReport, policy); const shouldShowMarkAsCashButton = - hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isCurrentUserSubmitter(parentReport?.reportID ?? ''))); + hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isCurrentUserSubmitter(parentReport?.reportID))); const markAsCash = useCallback(() => { - TransactionActions.markAsCash(transaction?.transactionID ?? '-1', reportID ?? ''); + TransactionActions.markAsCash(transaction?.transactionID, reportID); }, [reportID, transaction?.transactionID]); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index f586855a0d2a..79af844528a5 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -254,7 +254,7 @@ function MoneyRequestPreviewContent({ 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', allViolations))) { + if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1'))) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; } return {shouldShow: false}; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 30e4e507a12b..2bf77d10e2fe 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -894,7 +894,10 @@ function getRecentTransactions(transactions: Record, size = 2): * @param transactionID - the transaction to check * @param checkDismissed - whether to check if the violation has already been dismissed as well */ -function isDuplicate(transactionID: string, checkDismissed = false): boolean { +function isDuplicate(transactionID: string | undefined, checkDismissed = false): boolean { + if (!transactionID) { + return false; + } const hasDuplicatedViolation = !!allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]?.some( (violation: TransactionViolation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION, ); diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index c8a007458242..8fd955ce2515 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -461,7 +461,10 @@ function clearError(transactionID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null, errorFields: {route: null, waypoints: null, routes: null}}); } -function markAsCash(transactionID: string, transactionThreadReportID: string) { +function markAsCash(transactionID: string | undefined, transactionThreadReportID: string | undefined) { + if (!transactionID || !transactionThreadReportID) { + return; + } const optimisticReportAction = buildOptimisticDismissedViolationReportAction({ reason: 'manual', violationName: CONST.VIOLATIONS.RTER, From 5682ae0e9ce762f741d0981f2b912d3ec1bdb268 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 5 Jan 2025 18:34:36 +0530 Subject: [PATCH 004/145] fix eslint. Signed-off-by: krishna2323 --- src/components/BrokenConnectionDescription.tsx | 2 +- src/components/MoneyRequestHeader.tsx | 4 ++-- .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 8 ++++---- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- src/libs/TransactionUtils/index.ts | 5 ++++- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index 6567a4269275..9ea72ff539bc 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -13,7 +13,7 @@ import TextLink from './TextLink'; type BrokenConnectionDescriptionProps = { /** Transaction id of the corresponding report */ - transactionID: string; + transactionID?: string; /** Current report */ report: OnyxEntry; diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 97ff01a57463..89eac418d2c5 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -107,14 +107,14 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre icon: getStatusIcon(Expensicons.Hourglass), description: ( ), }; } - if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1'))) { + if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID))) { return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')}; } if (isScanning) { diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 79af844528a5..5cebd1ab5976 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -118,9 +118,9 @@ function MoneyRequestPreviewContent({ const isOnHold = TransactionUtils.isOnHold(transaction); const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; const isPartialHold = isSettlementOrApprovalPartial && isOnHold; - const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID ?? '-1', allViolations, true); - const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID ?? '-1', allViolations, true) && ReportUtils.isPaidGroupPolicy(iouReport); - const hasWarningTypeViolations = TransactionUtils.hasWarningTypeViolation(transaction?.transactionID ?? '-1', allViolations, true); + const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID, allViolations, true); + const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID, allViolations, true) && ReportUtils.isPaidGroupPolicy(iouReport); + const hasWarningTypeViolations = TransactionUtils.hasWarningTypeViolation(transaction?.transactionID, allViolations, true); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); @@ -254,7 +254,7 @@ function MoneyRequestPreviewContent({ 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'))) { + if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID))) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; } return {shouldShow: false}; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 19e3e8feca19..f7961c97249a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -71,7 +71,7 @@ const receiptImageViolationNames: OnyxTypes.ViolationName[] = [ const receiptFieldViolationNames: OnyxTypes.ViolationName[] = [CONST.VIOLATIONS.MODIFIED_AMOUNT, CONST.VIOLATIONS.MODIFIED_DATE]; const getTransactionID = (report: OnyxEntry, parentReportActions: OnyxEntry) => { - const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? CONST.DEFAULT_NUMBER_ID]; const originalMessage = parentReportAction && ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction) : undefined; return originalMessage?.IOUTransactionID ?? undefined; }; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 2bf77d10e2fe..9503d2a51664 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -751,7 +751,7 @@ function hasMissingSmartscanFields(transaction: OnyxInputOrEntry): * Get all transaction violations of the transaction with given tranactionID. */ function getTransactionViolations(transactionID: string | undefined): TransactionViolations | null { - if (transactionID) { + if (!transactionID) { return null; } return allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((v) => !isViolationDismissed(transactionID, v)) ?? null; @@ -945,6 +945,9 @@ function isViolationDismissed(transactionID: string | undefined, violation: Tran * Checks if any violations for the provided transaction are of type 'violation' */ function hasViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { + if (!transactionID) { + return false; + } return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some( (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION && From 10160650998f736eebeb4eedb1e7c8e6367ccb40 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jan 2025 03:01:21 +0530 Subject: [PATCH 005/145] fix eslint. Signed-off-by: krishna2323 --- src/components/MoneyRequestHeader.tsx | 20 +++++-- .../ReportActionItem/MoneyRequestView.tsx | 56 +++++++++---------- src/libs/actions/IOU.ts | 7 ++- src/libs/actions/Transaction.ts | 5 +- 4 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 89eac418d2c5..885d91364917 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -157,11 +157,15 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre shouldShowReportAvatarWithDisplay shouldEnableDetailPageNavigation shouldShowPinButton={false} - report={{ - ...report, - reportID: reportID ?? '', - ownerAccountID: parentReport?.ownerAccountID, - }} + report={ + reportID + ? { + ...report, + reportID: reportID ?? '', + ownerAccountID: parentReport?.ownerAccountID, + } + : undefined + } policy={policy} shouldShowBackButton={shouldUseNarrowLayout} shouldDisplaySearchRouter={shouldDisplaySearchRouter} @@ -181,6 +185,9 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre text={translate('iou.reviewDuplicates')} style={[styles.p0, styles.ml2]} onPress={() => { + if (!reportID) { + return; + } Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID ?? '', Navigation.getReportRHPActiveRoute())); }} /> @@ -203,6 +210,9 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre text={translate('iou.reviewDuplicates')} style={[styles.w100, styles.pr0]} onPress={() => { + if (!reportID) { + return; + } Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID ?? '', Navigation.getReportRHPActiveRoute())); }} /> diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index f7961c97249a..a4e29dbf9073 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -81,32 +81,32 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const session = useSession(); const {isOffline} = useNetwork(); const {translate, toLocaleDigit} = useLocalize(); - const parentReportID = report?.parentReportID ?? '-1'; - const policyID = report?.policyID ?? '-1'; - const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`); + const parentReportID = report?.parentReportID; + const policyID = report?.policyID; + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID ?? CONST.DEFAULT_NUMBER_ID}`); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReport?.parentReportID}`, { selector: (chatReportValue) => chatReportValue && {reportID: chatReportValue.reportID, errorFields: chatReportValue.errorFields}, }); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID ?? CONST.DEFAULT_NUMBER_ID}`); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID ?? CONST.DEFAULT_NUMBER_ID}`); const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${updatedTransaction?.reportID}`); - const targetPolicyID = updatedTransaction?.reportID ? transactionReport?.policyID : policyID; + const targetPolicyID = updatedTransaction?.reportID ? transactionReport?.policyID : policyID ?? CONST.DEFAULT_NUMBER_ID; const [policyTagList] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`); - const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID ?? CONST.DEFAULT_NUMBER_ID}`, { canEvict: false, }); const transactionViolations = TransactionUtils.getTransactionViolations(getTransactionID(report, parentReportActions) ?? undefined); - const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? CONST.DEFAULT_NUMBER_ID]; const isTrackExpense = ReportUtils.isTrackExpenseReport(report); const moneyRequestReport = parentReport; const linkedTransactionID = useMemo(() => { const originalMessage = parentReportAction && ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction) : undefined; - return originalMessage?.IOUTransactionID ?? '-1'; + return originalMessage?.IOUTransactionID; }, [parentReportAction]); - const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`); - const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${linkedTransactionID}`); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID ?? CONST.DEFAULT_NUMBER_ID}`); + const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${linkedTransactionID ?? CONST.DEFAULT_NUMBER_ID}`); const { created: transactionDate, @@ -227,7 +227,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals if (newBillable === TransactionUtils.getBillable(transaction)) { return; } - IOU.updateMoneyRequestBillable(transaction?.transactionID ?? '-1', report?.reportID ?? '-1', newBillable, policy, policyTagList, policyCategories); + IOU.updateMoneyRequestBillable(transaction?.transactionID, report?.reportID, newBillable, policy, policyTagList, policyCategories); }, [transaction, report, policy, policyTagList, policyCategories], ); @@ -318,17 +318,14 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals interactive={canEditDistance} shouldShowRightIcon={canEditDistance} titleStyle={styles.flex1} - onPress={() => + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1', - Navigation.getReportRHPActiveRoute(), - ), - ) - } + ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report?.reportID, Navigation.getReportRHPActiveRoute()), + ); + }} /> @@ -338,17 +335,20 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals interactive={canEditDistanceRate} shouldShowRightIcon={canEditDistanceRate} titleStyle={styles.flex1} - onPress={() => + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1', + transaction?.transactionID, + report?.reportID, Navigation.getReportRHPActiveRoute(), ), - ) - } + ); + }} brickRoadIndicator={getErrorForField('customUnitRateID') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('customUnitRateID')} /> @@ -455,7 +455,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals errors={errors} errorRowStyles={[styles.mh4]} onClose={() => { - if (!transaction?.transactionID && linkedTransactionID === '-1') { + if (!transaction?.transactionID && !linkedTransactionID) { return; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index d9e39efb61f5..f2b4da2f972a 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3394,13 +3394,16 @@ function updateMoneyRequestDate( /** Updates the billable field of an expense */ function updateMoneyRequestBillable( - transactionID: string, - transactionThreadReportID: string, + transactionID: string | undefined, + transactionThreadReportID: string | undefined, value: boolean, policy: OnyxEntry, policyTagList: OnyxEntry, policyCategories: OnyxEntry, ) { + if (!transactionID || !transactionThreadReportID) { + return; + } const transactionChanges: TransactionChanges = { billable: value, }; diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 8fd955ce2515..311b8dfea7dc 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -457,7 +457,10 @@ function abandonReviewDuplicateTransactions() { Onyx.set(ONYXKEYS.REVIEW_DUPLICATES, null); } -function clearError(transactionID: string) { +function clearError(transactionID?: string) { + if (!transactionID) { + return; + } Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null, errorFields: {route: null, waypoints: null, routes: null}}); } From a2d0da6534a383184151a747f596a7e943de4c9a Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jan 2025 03:26:58 +0530 Subject: [PATCH 006/145] fix ESLint. Signed-off-by: krishna2323 --- src/components/MoneyRequestHeader.tsx | 6 +-- .../ReportActionItem/MoneyRequestView.tsx | 41 +++++++++++-------- src/libs/actions/ReportActions.ts | 5 ++- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 885d91364917..edc9ab1e6250 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -161,7 +161,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre reportID ? { ...report, - reportID: reportID ?? '', + reportID, ownerAccountID: parentReport?.ownerAccountID, } : undefined @@ -188,7 +188,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre if (!reportID) { return; } - Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID ?? '', Navigation.getReportRHPActiveRoute())); + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID, Navigation.getReportRHPActiveRoute())); }} /> )} @@ -213,7 +213,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre if (!reportID) { return; } - Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID ?? '', Navigation.getReportRHPActiveRoute())); + Navigation.navigate(ROUTES.TRANSACTION_DUPLICATE_REVIEW_PAGE.getRoute(reportID, Navigation.getReportRHPActiveRoute())); }} /> diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index a4e29dbf9073..148418ccdcf5 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -418,18 +418,21 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals interactive={canEdit} shouldShowRightIcon={canEdit} titleStyle={styles.flex1} - onPress={() => + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_TAG.getRoute( CONST.IOU.ACTION.EDIT, iouType, orderWeight, - transaction?.transactionID ?? '', - report?.reportID ?? '-1', + transaction?.transactionID, + report?.reportID, Navigation.getReportRHPActiveRoute(), ), - ) - } + ); + }} brickRoadIndicator={tagError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={tagError} /> @@ -470,7 +473,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals } } Transaction.clearError(transaction?.transactionID ?? linkedTransactionID); - ReportActions.clearAllRelatedReportActionErrors(report?.reportID ?? '-1', parentReportAction); + ReportActions.clearAllRelatedReportActionErrors(report?.reportID, parentReportAction); }} > {hasReceipt && ( @@ -496,17 +499,20 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1', + transaction?.transactionID, + report?.reportID, Navigation.getReportRHPActiveRoute(), ), - ) - } + ); + }} /> )} @@ -521,18 +527,21 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals titleStyle={styles.textHeadlineH2} interactive={canEditAmount} shouldShowRightIcon={canEditAmount} - onPress={() => + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_AMOUNT.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1', + transaction?.transactionID, + report?.reportID, '', Navigation.getReportRHPActiveRoute(), ), - ) - } + ); + }} brickRoadIndicator={getErrorForField('amount') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('amount')} /> diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index 89517a753c26..95d9d3d17b28 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -81,7 +81,10 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction, k ignore: `undefined` means we want to check both parent and children report actions ignore: `parent` or `child` means we want to ignore checking parent or child report actions because they've been previously checked */ -function clearAllRelatedReportActionErrors(reportID: string, reportAction: ReportAction | null | undefined, ignore?: IgnoreDirection, keys?: string[]) { +function clearAllRelatedReportActionErrors(reportID: string | undefined, reportAction: ReportAction | null | undefined, ignore?: IgnoreDirection, keys?: string[]) { + if (!reportID) { + return; + } const errorKeys = keys ?? Object.keys(reportAction?.errors ?? {}); if (!reportAction || errorKeys.length === 0) { return; From ee414e3ef8057cea36dd84c3ac458a1c0d76bbc6 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 7 Jan 2025 03:32:37 +0530 Subject: [PATCH 007/145] fix ESLint. Signed-off-by: krishna2323 --- .../ReportActionItem/MoneyRequestView.tsx | 100 +++++++++++------- 1 file changed, 59 insertions(+), 41 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 148418ccdcf5..f2cec506ddc8 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -554,17 +554,20 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals interactive={canEdit} shouldShowRightIcon={canEdit} titleStyle={styles.flex1} - onPress={() => + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1', + transaction?.transactionID, + report?.reportID, Navigation.getReportRHPActiveRoute(), ), - ) - } + ); + }} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} brickRoadIndicator={getErrorForField('comment') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('comment')} @@ -581,17 +584,20 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals interactive={canEditMerchant} shouldShowRightIcon={canEditMerchant} titleStyle={styles.flex1} - onPress={() => + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1', + transaction?.transactionID, + report?.reportID, Navigation.getReportRHPActiveRoute(), ), - ) - } + ); + }} wrapperStyle={[styles.taskDescriptionMenuItem]} brickRoadIndicator={getErrorForField('merchant') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('merchant')} @@ -606,17 +612,14 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals interactive={canEditDate} shouldShowRightIcon={canEditDate} titleStyle={styles.flex1} - onPress={() => + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( - ROUTES.MONEY_REQUEST_STEP_DATE.getRoute( - CONST.IOU.ACTION.EDIT, - iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1' ?? '-1', - Navigation.getReportRHPActiveRoute(), - ), - ) - } + ROUTES.MONEY_REQUEST_STEP_DATE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report?.reportID, Navigation.getReportRHPActiveRoute()), + ); + }} brickRoadIndicator={getErrorForField('date') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('date')} /> @@ -629,17 +632,20 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals interactive={canEdit} shouldShowRightIcon={canEdit} titleStyle={styles.flex1} - onPress={() => + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1', + transaction?.transactionID, + report?.reportID, Navigation.getReportRHPActiveRoute(), ), - ) - } + ); + }} brickRoadIndicator={getErrorForField('category') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('category')} /> @@ -659,22 +665,25 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals {shouldShowTax && ( + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_TAX_RATE.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1', + transaction?.transactionID, + report?.reportID, Navigation.getReportRHPActiveRoute(), ), - ) - } + ); + }} brickRoadIndicator={getErrorForField('tax') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} errorText={getErrorForField('tax')} /> @@ -688,17 +697,20 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals interactive={canEditTaxFields} shouldShowRightIcon={canEditTaxFields} titleStyle={styles.flex1} - onPress={() => + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.getRoute( CONST.IOU.ACTION.EDIT, iouType, - transaction?.transactionID ?? '-1', - report?.reportID ?? '-1', + transaction?.transactionID, + report?.reportID, Navigation.getReportRHPActiveRoute(), ), - ) - } + ); + }} /> )} @@ -707,11 +719,14 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals title={translate('travel.viewTripDetails')} icon={Expensicons.Suitcase} onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } const reservations = transaction?.receipt?.reservationList?.length ?? 0; if (reservations > 1) { - Navigation.navigate(ROUTES.TRAVEL_TRIP_SUMMARY.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', Navigation.getReportRHPActiveRoute())); + Navigation.navigate(ROUTES.TRAVEL_TRIP_SUMMARY.getRoute(report?.reportID, transaction?.transactionID, Navigation.getReportRHPActiveRoute())); } - Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', 0, Navigation.getReportRHPActiveRoute())); + Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.reportID, transaction?.transactionID, 0, Navigation.getReportRHPActiveRoute())); }} /> )} @@ -726,9 +741,12 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals }`} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - onPress={() => - Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID ?? '-1', report?.reportID ?? '-1')) - } + onPress={() => { + if (!transaction?.transactionID || !report?.reportID) { + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction?.transactionID, report?.reportID)); + }} interactive shouldRenderAsHTML /> From 1d1275b1534894df1f5756633c692014eb4845b3 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 8 Jan 2025 17:06:28 +0700 Subject: [PATCH 008/145] fix: /bin/zsh doesn't save in the Receipt --- .../workspace/rules/IndividualExpenseRulesSection.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx index 7b9acc5e6ea0..62ea0150eedb 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx @@ -88,7 +88,7 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; const maxExpenseAmountNoReceiptText = useMemo(() => { - if (policy?.maxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE || !policy?.maxExpenseAmountNoReceipt) { + if (policy?.maxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE) { return ''; } @@ -96,7 +96,7 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection }, [policy?.maxExpenseAmountNoReceipt, policyCurrency]); const maxExpenseAmountText = useMemo(() => { - if (policy?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE || !policy?.maxExpenseAmount) { + if (policy?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE) { return ''; } @@ -104,11 +104,11 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection }, [policy?.maxExpenseAmount, policyCurrency]); const maxExpenseAgeText = useMemo(() => { - if (policy?.maxExpenseAge === CONST.DISABLED_MAX_EXPENSE_VALUE || !policy?.maxExpenseAge) { + if (policy?.maxExpenseAge === CONST.DISABLED_MAX_EXPENSE_VALUE) { return ''; } - return translate('workspace.rules.individualExpenseRules.maxExpenseAgeDays', {count: policy?.maxExpenseAge}); + return translate('workspace.rules.individualExpenseRules.maxExpenseAgeDays', {count: policy?.maxExpenseAge ?? 0}); }, [policy?.maxExpenseAge, translate]); const billableModeText = translate(`workspace.rules.individualExpenseRules.${policy?.defaultBillable ? 'billable' : 'nonBillable'}`); From be23aa732248adc3a7bf26f38e111782b53656cd Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 8 Jan 2025 22:37:30 +0700 Subject: [PATCH 009/145] fix lint --- src/pages/workspace/rules/IndividualExpenseRulesSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx index 62ea0150eedb..365fa083ec6f 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx @@ -39,7 +39,7 @@ type IndividualExpenseRulesMenuItem = { }; function IndividualExpenseRulesSectionSubtitle({policy, translate, styles}: IndividualExpenseRulesSectionSubtitleProps) { - const policyID = policy?.id ?? '-1'; + const policyID = `${policy?.id ?? CONST.DEFAULT_NUMBER_ID}`; const handleOnPressCategoriesLink = () => { if (policy?.areCategoriesEnabled) { From 4978349cc0cff2889334d5a7819b703c3c3ea63b Mon Sep 17 00:00:00 2001 From: daledah Date: Thu, 9 Jan 2025 17:04:07 +0700 Subject: [PATCH 010/145] fix: navigate to parent report on delete track expense --- src/libs/Navigation/Navigation.ts | 25 ++++++++++++++++++++++++- src/libs/ReportUtils.ts | 10 ++++++++-- src/pages/ReportDetailsPage.tsx | 2 +- src/pages/home/ReportScreen.tsx | 3 ++- tests/actions/IOUTest.ts | 11 +++++++++++ 5 files changed, 46 insertions(+), 5 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index eeb6db21447e..62efc1c50329 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -11,7 +11,7 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {HybridAppRoute, Route} from '@src/ROUTES'; import ROUTES, {HYBRID_APP_ROUTES} from '@src/ROUTES'; -import {PROTECTED_SCREENS} from '@src/SCREENS'; +import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS'; import type {Screen} from '@src/SCREENS'; import type {Report} from '@src/types/onyx'; import originalCloseRHPFlow from './closeRHPFlow'; @@ -428,6 +428,13 @@ function getTopMostCentralPaneRouteFromRootState() { return getTopmostCentralPaneRoute(navigationRef.getRootState() as State); } +function getPreviousTrackReport(reportID?: string) { + if (!reportID) { + return null; + } + return navigationRef.getRootState().routes.find((r) => r.name === SCREENS.REPORT && !!r.params && 'reportID' in r.params && r.params.reportID === reportID); +} + function removeScreenFromNavigationState(screen: Screen) { isNavigationReady().then(() => { navigationRef.dispatch((state) => { @@ -442,6 +449,20 @@ function removeScreenFromNavigationState(screen: Screen) { }); } +function removeScreenByKey(key: string) { + isNavigationReady().then(() => { + navigationRef.dispatch((state) => { + const routes = state.routes?.filter((item) => item.key !== key); + + return CommonActions.reset({ + ...state, + routes, + index: routes.length < state.routes.length ? state.index - 1 : state.index, + }); + }); + }); +} + export default { setShouldPopAllStateOnUP, navigate, @@ -467,6 +488,8 @@ export default { setNavigationActionToMicrotaskQueue, getTopMostCentralPaneRouteFromRootState, removeScreenFromNavigationState, + getPreviousTrackReport, + removeScreenByKey, }; export {navigationRef}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 8a5ae8b1d102..21737fda5a2d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4312,7 +4312,7 @@ function goBackToDetailsPage(report: OnyxEntry, backTo?: string) { } } -function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP?: boolean) { +function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP?: boolean, reportID?: string) { if (!backRoute) { return; } @@ -4322,7 +4322,13 @@ function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP return; } if (isFromRHP) { - Navigation.dismissModal(); + if (reportID) { + const trackReport = Navigation.getPreviousTrackReport(reportID); + if (trackReport?.key) { + Navigation.removeScreenByKey(trackReport.key); + } + } + Navigation.isNavigationReady().then(() => Navigation.dismissModal()); } Navigation.isNavigationReady().then(() => { Navigation.goBack(backRoute); diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 166b12b27751..17609a24e3c2 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -904,7 +904,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta Navigation.dismissModal(); } else { Report.setDeleteTransactionNavigateBackUrl(urlToNavigateBack); - ReportUtils.navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true); + ReportUtils.navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true, report.reportID); } }, [caseID, iouTransactionID, moneyRequestReport?.reportID, report, requestParentReportAction, isSingleTransactionView, isTransactionDeleted]); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index ef3137a8c7d2..191aaa6ada5d 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -736,7 +736,8 @@ function ReportScreen({route, navigation}: ReportScreenProps) { !isSingleExpenseReport && !isSingleInvoiceReport && !ReportActionsUtils.isActionOfType(mostRecentReportAction, CONST.REPORT.ACTIONS.TYPE.CREATED) && - !ReportActionsUtils.isDeletedAction(mostRecentReportAction); + !ReportActionsUtils.isDeletedAction(mostRecentReportAction) && + (!deleteTransactionNavigateBackUrl || !ReportActionsUtils.isActionOfType(mostRecentReportAction, CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_TRACK_EXPENSE_WHISPER)); const lastRoute = usePrevious(route); const lastReportActionIDFromRoute = usePrevious(reportActionIDFromRoute); diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index dc07c16c8d7f..3b51081ca96d 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -43,8 +43,19 @@ jest.mock('@src/libs/Navigation/Navigation', () => ({ dismissModal: jest.fn(), dismissModalWithReport: jest.fn(), goBack: jest.fn(), + removeScreenByKey: jest.fn(), + isNavigationReady: jest.fn(() => Promise.resolve()), + getPreviousTrackReport: jest.fn(), })); +jest.mock('@src/libs/Navigation/navigationRef', () => ({ + getRootState: () => ({ + routes: [], + }), +})); + +jest.mock('@react-navigation/native'); + jest.mock('@src/libs/Navigation/isSearchTopmostCentralPane', () => jest.fn()); const CARLOS_EMAIL = 'cmartins@expensifail.com'; From 18f9c6f48f721cd468ee68d1fac4a8cedbf2c6f9 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 9 Jan 2025 21:02:51 +0530 Subject: [PATCH 011/145] fix ESlint issues. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 5 ++--- src/libs/ReportActionsUtils.ts | 8 ++++++-- src/libs/actions/ReportActions.ts | 4 ++-- src/libs/actions/Transaction.ts | 12 ++++++------ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index ce217266ea0a..769690e63ce8 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -188,9 +188,8 @@ function ReportPreview({ const lastThreeTransactions = allTransactions.slice(-3); 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')); - const shouldShowBrokenConnectionViolation = - numberOfRequests === 1 && TransactionUtils.shouldShowBrokenConnectionViolation(allTransactions.at(0)?.transactionID ?? '-1', iouReport, policy); + numberOfRequests === 1 && TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID)); + 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 c1f4057199ee..6705584b0314 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -924,7 +924,8 @@ function getLatestReportActionFromOnyxData(onyxData: OnyxUpdate[] | null): NonNu * Find the transaction associated with this reportAction, if one exists. */ function getLinkedTransactionID(reportActionOrID: string | OnyxEntry, reportID?: string): string | null { - const reportAction = typeof reportActionOrID === 'string' ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]?.[reportActionOrID] : reportActionOrID; + const reportAction = + typeof reportActionOrID === 'string' ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID ?? CONST.DEFAULT_NUMBER_ID}`]?.[reportActionOrID] : reportActionOrID; if (!reportAction || !isMoneyRequestAction(reportAction)) { return null; } @@ -1599,7 +1600,10 @@ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry { +function getIOUActionForReportID(reportID?: string, transactionID?: string): OnyxEntry { + if (!reportID || !transactionID) { + return; + } const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const reportActions = getAllReportActions(report?.reportID); const action = Object.values(reportActions ?? {})?.find((reportAction) => { diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index 95d9d3d17b28..93752c3f02ab 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -41,7 +41,7 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction, k // If there's a linked transaction, delete that too // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(reportAction.reportActionID, originalReportID || '-1'); + const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(reportAction.reportActionID, originalReportID); if (linkedTransactionID) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportAction.childReportID}`, null); @@ -104,7 +104,7 @@ function clearAllRelatedReportActionErrors(reportID: string | undefined, reportA const childActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportAction.childReportID}`] ?? {}; Object.values(childActions).forEach((action) => { const childErrorKeys = Object.keys(action.errors ?? {}).filter((err) => errorKeys.includes(err)); - clearAllRelatedReportActionErrors(reportAction.childReportID ?? '-1', action, 'parent', childErrorKeys); + clearAllRelatedReportActionErrors(reportAction.childReportID, action, 'parent', childErrorKeys); }); } } diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 311b8dfea7dc..9bed660dcfbf 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -355,7 +355,7 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmissedPersonalDetails: PersonalDetails) { const currentTransactionViolations = transactionIDs.map((id) => ({transactionID: id, violations: allTransactionViolation?.[id] ?? []})); const currentTransactions = transactionIDs.map((id) => allTransactions?.[id]); - const transactionsReportActions = currentTransactions.map((transaction) => ReportActionsUtils.getIOUActionForReportID(transaction.reportID ?? '', transaction.transactionID ?? '')); + const transactionsReportActions = currentTransactions.map((transaction) => ReportActionsUtils.getIOUActionForReportID(transaction.reportID, transaction.transactionID)); const optimisticDissmidedViolationReportActions = transactionsReportActions.map(() => { return buildOptimisticDismissedViolationReportAction({reason: 'manual', violationName: CONST.VIOLATIONS.DUPLICATED_TRANSACTION}); }); @@ -365,7 +365,7 @@ function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmiss const optimisticReportActions: OnyxUpdate[] = transactionsReportActions.map((action, index) => ({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? '-1'}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? CONST.DEFAULT_NUMBER_ID}`, value: { [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? '']: optimisticDissmidedViolationReportActions.at(index) as ReportAction, }, @@ -413,9 +413,9 @@ function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmiss const failureReportActions: OnyxUpdate[] = transactionsReportActions.map((action, index) => ({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? '-1'}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? CONST.DEFAULT_NUMBER_ID}`, value: { - [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? '']: null, + [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? CONST.DEFAULT_NUMBER_ID]: null, }, })); @@ -425,9 +425,9 @@ function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmiss const successData: OnyxUpdate[] = transactionsReportActions.map((action, index) => ({ onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? '-1'}`, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? CONST.DEFAULT_NUMBER_ID}`, value: { - [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? '']: null, + [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? CONST.DEFAULT_NUMBER_ID]: null, }, })); // We are creating duplicate resolved report actions for each duplicate transactions and all the report actions From 40c5559ee7ad694427846a20e94afb4a88e87062 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 9 Jan 2025 21:09:50 +0530 Subject: [PATCH 012/145] fix ESlint issues. Signed-off-by: krishna2323 --- src/libs/actions/Transaction.ts | 2 +- src/pages/TransactionDuplicate/Review.tsx | 6 +++--- src/pages/iou/request/step/IOURequestStepAttendees.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 9bed660dcfbf..075eb4bc8e60 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -367,7 +367,7 @@ function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmiss onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? CONST.DEFAULT_NUMBER_ID}`, value: { - [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? '']: optimisticDissmidedViolationReportActions.at(index) as ReportAction, + [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? CONST.DEFAULT_NUMBER_ID]: optimisticDissmidedViolationReportActions.at(index) as ReportAction, }, })); const optimisticDataTransactionViolations: OnyxUpdate[] = currentTransactionViolations.map((transactionViolations) => ({ diff --git a/src/pages/TransactionDuplicate/Review.tsx b/src/pages/TransactionDuplicate/Review.tsx index e7f1ec355d57..5f010c4133ac 100644 --- a/src/pages/TransactionDuplicate/Review.tsx +++ b/src/pages/TransactionDuplicate/Review.tsx @@ -28,15 +28,15 @@ function TransactionDuplicateReview() { const route = useRoute>(); const currentPersonalDetails = useCurrentUserPersonalDetails(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`); - const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '-1', report?.parentReportActionID ?? '-1'); - const transactionID = ReportActionsUtils.getLinkedTransactionID(reportAction, report?.reportID ?? '-1') ?? '-1'; + const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID, report?.parentReportActionID); + const transactionID = ReportActionsUtils.getLinkedTransactionID(reportAction, report?.reportID) ?? undefined; const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); const duplicateTransactionIDs = useMemo( () => transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [], [transactionViolations], ); - const transactionIDs = [transactionID, ...duplicateTransactionIDs]; + const transactionIDs = transactionID ? [transactionID, ...duplicateTransactionIDs] : [...duplicateTransactionIDs]; const transactions = transactionIDs.map((item) => TransactionUtils.getTransaction(item)).sort((a, b) => new Date(a?.created ?? '').getTime() - new Date(b?.created ?? '').getTime()); diff --git a/src/pages/iou/request/step/IOURequestStepAttendees.tsx b/src/pages/iou/request/step/IOURequestStepAttendees.tsx index 105cedd62dae..6672306f5221 100644 --- a/src/pages/iou/request/step/IOURequestStepAttendees.tsx +++ b/src/pages/iou/request/step/IOURequestStepAttendees.tsx @@ -39,7 +39,7 @@ function IOURequestStepAttendees({ policyCategories, }: IOURequestStepAttendeesProps) { const isEditing = action === CONST.IOU.ACTION.EDIT; - const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || -1}`); + const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || CONST.DEFAULT_NUMBER_ID}`); const [attendees, setAttendees] = useState(() => TransactionUtils.getAttendees(transaction)); const previousAttendees = usePrevious(attendees); const {translate} = useLocalize(); From b32cbee063c9a61cefbc815bd16c07fcb7e97e3b Mon Sep 17 00:00:00 2001 From: daledah Date: Mon, 13 Jan 2025 17:36:04 +0700 Subject: [PATCH 013/145] fix: update removeScreenByKey --- src/libs/Navigation/Navigation.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 62efc1c50329..20b8cc31fff0 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -450,15 +450,14 @@ function removeScreenFromNavigationState(screen: Screen) { } function removeScreenByKey(key: string) { - isNavigationReady().then(() => { - navigationRef.dispatch((state) => { - const routes = state.routes?.filter((item) => item.key !== key); - - return CommonActions.reset({ - ...state, - routes, - index: routes.length < state.routes.length ? state.index - 1 : state.index, - }); + const state = navigationRef.getRootState(); + const routes = state.routes.filter((item) => item.key !== key); + + navigationRef.current?.dispatch(() => { + return CommonActions.reset({ + ...state, + routes, + index: routes.length < state.routes.length ? state.index - 1 : state.index, }); }); } From 3db3c2c0a07d44d6f5bce25783fcd8d000a97d65 Mon Sep 17 00:00:00 2001 From: daledah Date: Wed, 15 Jan 2025 15:54:09 +0700 Subject: [PATCH 014/145] fix: lint --- src/pages/ReportDetailsPage.tsx | 312 +++++++++++++++++++------------- src/pages/home/ReportScreen.tsx | 141 +++++++++------ 2 files changed, 281 insertions(+), 172 deletions(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 41cff0f32122..f18a46a4f405 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -31,20 +31,100 @@ import useNetwork from '@hooks/useNetwork'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ReportActions from '@libs/actions/Report'; +import { + cancelPayment as cancelPaymentIOU, + deleteMoneyRequest, + deleteTrackExpense, + getNavigationUrlAfterTrackExpenseDelete, + getNavigationUrlOnMoneyRequestDelete, + unapproveExpenseReport, +} from '@libs/actions/IOU'; +import {checkIfActionIsAllowed} from '@libs/actions/Session'; +import {canActionTask as canActionTaskActions, canModifyTask as canModifyTaskActions, deleteTask, reopenTask} from '@libs/actions/Task'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; +import {getConnectedIntegration, isPolicyAdmin, isPolicyEmployee, isSubmitAndClose, shouldShowPolicy} from '@libs/PolicyUtils'; +import { + getOneTransactionThreadReportID, + getOriginalMessage, + getReportAction, + getTrackExpenseActionableWhisper, + isDeletedAction, + isMoneyRequestAction, + isTrackExpenseAction, +} from '@libs/ReportActionsUtils'; +import { + canDeleteTransaction, + canEditReportDescription as canEditReportDescriptionReportUtils, + canHoldUnholdReportAction as canHoldUnholdReportActionReportUtils, + canJoinChat, + canLeaveChat, + canWriteInReport, + createDraftTransactionAndNavigateToParticipantSelector, + getAvailableReportFields, + getChatRoomSubtitle, + getDisplayNamesWithTooltips, + getIcons, + getOriginalReportID, + getParentNavigationSubtitle, + getParticipantsAccountIDsForDisplay, + getParticipantsList, + getReportDescription, + getReportFieldKey, + getReportName, + isAdminOwnerApproverOrReportOwner, + isArchivedNonExpenseReport as isArchivedNonExpenseReportReportUtils, + isCanceledTaskReport as isCanceledTaskReportReportUtils, + isChatRoom as isChatRoomReportUtils, + isChatThread as isChatThreadReportUtils, + isClosedReport, + isCompletedTaskReport, + isConciergeChatReport, + isDefaultRoom as isDefaultRoomReportUtils, + isExpenseReport as isExpenseReportReportUtils, + isExported, + isGroupChat as isGroupChatReportUtils, + isHiddenForCurrentUser, + isInvoiceReport as isInvoiceReportReportUtils, + isInvoiceRoom as isInvoiceRoomReportUtils, + isMoneyRequestReport as isMoneyRequestReportReportUtils, + isMoneyRequest as isMoneyRequestReportUtils, + isPayer as isPayerReportUtils, + isPolicyExpenseChat as isPolicyExpenseChatReportUtils, + isPublicRoom, + isReportApproved, + isReportFieldDisabled, + isReportFieldOfTypeTitle, + isReportManager, + isRootGroupChat as isRootGroupChatReportUtils, + isSelfDM as isSelfDMReportUtils, + isSettled as isSettledReportUtils, + isSystemChat as isSystemChatReportUtils, + isTaskReport as isTaskReportReportUtils, + isThread as isThreadReportUtils, + isTrackExpenseReport as isTrackExpenseReportReportUtils, + isUserCreatedPolicyRoom as isUserCreatedPolicyRoomReportUtils, + navigateBackOnDeleteTransaction, + navigateToPrivateNotes, + shouldDisableRename as shouldDisableRenameReportUtils, + shouldUseFullTitleToDisplay as shouldUseFullTitleToDisplayReportUtils, +} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import {getAllReportTransactions} from '@libs/TransactionUtils'; -import * as IOU from '@userActions/IOU'; -import * as Report from '@userActions/Report'; -import * as Session from '@userActions/Session'; -import * as Task from '@userActions/Task'; +import { + clearAvatarErrors, + clearPolicyRoomNameErrors, + clearReportFieldKeyErrors, + exportReportToCSV, + getReportPrivateNote, + hasErrorInPrivateNotes, + leaveGroupChat, + leaveRoom, + setDeleteTransactionNavigateBackUrl, + updateGroupChatAvatar, +} from '@userActions/Report'; import ConfirmModal from '@src/components/ConfirmModal'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -99,10 +179,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); - const transactionThreadReportID = useMemo( - () => ReportActionsUtils.getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline), - [report.reportID, reportActions, isOffline], - ); + const transactionThreadReportID = useMemo(() => getOneTransactionThreadReportID(report.reportID, reportActions ?? [], isOffline), [report.reportID, reportActions, isOffline]); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); const [isDebugModeEnabled] = useOnyx(ONYXKEYS.USER, {selector: (user) => !!user?.isDebugModeEnabled}); @@ -117,34 +194,34 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta const [offlineModalVisible, setOfflineModalVisible] = useState(false); const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`], [policies, report?.policyID]); - const isPolicyAdmin = useMemo(() => PolicyUtils.isPolicyAdmin(policy), [policy]); - const isPolicyEmployee = useMemo(() => PolicyUtils.isPolicyEmployee(report?.policyID, policies), [report?.policyID, policies]); - const isPolicyExpenseChat = useMemo(() => ReportUtils.isPolicyExpenseChat(report), [report]); - const shouldUseFullTitle = useMemo(() => ReportUtils.shouldUseFullTitleToDisplay(report), [report]); - const isChatRoom = useMemo(() => ReportUtils.isChatRoom(report), [report]); - const isUserCreatedPolicyRoom = useMemo(() => ReportUtils.isUserCreatedPolicyRoom(report), [report]); - const isDefaultRoom = useMemo(() => ReportUtils.isDefaultRoom(report), [report]); - const isChatThread = useMemo(() => ReportUtils.isChatThread(report), [report]); - const isArchivedRoom = useMemo(() => ReportUtils.isArchivedNonExpenseReport(report, reportNameValuePairs), [report, reportNameValuePairs]); - const isMoneyRequestReport = useMemo(() => ReportUtils.isMoneyRequestReport(report), [report]); - const isMoneyRequest = useMemo(() => ReportUtils.isMoneyRequest(report), [report]); - const isInvoiceReport = useMemo(() => ReportUtils.isInvoiceReport(report), [report]); - const isInvoiceRoom = useMemo(() => ReportUtils.isInvoiceRoom(report), [report]); - const isTaskReport = useMemo(() => ReportUtils.isTaskReport(report), [report]); - const isSelfDM = useMemo(() => ReportUtils.isSelfDM(report), [report]); - const isTrackExpenseReport = ReportUtils.isTrackExpenseReport(report); - const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID, report?.parentReportActionID); - const isCanceledTaskReport = ReportUtils.isCanceledTaskReport(report, parentReportAction); - const canEditReportDescription = useMemo(() => ReportUtils.canEditReportDescription(report, policy), [report, policy]); + const isUserPolicyAdmin = useMemo(() => isPolicyAdmin(policy), [policy]); + const isUserPolicyEmployee = useMemo(() => isPolicyEmployee(report?.policyID, policies), [report?.policyID, policies]); + const isPolicyExpenseChat = useMemo(() => isPolicyExpenseChatReportUtils(report), [report]); + const shouldUseFullTitle = useMemo(() => shouldUseFullTitleToDisplayReportUtils(report), [report]); + const isChatRoom = useMemo(() => isChatRoomReportUtils(report), [report]); + const isUserCreatedPolicyRoom = useMemo(() => isUserCreatedPolicyRoomReportUtils(report), [report]); + const isDefaultRoom = useMemo(() => isDefaultRoomReportUtils(report), [report]); + const isChatThread = useMemo(() => isChatThreadReportUtils(report), [report]); + const isArchivedRoom = useMemo(() => isArchivedNonExpenseReportReportUtils(report, reportNameValuePairs), [report, reportNameValuePairs]); + const isMoneyRequestReport = useMemo(() => isMoneyRequestReportReportUtils(report), [report]); + const isMoneyRequest = useMemo(() => isMoneyRequestReportUtils(report), [report]); + const isInvoiceReport = useMemo(() => isInvoiceReportReportUtils(report), [report]); + const isInvoiceRoom = useMemo(() => isInvoiceRoomReportUtils(report), [report]); + const isTaskReport = useMemo(() => isTaskReportReportUtils(report), [report]); + const isSelfDM = useMemo(() => isSelfDMReportUtils(report), [report]); + const isTrackExpenseReport = isTrackExpenseReportReportUtils(report); + const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); + const isCanceledTaskReport = isCanceledTaskReportReportUtils(report, parentReportAction); + const canEditReportDescription = useMemo(() => canEditReportDescriptionReportUtils(report, policy), [report, policy]); const shouldShowReportDescription = isChatRoom && (canEditReportDescription || report.description !== ''); const isExpenseReport = isMoneyRequestReport || isInvoiceReport || isMoneyRequest; const isSingleTransactionView = isMoneyRequest || isTrackExpenseReport; - const isSelfDMTrackExpenseReport = isTrackExpenseReport && ReportUtils.isSelfDM(parentReport); - const shouldDisableRename = useMemo(() => ReportUtils.shouldDisableRename(report), [report]); - const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(report); + const isSelfDMTrackExpenseReport = isTrackExpenseReport && isSelfDMReportUtils(parentReport); + const shouldDisableRename = useMemo(() => shouldDisableRenameReportUtils(report), [report]); + const parentNavigationSubtitleData = getParentNavigationSubtitle(report); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- policy is a dependency because `getChatRoomSubtitle` calls `getPolicyName` which in turn retrieves the value from the `policy` value stored in Onyx const chatRoomSubtitle = useMemo(() => { - const subtitle = ReportUtils.getChatRoomSubtitle(report); + const subtitle = getChatRoomSubtitle(report); if (subtitle) { return subtitle; @@ -152,15 +229,15 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta return ''; }, [report]); - const isSystemChat = useMemo(() => ReportUtils.isSystemChat(report), [report]); - const isGroupChat = useMemo(() => ReportUtils.isGroupChat(report), [report]); - const isRootGroupChat = useMemo(() => ReportUtils.isRootGroupChat(report), [report]); - const isThread = useMemo(() => ReportUtils.isThread(report), [report]); - const shouldOpenRoomMembersPage = isUserCreatedPolicyRoom || isChatThread || (isPolicyExpenseChat && isPolicyAdmin); + const isSystemChat = useMemo(() => isSystemChatReportUtils(report), [report]); + const isGroupChat = useMemo(() => isGroupChatReportUtils(report), [report]); + const isRootGroupChat = useMemo(() => isRootGroupChatReportUtils(report), [report]); + const isThread = useMemo(() => isThreadReportUtils(report), [report]); + const shouldOpenRoomMembersPage = isUserCreatedPolicyRoom || isChatThread || (isPolicyExpenseChat && isUserPolicyAdmin); const participants = useMemo(() => { - return ReportUtils.getParticipantsList(report, personalDetails, shouldOpenRoomMembersPage); + return getParticipantsList(report, personalDetails, shouldOpenRoomMembersPage); }, [report, personalDetails, shouldOpenRoomMembersPage]); - const connectedIntegration = PolicyUtils.getConnectedIntegration(policy); + const connectedIntegration = getConnectedIntegration(policy); const transactionIDList = useMemo(() => { if (!isMoneyRequestReport) { @@ -206,7 +283,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta const isActionOwner = typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID; - const isDeletedParentAction = ReportActionsUtils.isDeletedAction(requestParentReportAction); + const isDeletedParentAction = isDeletedAction(requestParentReportAction); const moneyRequestReport: OnyxEntry = useMemo(() => { if (caseID === CASES.MONEY_REQUEST) { @@ -217,21 +294,14 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta const moneyRequestAction = transactionThreadReportID ? requestParentReportAction : parentReportAction; - const canModifyTask = Task.canModifyTask(report, session?.accountID ?? CONST.DEFAULT_NUMBER_ID); - const canActionTask = Task.canActionTask(report, session?.accountID ?? CONST.DEFAULT_NUMBER_ID); + const canModifyTask = canModifyTaskActions(report, session?.accountID ?? CONST.DEFAULT_NUMBER_ID); + const canActionTask = canActionTaskActions(report, session?.accountID ?? CONST.DEFAULT_NUMBER_ID); const shouldShowTaskDeleteButton = - isTaskReport && - !isCanceledTaskReport && - ReportUtils.canWriteInReport(report) && - report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && - !ReportUtils.isClosedReport(report) && - canModifyTask && - canActionTask; - const canDeleteRequest = isActionOwner && (ReportUtils.canDeleteTransaction(moneyRequestReport) || isSelfDMTrackExpenseReport) && !isDeletedParentAction; + isTaskReport && !isCanceledTaskReport && canWriteInReport(report) && report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && !isClosedReport(report) && canModifyTask && canActionTask; + const canDeleteRequest = isActionOwner && (canDeleteTransaction(moneyRequestReport) || isSelfDMTrackExpenseReport) && !isDeletedParentAction; const shouldShowDeleteButton = shouldShowTaskDeleteButton || canDeleteRequest; - const canUnapproveRequest = - ReportUtils.isExpenseReport(report) && (ReportUtils.isReportManager(report) || isPolicyAdmin) && ReportUtils.isReportApproved(report) && !PolicyUtils.isSubmitAndClose(policy); + const canUnapproveRequest = isExpenseReportReportUtils(report) && (isReportManager(report) || isUserPolicyAdmin) && isReportApproved(report) && !isSubmitAndClose(policy); useEffect(() => { if (canDeleteRequest) { @@ -247,23 +317,23 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta return; } - Report.getReportPrivateNote(report?.reportID); + getReportPrivateNote(report?.reportID); }, [report?.reportID, isOffline, isPrivateNotesFetchTriggered, isSelfDM]); const leaveChat = useCallback(() => { Navigation.dismissModal(); Navigation.isNavigationReady().then(() => { if (isRootGroupChat) { - Report.leaveGroupChat(report.reportID); + leaveGroupChat(report.reportID); return; } - const isWorkspaceMemberLeavingWorkspaceRoom = (report.visibility === CONST.REPORT.VISIBILITY.RESTRICTED || isPolicyExpenseChat) && isPolicyEmployee; - Report.leaveRoom(report.reportID, isWorkspaceMemberLeavingWorkspaceRoom); + const isWorkspaceMemberLeavingWorkspaceRoom = (report.visibility === CONST.REPORT.VISIBILITY.RESTRICTED || isPolicyExpenseChat) && isUserPolicyEmployee; + leaveRoom(report.reportID, isWorkspaceMemberLeavingWorkspaceRoom); }); - }, [isPolicyEmployee, isPolicyExpenseChat, isRootGroupChat, report.reportID, report.visibility]); + }, [isUserPolicyEmployee, isPolicyExpenseChat, isRootGroupChat, report.reportID, report.visibility]); const [moneyRequestReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`); - const isMoneyRequestExported = ReportUtils.isExported(moneyRequestReportActions); + const isMoneyRequestExported = isExported(moneyRequestReportActions); const {isDelegateAccessRestricted} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); @@ -275,16 +345,16 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta return; } Navigation.dismissModal(); - IOU.unapproveExpenseReport(moneyRequestReport); + unapproveExpenseReport(moneyRequestReport); }, [isMoneyRequestExported, moneyRequestReport, isDelegateAccessRestricted]); - const shouldShowLeaveButton = ReportUtils.canLeaveChat(report, policy); - const shouldShowGoToWorkspace = PolicyUtils.shouldShowPolicy(policy, false, session?.email) && !policy?.isJoinRequestPending; + const shouldShowLeaveButton = canLeaveChat(report, policy); + const shouldShowGoToWorkspace = shouldShowPolicy(policy, false, session?.email) && !policy?.isJoinRequestPending; - const reportName = ReportUtils.getReportName(report); + const reportName = getReportName(report); const additionalRoomDetails = - (isPolicyExpenseChat && !!report?.isOwnPolicyExpenseChat) || ReportUtils.isExpenseReport(report) || isPolicyExpenseChat || isInvoiceRoom + (isPolicyExpenseChat && !!report?.isOwnPolicyExpenseChat) || isExpenseReportReportUtils(report) || isPolicyExpenseChat || isInvoiceRoom ? chatRoomSubtitle : `${translate('threads.in')} ${chatRoomSubtitle}`; @@ -297,24 +367,24 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta roomDescription = translate('newRoomPage.roomName'); } - const shouldShowNotificationPref = !isMoneyRequestReport && !ReportUtils.isHiddenForCurrentUser(report); + const shouldShowNotificationPref = !isMoneyRequestReport && !isHiddenForCurrentUser(report); const shouldShowWriteCapability = !isMoneyRequestReport; const shouldShowMenuItem = shouldShowNotificationPref || shouldShowWriteCapability || (!!report?.visibility && report.chatType !== CONST.REPORT.CHAT_TYPE.INVOICE); - const isPayer = ReportUtils.isPayer(session, moneyRequestReport); - const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); + const isPayer = isPayerReportUtils(session, moneyRequestReport); + const isSettled = isSettledReportUtils(moneyRequestReport?.reportID); - const shouldShowCancelPaymentButton = caseID === CASES.MONEY_REPORT && isPayer && isSettled && ReportUtils.isExpenseReport(moneyRequestReport); + const shouldShowCancelPaymentButton = caseID === CASES.MONEY_REPORT && isPayer && isSettled && isExpenseReportReportUtils(moneyRequestReport); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`); - const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID : ''; + const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : ''; const cancelPayment = useCallback(() => { if (!chatReport) { return; } - IOU.cancelPayment(moneyRequestReport, chatReport); + cancelPaymentIOU(moneyRequestReport, chatReport); setIsConfirmModalVisible(false); }, [moneyRequestReport, chatReport]); @@ -335,10 +405,10 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta // - The report is a user created room and the room and the current user is a workspace member i.e. non-workspace members should not see this option. if ( (isGroupChat || - (isDefaultRoom && isChatThread && isPolicyEmployee) || + (isDefaultRoom && isChatThread && isUserPolicyEmployee) || (!isUserCreatedPolicyRoom && participants.length) || - (isUserCreatedPolicyRoom && (isPolicyEmployee || (isChatThread && !ReportUtils.isPublicRoom(report))))) && - !ReportUtils.isConciergeChatReport(report) && + (isUserCreatedPolicyRoom && (isUserPolicyEmployee || (isChatThread && !isPublicRoom(report))))) && + !isConciergeChatReport(report) && !isSystemChat ) { items.push({ @@ -356,7 +426,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta } }, }); - } else if ((isUserCreatedPolicyRoom && (!participants.length || !isPolicyEmployee)) || ((isDefaultRoom || isPolicyExpenseChat) && isChatThread && !isPolicyEmployee)) { + } else if ((isUserCreatedPolicyRoom && (!participants.length || !isUserPolicyEmployee)) || ((isDefaultRoom || isPolicyExpenseChat) && isChatThread && !isUserPolicyEmployee)) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.INVITE, translationKey: 'common.invite', @@ -383,8 +453,8 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta } if (isTrackExpenseReport && !isDeletedParentAction) { - const actionReportID = ReportUtils.getOriginalReportID(report.reportID, parentReportAction); - const whisperAction = ReportActionsUtils.getTrackExpenseActionableWhisper(iouTransactionID, moneyRequestReport?.reportID); + const actionReportID = getOriginalReportID(report.reportID, parentReportAction); + const whisperAction = getTrackExpenseActionableWhisper(iouTransactionID, moneyRequestReport?.reportID); const actionableWhisperReportActionID = whisperAction?.reportActionID; items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.SETTINGS, @@ -393,7 +463,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta isAnonymousAction: false, shouldShowRightIcon: true, action: () => { - ReportUtils.createDraftTransactionAndNavigateToParticipantSelector(iouTransactionID, actionReportID, CONST.IOU.ACTION.SUBMIT, actionableWhisperReportActionID); + createDraftTransactionAndNavigateToParticipantSelector(iouTransactionID, actionReportID, CONST.IOU.ACTION.SUBMIT, actionableWhisperReportActionID); }, }); items.push({ @@ -403,7 +473,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta isAnonymousAction: false, shouldShowRightIcon: true, action: () => { - ReportUtils.createDraftTransactionAndNavigateToParticipantSelector(iouTransactionID, actionReportID, CONST.IOU.ACTION.CATEGORIZE, actionableWhisperReportActionID); + createDraftTransactionAndNavigateToParticipantSelector(iouTransactionID, actionReportID, CONST.IOU.ACTION.CATEGORIZE, actionableWhisperReportActionID); }, }); items.push({ @@ -413,7 +483,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta isAnonymousAction: false, shouldShowRightIcon: true, action: () => { - ReportUtils.createDraftTransactionAndNavigateToParticipantSelector(iouTransactionID, actionReportID, CONST.IOU.ACTION.SHARE, actionableWhisperReportActionID); + createDraftTransactionAndNavigateToParticipantSelector(iouTransactionID, actionReportID, CONST.IOU.ACTION.SHARE, actionableWhisperReportActionID); }, }); } @@ -426,22 +496,22 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta icon: Expensicons.Pencil, isAnonymousAction: false, shouldShowRightIcon: true, - action: () => ReportUtils.navigateToPrivateNotes(report, session, backTo), - brickRoadIndicator: Report.hasErrorInPrivateNotes(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + action: () => navigateToPrivateNotes(report, session, backTo), + brickRoadIndicator: hasErrorInPrivateNotes(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } // Show actions related to Task Reports if (isTaskReport && !isCanceledTaskReport) { - if (ReportUtils.isCompletedTaskReport(report) && canModifyTask && canActionTask) { + if (isCompletedTaskReport(report) && canModifyTask && canActionTask) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.MARK_AS_INCOMPLETE, icon: Expensicons.Checkmark, translationKey: 'task.markAsIncomplete', isAnonymousAction: false, - action: Session.checkIfActionIsAllowed(() => { + action: checkIfActionIsAllowed(() => { Navigation.dismissModal(); - Task.reopenTask(report); + reopenTask(report); }), }); } @@ -469,14 +539,14 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta return; } - ReportActions.exportReportToCSV({reportID: report.reportID, transactionIDList}, () => { + exportReportToCSV({reportID: report.reportID, transactionIDList}, () => { setDownloadErrorModalVisible(true); }); }, }); } - if (policy && connectedIntegration && isPolicyAdmin && !isSingleTransactionView && isExpenseReport) { + if (policy && connectedIntegration && isUserPolicyAdmin && !isSingleTransactionView && isExpenseReport) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.EXPORT, translationKey: 'common.export', @@ -525,7 +595,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta icon: Expensicons.Exit, isAnonymousAction: true, action: () => { - if (ReportUtils.getParticipantsAccountIDsForDisplay(report, false, true).length === 1 && isRootGroupChat) { + if (getParticipantsAccountIDsForDisplay(report, false, true).length === 1 && isRootGroupChat) { setIsLastMemberLeavingGroupModalVisible(true); return; } @@ -553,7 +623,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta isGroupChat, isDefaultRoom, isChatThread, - isPolicyEmployee, + isUserPolicyEmployee, isUserCreatedPolicyRoom, participants.length, report, @@ -570,7 +640,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta shouldShowLeaveButton, policy, connectedIntegration, - isPolicyAdmin, + isUserPolicyAdmin, isSingleTransactionView, isExpenseReport, canUnapproveRequest, @@ -595,10 +665,10 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta const displayNamesWithTooltips = useMemo(() => { const hasMultipleParticipants = participants.length > 1; - return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails), hasMultipleParticipants); + return getDisplayNamesWithTooltips(getPersonalDetailsForAccountIDs(participants, personalDetails), hasMultipleParticipants); }, [participants, personalDetails]); - const icons = useMemo(() => ReportUtils.getIcons(report, personalDetails, null, '', -1, policy), [report, personalDetails, policy]); + const icons = useMemo(() => getIcons(report, personalDetails, null, '', -1, policy), [report, personalDetails, policy]); const chatRoomSubtitleText = chatRoomSubtitle ? ( Navigation.navigate(ROUTES.REPORT_AVATAR.getRoute(report.reportID))} onImageRemoved={() => { // Calling this without a file will remove the avatar - Report.updateGroupChatAvatar(report.reportID); + updateGroupChatAvatar(report.reportID); }} - onImageSelected={(file) => Report.updateGroupChatAvatar(report.reportID, file)} + onImageSelected={(file) => updateGroupChatAvatar(report.reportID, file)} editIcon={Expensicons.Camera} editIconStyle={styles.smallEditIconAccount} pendingAction={report.pendingFields?.avatar ?? undefined} errors={report.errorFields?.avatar ?? null} errorRowStyles={styles.mt6} - onErrorClose={() => Report.clearAvatarErrors(report.reportID)} + onErrorClose={() => clearAvatarErrors(report.reportID)} shouldUseStyleUtilityForAnchorPosition style={[styles.w100, styles.mb3]} /> @@ -664,12 +734,12 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta ); }, [report, icons, isMoneyRequestReport, isInvoiceReport, isGroupChat, isThread, styles]); - const canHoldUnholdReportAction = ReportUtils.canHoldUnholdReportAction(moneyRequestAction); + const canHoldUnholdReportAction = canHoldUnholdReportActionReportUtils(moneyRequestAction); const shouldShowHoldAction = caseID !== CASES.DEFAULT && (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) && - !ReportUtils.isArchivedNonExpenseReport(transactionThreadReportID ? report : parentReport, parentReportNameValuePairs); - const canJoin = ReportUtils.canJoinChat(report, parentReportAction, policy); + !isArchivedNonExpenseReportReportUtils(transactionThreadReportID ? report : parentReport, parentReportNameValuePairs); + const canJoin = canJoinChat(report, parentReportAction, policy); const promotedActions = useMemo(() => { const result: PromotedAction[] = []; @@ -725,7 +795,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta shouldUseFullTitle={shouldUseFullTitle} /> - {isPolicyAdmin ? ( + {isUserPolicyAdmin ? ( Report.clearPolicyRoomNameErrors(report?.reportID)} + onClose={() => clearPolicyRoomNameErrors(report?.reportID)} > ((): OnyxTypes.PolicyReportField | undefined => { - const fields = ReportUtils.getAvailableReportFields(report, Object.values(policy?.fieldList ?? {})); - return fields.find((reportField) => ReportUtils.isReportFieldOfTypeTitle(reportField)); + const fields = getAvailableReportFields(report, Object.values(policy?.fieldList ?? {})); + return fields.find((reportField) => isReportFieldOfTypeTitle(reportField)); }, [report, policy?.fieldList]); - const fieldKey = ReportUtils.getReportFieldKey(titleField?.fieldID); - const isFieldDisabled = ReportUtils.isReportFieldDisabled(report, titleField, policy); + const fieldKey = getReportFieldKey(titleField?.fieldID); + const isFieldDisabled = isReportFieldDisabled(report, titleField, policy); - const shouldShowTitleField = caseID !== CASES.MONEY_REQUEST && !isFieldDisabled && ReportUtils.isAdminOwnerApproverOrReportOwner(report, policy); + const shouldShowTitleField = caseID !== CASES.MONEY_REQUEST && !isFieldDisabled && isAdminOwnerApproverOrReportOwner(report, policy); const nameSectionFurtherDetailsContent = ( { if (report.errorFields?.reportName) { - Report.clearPolicyRoomNameErrors(report.reportID); + clearPolicyRoomNameErrors(report.reportID); } - Report.clearReportFieldKeyErrors(report.reportID, fieldKey); + clearReportFieldKeyErrors(report.reportID, fieldKey); }} > @@ -841,7 +911,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta const deleteTransaction = useCallback(() => { if (caseID === CASES.DEFAULT) { - Task.deleteTask(report); + deleteTask(report); return; } @@ -849,12 +919,12 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta return; } - const isTrackExpense = ReportActionsUtils.isTrackExpenseAction(requestParentReportAction); + const isTrackExpense = isTrackExpenseAction(requestParentReportAction); if (isTrackExpense) { - IOU.deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, isSingleTransactionView); + deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, isSingleTransactionView); } else { - IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); + deleteMoneyRequest(iouTransactionID, requestParentReportAction, isSingleTransactionView); } }, [caseID, iouTransactionID, isSingleTransactionView, moneyRequestReport?.reportID, report, requestParentReportAction]); @@ -884,21 +954,21 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta // Only proceed with navigation logic if transaction was actually deleted if (!isEmptyObject(requestParentReportAction)) { - const isTrackExpense = ReportActionsUtils.isTrackExpenseAction(requestParentReportAction); + const isTrackExpense = isTrackExpenseAction(requestParentReportAction); if (isTrackExpense) { - urlToNavigateBack = IOU.getNavigationUrlAfterTrackExpenseDelete(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, isSingleTransactionView); + urlToNavigateBack = getNavigationUrlAfterTrackExpenseDelete(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, isSingleTransactionView); } else { - urlToNavigateBack = IOU.getNavigationUrlOnMoneyRequestDelete(iouTransactionID, requestParentReportAction, isSingleTransactionView); + urlToNavigateBack = getNavigationUrlOnMoneyRequestDelete(iouTransactionID, requestParentReportAction, isSingleTransactionView); } } if (!urlToNavigateBack) { Navigation.dismissModal(); } else { - Report.setDeleteTransactionNavigateBackUrl(urlToNavigateBack); - ReportUtils.navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true, report.reportID); + setDeleteTransactionNavigateBackUrl(urlToNavigateBack); + navigateBackOnDeleteTransaction(urlToNavigateBack as Route, true, report.reportID); } - }, [iouTransactionID, requestParentReportAction, isSingleTransactionView, isTransactionDeleted, moneyRequestReport?.reportID]); + }, [iouTransactionID, requestParentReportAction, isSingleTransactionView, isTransactionDeleted, moneyRequestReport?.reportID, report.reportID]); const mentionReportContextValue = useMemo(() => ({currentReportID: report.reportID, exactlyMatch: true}), [report.reportID]); @@ -925,7 +995,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta { setIsUnapproveModalVisible(false); Navigation.dismissModal(); - IOU.unapproveExpenseReport(moneyRequestReport); + unapproveExpenseReport(moneyRequestReport); }} cancelText={translate('common.cancel')} onCancel={() => setIsUnapproveModalVisible(false)} diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 0b616a6d18e5..4b488b0ff4b8 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -28,19 +28,59 @@ import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; +import {setShouldShowComposeInput} from '@libs/actions/Composer'; +import { + clearDeleteTransactionNavigateBackUrl, + navigateToConciergeChat, + openReport, + readNewestAction, + subscribeToReportLeavingEvents, + unsubscribeFromLeavingRoomReportChannel, + updateLastVisitTime, +} from '@libs/actions/Report'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import clearReportNotifications from '@libs/Notification/clearReportNotifications'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; +import {getDisplayNameOrDefault, isPersonalDetailsEmpty} from '@libs/PersonalDetailsUtils'; +import { + getCombinedReportActions, + getOneTransactionThreadReportID, + isActionOfType, + isCreatedAction, + isDeletedAction, + isDeletedParentAction as isDeletedParentActionReportActionUtils, + isMoneyRequestAction, + isWhisperAction, + shouldReportActionBeVisible, +} from '@libs/ReportActionsUtils'; +import { + canAccessReport, + canEditReportAction, + canUserPerformWriteAction, + findLastAccessedReport, + getParticipantsAccountIDsForDisplay, + getReportIDFromLink, + getReportOfflinePendingActionAndErrors, + isChatThread, + isConciergeChatReport, + isExpenseReport, + isGroupChat, + isHiddenForCurrentUser, + isInvoiceReport, + isMoneyRequest, + isMoneyRequestReport, + isMoneyRequestReportPendingDeletion, + isOneTransactionThread, + isPolicyExpenseChat, + isTaskReport, + isTrackExpenseReport, + isValidReportIDFromPath, +} from '@libs/ReportUtils'; import shouldFetchReport from '@libs/shouldFetchReport'; -import * as ValidationUtils from '@libs/ValidationUtils'; +import {isNumeric} from '@libs/ValidationUtils'; import type {AuthScreensParamList} from '@navigation/types'; -import * as ComposerActions from '@userActions/Composer'; -import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -133,7 +173,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); const wasLoadingApp = usePrevious(isLoadingApp); const finishedLoadingApp = wasLoadingApp && !isLoadingApp; - const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(parentReportAction); + const isDeletedParentAction = isDeletedParentActionReportActionUtils(parentReportAction); const prevIsDeletedParentAction = usePrevious(isDeletedParentAction); const isLoadingReportOnyx = isLoadingOnyxValue(reportResult); @@ -143,14 +183,14 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // Don't update if there is a reportID in the params already if (route.params.reportID) { const reportActionID = route?.params?.reportActionID; - const isValidReportActionID = ValidationUtils.isNumeric(reportActionID); + const isValidReportActionID = isNumeric(reportActionID); if (reportActionID && !isValidReportActionID) { navigation.setParams({reportActionID: ''}); } return; } - const lastAccessedReportID = ReportUtils.findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID; + const lastAccessedReportID = findLastAccessedReport(!canUseDefaultRooms, !!route.params.openOnAdminRoom, activeWorkspaceID)?.reportID; // It's possible that reports aren't fully loaded yet // in that case the reportID is undefined @@ -165,10 +205,10 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const chatWithAccountManagerText = useMemo(() => { if (accountManagerReportID) { - const participants = ReportUtils.getParticipantsAccountIDsForDisplay(accountManagerReport, false, true); - const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs([participants?.at(0) ?? -1], personalDetails); + const participants = getParticipantsAccountIDsForDisplay(accountManagerReport, false, true); + const participantPersonalDetails = getPersonalDetailsForAccountIDs([participants?.at(0) ?? -1], personalDetails); const participantPersonalDetail = Object.values(participantPersonalDetails).at(0); - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(participantPersonalDetail); + const displayName = getDisplayNameOrDefault(participantPersonalDetail); const login = participantPersonalDetail?.login; if (displayName && login) { return translate('common.chatWithAccountManager', {accountManagerDisplayName: `${displayName} (${login})`}); @@ -250,7 +290,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const shouldAdjustScrollView = useMemo(() => isComposerFocus && !modal?.willAlertModalBecomeVisible, [isComposerFocus, modal]); const viewportOffsetTop = useViewportOffsetTop(shouldAdjustScrollView); - const {reportPendingAction, reportErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); + const {reportPendingAction, reportErrors} = getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle: ViewStyle[] = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; const indexOfLinkedMessage = useMemo( @@ -259,7 +299,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { ); const isPendingActionExist = !!reportActions.at(0)?.pendingAction; - const doesCreatedActionExists = useCallback(() => !!sortedAllReportActions?.findLast((action) => ReportActionsUtils.isCreatedAction(action)), [sortedAllReportActions]); + const doesCreatedActionExists = useCallback(() => !!sortedAllReportActions?.findLast((action) => isCreatedAction(action)), [sortedAllReportActions]); const isLinkedMessageAvailable = useMemo(() => indexOfLinkedMessage > -1, [indexOfLinkedMessage]); // The linked report actions should have at least 15 messages (counting as 1 page) above them to fill the screen. @@ -269,13 +309,13 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // If there's a non-404 error for the report we should show it instead of blocking the screen const hasHelpfulErrors = Object.keys(report?.errorFields ?? {}).some((key) => key !== 'notFound'); - const shouldHideReport = !hasHelpfulErrors && !ReportUtils.canAccessReport(report, policies, betas); + const shouldHideReport = !hasHelpfulErrors && !canAccessReport(report, policies, betas); - const transactionThreadReportID = ReportActionsUtils.getOneTransactionThreadReportID(reportID ?? '', reportActions ?? [], isOffline); + const transactionThreadReportID = getOneTransactionThreadReportID(reportID ?? '', reportActions ?? [], isOffline); const [transactionThreadReportActions = {}] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`); - const combinedReportActions = ReportActionsUtils.getCombinedReportActions(reportActions, transactionThreadReportID ?? null, Object.values(transactionThreadReportActions)); - const lastReportAction = [...combinedReportActions, parentReportAction].find((action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)); - const isSingleTransactionView = ReportUtils.isMoneyRequest(report) || ReportUtils.isTrackExpenseReport(report); + const combinedReportActions = getCombinedReportActions(reportActions, transactionThreadReportID ?? null, Object.values(transactionThreadReportActions)); + const lastReportAction = [...combinedReportActions, parentReportAction].find((action) => canEditReportAction(action) && !isMoneyRequestAction(action)); + const isSingleTransactionView = isMoneyRequest(report) || isTrackExpenseReport(report); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID ?? '-1'}`]; const isTopMostReportId = currentReportIDValue?.currentReportID === reportIDFromRoute; const didSubscribeToReportLeavingEvents = useRef(false); @@ -319,13 +359,13 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } useEffect(() => { - if (!transactionThreadReportID || !route?.params?.reportActionID || !ReportUtils.isOneTransactionThread(linkedAction?.childReportID ?? '-1', reportID ?? '', linkedAction)) { + if (!transactionThreadReportID || !route?.params?.reportActionID || !isOneTransactionThread(linkedAction?.childReportID ?? '-1', reportID ?? '', linkedAction)) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(route?.params?.reportID)); }, [transactionThreadReportID, route?.params?.reportActionID, route?.params?.reportID, linkedAction, reportID]); - if (ReportUtils.isMoneyRequestReport(report) || ReportUtils.isInvoiceReport(report)) { + if (isMoneyRequestReport(report) || isInvoiceReport(report)) { headerView = ( = CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || isPendingActionExist || (doesCreatedActionExists() && reportActions.length > 0); const isLinkedActionDeleted = useMemo( - () => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, ReportUtils.canUserPerformWriteAction(report)), + () => !!linkedAction && !shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, canUserPerformWriteAction(report)), [linkedAction, report], ); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); const isLinkedActionInaccessibleWhisper = useMemo( - () => !!linkedAction && ReportActionsUtils.isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), + () => !!linkedAction && isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), [currentUserAccountID, linkedAction], ); const [deleteTransactionNavigateBackUrl] = useOnyx(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL); @@ -368,12 +408,12 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // Clear the URL after all interactions are processed to ensure all updates are completed before hiding the skeleton InteractionManager.runAfterInteractions(() => { requestAnimationFrame(() => { - Report.clearDeleteTransactionNavigateBackUrl(); + clearDeleteTransactionNavigateBackUrl(); }); }); }, [isFocused, deleteTransactionNavigateBackUrl]); - const isLoading = isLoadingApp ?? (!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || PersonalDetailsUtils.isPersonalDetailsEmpty()); + const isLoading = isLoadingApp ?? (!reportIDFromRoute || (!isSidebarLoaded && !isInNarrowPaneModal) || isPersonalDetailsEmpty()); const shouldShowSkeleton = (isLinkingToMessage && !isLinkedMessagePageReady) || @@ -382,7 +422,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { isLoadingReportOnyx || !isCurrentReportLoadedFromOnyx || // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (deleteTransactionNavigateBackUrl && ReportUtils.getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID) || + (deleteTransactionNavigateBackUrl && getReportIDFromLink(deleteTransactionNavigateBackUrl) === report?.reportID) || (!reportMetadata.isOptimisticReport && isLoading); const isLinkedActionBecomesDeleted = prevIsLinkedActionDeleted !== undefined && !prevIsLinkedActionDeleted && isLinkedActionDeleted; @@ -426,7 +466,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { if (shouldHideReport) { return true; } - return !!currentReportIDFormRoute && !ReportUtils.isValidReportIDFromPath(currentReportIDFormRoute); + return !!currentReportIDFormRoute && !isValidReportIDFromPath(currentReportIDFormRoute); }, [ shouldShowNotFoundLinkedAction, isLoadingApp, @@ -440,14 +480,14 @@ function ReportScreen({route, navigation}: ReportScreenProps) { ]); const fetchReport = useCallback(() => { - Report.openReport(reportIDFromRoute, reportActionIDFromRoute); + openReport(reportIDFromRoute, reportActionIDFromRoute); }, [reportIDFromRoute, reportActionIDFromRoute]); useEffect(() => { if (!reportID || !isFocused) { return; } - Report.updateLastVisitTime(reportID); + updateLastVisitTime(reportID); }, [reportID, isFocused]); useEffect(() => { @@ -466,7 +506,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const fetchReportIfNeeded = useCallback(() => { // Report ID will be empty when the reports collection is empty. // This could happen when we are loading the collection for the first time after logging in. - if (!ReportUtils.isValidReportIDFromPath(reportIDFromRoute)) { + if (!isValidReportIDFromPath(reportIDFromRoute)) { return; } @@ -518,7 +558,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { useEffect(() => { const interactionTask = InteractionManager.runAfterInteractions(() => { - ComposerActions.setShouldShowComposeInput(true); + setShouldShowComposeInput(true); }); return () => { interactionTask.cancel(); @@ -526,7 +566,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { return; } - Report.unsubscribeFromLeavingRoomReportChannel(reportID ?? ''); + unsubscribeFromLeavingRoomReportChannel(reportID ?? ''); }; // I'm disabling the warning, as it expects to use exhaustive deps, even though we want this useEffect to run only on the first render. @@ -556,10 +596,10 @@ function ReportScreen({route, navigation}: ReportScreenProps) { // If a user has chosen to leave a thread, and then returns to it (e.g. with the back button), we need to call `openReport` again in order to allow the user to rejoin and to receive real-time updates useEffect(() => { - if (!shouldUseNarrowLayout || !isFocused || prevIsFocused || !ReportUtils.isChatThread(report) || !ReportUtils.isHiddenForCurrentUser(report) || isSingleTransactionView) { + if (!shouldUseNarrowLayout || !isFocused || prevIsFocused || !isChatThread(report) || !isHiddenForCurrentUser(report) || isSingleTransactionView) { return; } - Report.openReport(reportID ?? ''); + openReport(reportID ?? ''); // We don't want to run this useEffect every time `report` is changed // Excluding shouldUseNarrowLayout from the dependency list to prevent re-triggering on screen resize events. @@ -577,8 +617,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const prevOnyxReportID = prevReport?.reportID; const wasReportRemoved = !!prevOnyxReportID && prevOnyxReportID === reportIDFromRoute && !onyxReportID; const isRemovalExpectedForReportType = - isEmpty(report) && - (ReportUtils.isMoneyRequest(prevReport) || ReportUtils.isMoneyRequestReport(prevReport) || ReportUtils.isPolicyExpenseChat(prevReport) || ReportUtils.isGroupChat(prevReport)); + isEmpty(report) && (isMoneyRequest(prevReport) || isMoneyRequestReport(prevReport) || isPolicyExpenseChat(prevReport) || isGroupChat(prevReport)); const didReportClose = wasReportRemoved && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; const isTopLevelPolicyRoomWithNoStatus = !report?.statusNum && !prevReport?.parentReportID && prevReport?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_ROOM; const isClosedTopLevelPolicyRoom = wasReportRemoved && prevReport.statusNum === CONST.REPORT.STATUS_NUM.OPEN && isTopLevelPolicyRoomWithNoStatus; @@ -603,14 +642,14 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } if (prevReport?.parentReportID) { // Prevent navigation to the IOU/Expense Report if it is pending deletion. - if (ReportUtils.isMoneyRequestReportPendingDeletion(prevReport.parentReportID)) { + if (isMoneyRequestReportPendingDeletion(prevReport.parentReportID)) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(prevReport.parentReportID)); return; } - Report.navigateToConciergeChat(); + navigateToConciergeChat(); return; } @@ -623,7 +662,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { } fetchReportIfNeeded(); - ComposerActions.setShouldShowComposeInput(true); + setShouldShowComposeInput(true); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [ route, @@ -645,7 +684,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { ]); useEffect(() => { - if (!ReportUtils.isValidReportIDFromPath(reportIDFromRoute)) { + if (!isValidReportIDFromPath(reportIDFromRoute)) { return; } // Ensures the optimistic report is created successfully @@ -660,7 +699,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { let interactionTask: ReturnType | null = null; if (!didSubscribeToReportLeavingEvents.current && didCreateReportSuccessfully) { interactionTask = InteractionManager.runAfterInteractions(() => { - Report.subscribeToReportLeavingEvents(reportIDFromRoute); + subscribeToReportLeavingEvents(reportIDFromRoute); didSubscribeToReportLeavingEvents.current = true; }); } @@ -719,24 +758,24 @@ function ReportScreen({route, navigation}: ReportScreenProps) { }, [isLinkedActionInaccessibleWhisper]); useEffect(() => { - if (!!report?.lastReadTime || !ReportUtils.isTaskReport(report)) { + if (!!report?.lastReadTime || !isTaskReport(report)) { return; } // After creating the task report then navigating to task detail we don't have any report actions and the last read time is empty so We need to update the initial last read time when opening the task report detail. - Report.readNewestAction(report?.reportID ?? ''); + readNewestAction(report?.reportID ?? ''); }, [report]); const mostRecentReportAction = reportActions.at(0); const isMostRecentReportIOU = mostRecentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU; const isSingleIOUReportAction = reportActions.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU).length === 1; - const isSingleExpenseReport = ReportUtils.isExpenseReport(report) && isMostRecentReportIOU && isSingleIOUReportAction; - const isSingleInvoiceReport = ReportUtils.isInvoiceReport(report) && isMostRecentReportIOU && isSingleIOUReportAction; + const isSingleExpenseReport = isExpenseReport(report) && isMostRecentReportIOU && isSingleIOUReportAction; + const isSingleInvoiceReport = isInvoiceReport(report) && isMostRecentReportIOU && isSingleIOUReportAction; const shouldShowMostRecentReportAction = !!mostRecentReportAction && !isSingleExpenseReport && !isSingleInvoiceReport && - !ReportActionsUtils.isActionOfType(mostRecentReportAction, CONST.REPORT.ACTIONS.TYPE.CREATED) && - !ReportActionsUtils.isDeletedAction(mostRecentReportAction) && - (!deleteTransactionNavigateBackUrl || !ReportActionsUtils.isActionOfType(mostRecentReportAction, CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_TRACK_EXPENSE_WHISPER)); + !isActionOfType(mostRecentReportAction, CONST.REPORT.ACTIONS.TYPE.CREATED) && + !isDeletedAction(mostRecentReportAction) && + (!deleteTransactionNavigateBackUrl || !isActionOfType(mostRecentReportAction, CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_TRACK_EXPENSE_WHISPER)); const lastRoute = usePrevious(route); const lastReportActionIDFromRoute = usePrevious(reportActionIDFromRoute); @@ -778,7 +817,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { > {headerView} - {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( + {!!accountManagerReportID && isConciergeChatReport(report) && isBannerVisible && ( )} - + Date: Wed, 15 Jan 2025 19:50:39 +0530 Subject: [PATCH 015/145] fix ESlint. Signed-off-by: krishna2323 --- .../BrokenConnectionDescription.tsx | 14 ++-- src/components/MoneyRequestHeader.tsx | 40 ++++++---- .../MoneyRequestPreviewContent.tsx | 79 ++++++++++--------- 3 files changed, 74 insertions(+), 59 deletions(-) diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index 2b0b5a3dba8d..14265030bf75 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -2,9 +2,9 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {isInstantSubmitEnabled, isPolicyAdmin as isPolicyAdminUtil} from '@libs/PolicyUtils'; +import {isCurrentUserSubmitter, isProcessingReport, isReportApproved, isReportManuallyReimbursed} from '@libs/ReportUtils'; +import {getTransactionViolations} from '@libs/TransactionUtils'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -25,11 +25,11 @@ type BrokenConnectionDescriptionProps = { function BrokenConnectionDescription({transactionID, policy, report}: BrokenConnectionDescriptionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); + const transactionViolations = getTransactionViolations(transactionID); const brokenConnection530Error = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530); const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); - const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + const isPolicyAdmin = isPolicyAdminUtil(policy); if (!brokenConnection530Error && !brokenConnectionError) { return ''; @@ -39,7 +39,7 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn return translate('violations.brokenConnection530Error'); } - if (isPolicyAdmin && !ReportUtils.isCurrentUserSubmitter(report?.reportID)) { + if (isPolicyAdmin && !isCurrentUserSubmitter(report?.reportID)) { return ( <> {`${translate('violations.adminBrokenConnectionError')}`} @@ -57,7 +57,7 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn ); } - if (ReportUtils.isReportApproved(report) || ReportUtils.isReportManuallyReimbursed(report) || (ReportUtils.isProcessingReport(report) && !PolicyUtils.isInstantSubmitEnabled(policy))) { + if (isReportApproved(report) || isReportManuallyReimbursed(report) || (isProcessingReport(report) && !isInstantSubmitEnabled(policy))) { return translate('violations.memberBrokenConnectionError'); } diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index edc9ab1e6250..4a24dbaa1134 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -9,10 +9,21 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {isPolicyAdmin} from '@libs/PolicyUtils'; +import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {isCurrentUserSubmitter} from '@libs/ReportUtils'; +import { + allHavePendingRTERViolation, + getTransactionViolations, + hasPendingRTERViolation, + hasReceipt, + isDuplicate as isDuplicateUtil, + isExpensifyCardTransaction, + isOnHold as isOnHoldUtil, + isPending, + isReceiptBeingScanned, + shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationUtil, +} from '@libs/TransactionUtils'; import variables from '@styles/variables'; import * as IOU from '@userActions/IOU'; import * as TransactionActions from '@userActions/Transaction'; @@ -54,9 +65,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? CONST.DEFAULT_NUMBER_ID}`); const [transaction] = useOnyx( `${ONYXKEYS.COLLECTION.TRANSACTION}${ - ReportActionsUtils.isMoneyRequestAction(parentReportAction) - ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID - : CONST.DEFAULT_NUMBER_ID + isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID : CONST.DEFAULT_NUMBER_ID }`, ); const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true}); @@ -65,25 +74,24 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const theme = useTheme(); const {translate} = useLocalize(); const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); - const isOnHold = TransactionUtils.isOnHold(transaction); - const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID); + const isOnHold = isOnHoldUtil(transaction); + const isDuplicate = isDuplicateUtil(transaction?.transactionID); const reportID = report?.reportID; const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; - const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation(transaction?.transactionID ? [transaction?.transactionID] : []); + const hasAllPendingRTERViolations = allHavePendingRTERViolation(transaction?.transactionID ? [transaction?.transactionID] : []); - const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID, parentReport, policy); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationUtil(transaction?.transactionID, parentReport, policy); - const shouldShowMarkAsCashButton = - hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isCurrentUserSubmitter(parentReport?.reportID))); + const shouldShowMarkAsCashButton = hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(parentReport?.reportID))); const markAsCash = useCallback(() => { TransactionActions.markAsCash(transaction?.transactionID, reportID); }, [reportID, transaction?.transactionID]); - const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); + const isScanning = hasReceipt(transaction) && isReceiptBeingScanned(transaction); const getStatusIcon: (src: IconAsset) => ReactNode = (src) => ( avatar.id); if (isPolicyExpenseChat && isBillSplit) { - sortedParticipantAvatars.push(ReportUtils.getWorkspaceIcon(chatReport)); + sortedParticipantAvatars.push(getWorkspaceIcon(chatReport)); } // Pay button should only be visible to the manager of the report. @@ -109,7 +116,7 @@ function MoneyRequestPreviewContent({ merchant, tag, category, - } = useMemo>(() => ReportUtils.getTransactionDetails(transaction) ?? {}, [transaction]); + } = useMemo>(() => getTransactionDetails(transaction) ?? {}, [transaction]); const description = truncate(StringUtils.lineBreaksToSpaces(requestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); @@ -119,14 +126,14 @@ function MoneyRequestPreviewContent({ const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; const isPartialHold = isSettlementOrApprovalPartial && isOnHold; const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID, allViolations, true); - const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID, allViolations, true) && ReportUtils.isPaidGroupPolicy(iouReport); + const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID, allViolations, true) && isPaidGroupPolicy(iouReport); const hasWarningTypeViolations = TransactionUtils.hasWarningTypeViolation(transaction?.transactionID, allViolations, true); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); - const isSettled = ReportUtils.isSettled(iouReport?.reportID); - const isApproved = ReportUtils.isReportApproved(iouReport); + const isSettled = isSettledUtil(iouReport?.reportID); + const isApproved = isReportApproved(iouReport); const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; const isReviewDuplicateTransactionPage = route.name === SCREENS.TRANSACTION_DUPLICATE.REVIEW; @@ -153,8 +160,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 : undefined; + const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); + const reviewingTransactionID = isMoneyRequestActionUtil(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined; /* Show the merchant for IOUs and expenses only if: @@ -174,7 +181,7 @@ function MoneyRequestPreviewContent({ merchantOrDescription = description || ''; } - const receiptImages = [{...ReceiptUtils.getThumbnailAndImageURIs(transaction), transaction}]; + const receiptImages = [{...getThumbnailAndImageURIs(transaction), transaction}]; const getSettledMessage = (): string => { if (isCardTransaction) { @@ -232,9 +239,9 @@ function MoneyRequestPreviewContent({ } return message; } - } else if (hasNoticeTypeViolations && transaction && !ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID)) { + } else if (hasNoticeTypeViolations && transaction && !isReportApproved(iouReport) && !isSettledUtil(iouReport?.reportID)) { message += ` • ${translate('violations.reviewRequired')}`; - } else if (ReportUtils.isPaidGroupPolicyExpenseReport(iouReport) && ReportUtils.isReportApproved(iouReport) && !ReportUtils.isSettled(iouReport?.reportID) && !isPartialHold) { + } else if (isPaidGroupPolicyExpenseReport(iouReport) && isReportApproved(iouReport) && !isSettledUtil(iouReport?.reportID) && !isPartialHold) { message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.approved')}`; } else if (iouReport?.isCancelledIOU) { message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.canceled')}`; @@ -271,12 +278,12 @@ function MoneyRequestPreviewContent({ return translate('iou.fieldPending'); } - return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); + return convertToDisplayString(requestAmount, requestCurrency); }; const getDisplayDeleteAmountText = (): string => { - const iouOriginalMessage: OnyxEntry = ReportActionsUtils.isMoneyRequestAction(action) ? ReportActionsUtils.getOriginalMessage(action) ?? undefined : undefined; - return CurrencyUtils.convertToDisplayString(iouOriginalMessage?.amount, iouOriginalMessage?.currency); + const iouOriginalMessage: OnyxEntry = isMoneyRequestActionUtil(action) ? getOriginalMessage(action) ?? undefined : undefined; + return convertToDisplayString(iouOriginalMessage?.amount, iouOriginalMessage?.currency); }; const displayAmount = isDeleted ? getDisplayDeleteAmountText() : getDisplayAmountText(); @@ -288,7 +295,7 @@ function MoneyRequestPreviewContent({ () => shouldShowSplitShare ? transaction?.comment?.splits?.find((split) => split.accountID === sessionAccountID)?.amount ?? - IOUUtils.calculateAmount(isPolicyExpenseChat ? 1 : participantAccountIDs.length - 1, requestAmount, requestCurrency ?? '', action.actorAccountID === sessionAccountID) + calculateAmount(isPolicyExpenseChat ? 1 : participantAccountIDs.length - 1, requestAmount, requestCurrency ?? '', action.actorAccountID === sessionAccountID) : 0, [shouldShowSplitShare, isPolicyExpenseChat, action.actorAccountID, participantAccountIDs.length, transaction?.comment?.splits, requestAmount, requestCurrency, sessionAccountID], ); @@ -350,7 +357,7 @@ function MoneyRequestPreviewContent({ size={1} /> )} - {isEmptyObject(transaction) && !ReportActionsUtils.isMessageDeleted(action) && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? ( + {isEmptyObject(transaction) && !isMessageDeleted(action) && action.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ? ( ) : ( @@ -384,7 +391,7 @@ function MoneyRequestPreviewContent({ > {displayAmount} - {ReportUtils.isSettled(iouReport?.reportID) && !isPartialHold && !isBillSplit && ( + {isSettledUtil(iouReport?.reportID) && !isPartialHold && !isBillSplit && ( {!!splitShare && ( - {translate('iou.yourSplit', {amount: CurrencyUtils.convertToDisplayString(splitShare, requestCurrency)})} + {translate('iou.yourSplit', {amount: convertToDisplayString(splitShare, requestCurrency)})} )} @@ -471,7 +478,7 @@ function MoneyRequestPreviewContent({ numberOfLines={1} style={[styles.textMicroSupporting, styles.pre, styles.flexShrink1]} > - {PolicyUtils.getCleanedTagName(tag)} + {getCleanedTagName(tag)} )} @@ -493,17 +500,17 @@ function MoneyRequestPreviewContent({ return ( DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressIn={() => canUseTouchScreen() && ControlSelection.block()} onPressOut={() => ControlSelection.unblock()} onLongPress={showContextMenu} shouldUseHapticsOnLongPress accessibilityLabel={isBillSplit ? translate('iou.split') : showCashOrCard} - accessibilityHint={CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency)} + accessibilityHint={convertToDisplayString(requestAmount, requestCurrency)} style={[ styles.moneyRequestPreviewBox, containerStyles, shouldDisableOnPress && styles.cursorDefault, - (isSettled || ReportUtils.isReportApproved(iouReport)) && isSettlementOrApprovalPartial && styles.offlineFeedback.pending, + (isSettled || isReportApproved(iouReport)) && isSettlementOrApprovalPartial && styles.offlineFeedback.pending, ]} > {childContainer} From 825c4bea39e8c6a588695d49d7f535b452161627 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 15 Jan 2025 20:33:37 +0530 Subject: [PATCH 016/145] fix ESlint. Signed-off-by: krishna2323 --- src/components/MoneyRequestHeader.tsx | 8 +- .../MoneyRequestPreviewContent.tsx | 73 +++++--- .../ReportActionItem/MoneyRequestView.tsx | 169 ++++++++++-------- 3 files changed, 149 insertions(+), 101 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 4a24dbaa1134..45fb3134b0db 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -8,6 +8,8 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import {dismissHoldUseExplanation} from '@libs/actions/IOU'; +import {markAsCash as markAsCashUtil} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import {isPolicyAdmin} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; @@ -25,8 +27,6 @@ import { shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationUtil, } from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import * as IOU from '@userActions/IOU'; -import * as TransactionActions from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -88,7 +88,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const shouldShowMarkAsCashButton = hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(parentReport?.reportID))); const markAsCash = useCallback(() => { - TransactionActions.markAsCash(transaction?.transactionID, reportID); + markAsCashUtil(transaction?.transactionID, reportID); }, [reportID, transaction?.transactionID]); const isScanning = hasReceipt(transaction) && isReceiptBeingScanned(transaction); @@ -154,7 +154,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre }, [isSmallScreenWidth, shouldShowHoldMenu]); const handleHoldRequestClose = () => { - IOU.dismissHoldUseExplanation(); + dismissHoldUseExplanation(); }; return ( diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 6363676892e4..d59f56ec53a8 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -23,6 +23,9 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {clearWalletTermsError} from '@libs/actions/PaymentMethods'; +import {clearIOUError} from '@libs/actions/Report'; +import {abandonReviewDuplicateTransactions, setReviewDuplicatesKey} from '@libs/actions/Transaction'; import ControlSelection from '@libs/ControlSelection'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; @@ -45,12 +48,28 @@ import { } from '@libs/ReportUtils'; import type {TransactionDetails} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import { + compareDuplicateTransactionFields, + getTransactionViolations, + hasMissingSmartscanFields, + hasNoticeTypeViolation, + hasPendingUI, + hasReceipt as hasReceiptUtil, + hasViolation, + hasWarningTypeViolation, + isAmountMissing as isAmountMissingUtil, + isCardTransaction as isCardTransactionUtil, + isDistanceRequest as isDistanceRequestUtil, + isFetchingWaypointsFromServer as isFetchingWaypointsFromServerUtil, + isMerchantMissing as isMerchantMissingUtil, + isOnHold as isOnHoldUtil, + isPending, + isReceiptBeingScanned, + removeSettledAndApprovedTransactions, + shouldShowBrokenConnectionViolation, +} from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import variables from '@styles/variables'; -import * as PaymentMethods from '@userActions/PaymentMethods'; -import * as Report from '@userActions/Report'; -import * as Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -92,7 +111,7 @@ function MoneyRequestPreviewContent({ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); const [allViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); - const transactionViolations = TransactionUtils.getTransactionViolations(transaction?.transactionID); + const transactionViolations = getTransactionViolations(transaction?.transactionID); const sessionAccountID = session?.accountID; const managerID = iouReport?.managerID ?? CONST.DEFAULT_NUMBER_ID; @@ -120,18 +139,18 @@ function MoneyRequestPreviewContent({ const description = truncate(StringUtils.lineBreaksToSpaces(requestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); - const hasReceipt = TransactionUtils.hasReceipt(transaction); - const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); - const isOnHold = TransactionUtils.isOnHold(transaction); + const hasReceipt = hasReceiptUtil(transaction); + const isScanning = hasReceipt && isReceiptBeingScanned(transaction); + const isOnHold = isOnHoldUtil(transaction); const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; const isPartialHold = isSettlementOrApprovalPartial && isOnHold; - const hasViolations = TransactionUtils.hasViolation(transaction?.transactionID, allViolations, true); - const hasNoticeTypeViolations = TransactionUtils.hasNoticeTypeViolation(transaction?.transactionID, allViolations, true) && isPaidGroupPolicy(iouReport); - const hasWarningTypeViolations = TransactionUtils.hasWarningTypeViolation(transaction?.transactionID, allViolations, true); - const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); - const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); - const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); - const isCardTransaction = TransactionUtils.isCardTransaction(transaction); + const hasViolations = hasViolation(transaction?.transactionID, allViolations, true); + const hasNoticeTypeViolations = hasNoticeTypeViolation(transaction?.transactionID, allViolations, true) && isPaidGroupPolicy(iouReport); + const hasWarningTypeViolations = hasWarningTypeViolation(transaction?.transactionID, allViolations, true); + const hasFieldErrors = hasMissingSmartscanFields(transaction); + const isDistanceRequest = isDistanceRequestUtil(transaction); + const isFetchingWaypointsFromServer = isFetchingWaypointsFromServerUtil(transaction); + const isCardTransaction = isCardTransactionUtil(transaction); const isSettled = isSettledUtil(iouReport?.reportID); const isApproved = isReportApproved(iouReport); const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; @@ -147,7 +166,7 @@ function MoneyRequestPreviewContent({ ); // Remove settled transactions from duplicates - const duplicates = useMemo(() => TransactionUtils.removeSettledAndApprovedTransactions(allDuplicates), [allDuplicates]); + const duplicates = useMemo(() => removeSettledAndApprovedTransactions(allDuplicates), [allDuplicates]); // When there are no settled transactions in duplicates, show the "Keep this one" button const shouldShowKeepButton = !!(allDuplicates.length && duplicates.length && allDuplicates.length === duplicates.length); @@ -214,7 +233,7 @@ function MoneyRequestPreviewContent({ } if (shouldShowRBR && transaction) { - const violations = TransactionUtils.getTransactionViolations(transaction.transactionID); + const violations = getTransactionViolations(transaction.transactionID); if (shouldShowHoldMessage) { return `${message} ${CONST.DOT_SEPARATOR} ${translate('violations.hold')}`; } @@ -228,8 +247,8 @@ function MoneyRequestPreviewContent({ return `${message} ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`; } if (hasFieldErrors) { - const isMerchantMissing = TransactionUtils.isMerchantMissing(transaction); - const isAmountMissing = TransactionUtils.isAmountMissing(transaction); + const isMerchantMissing = isMerchantMissingUtil(transaction); + const isAmountMissing = isAmountMissingUtil(transaction); if (isAmountMissing && isMerchantMissing) { message += ` ${CONST.DOT_SEPARATOR} ${translate('violations.reviewRequired')}`; } else if (isAmountMissing) { @@ -255,13 +274,13 @@ function MoneyRequestPreviewContent({ if (isScanning) { return {shouldShow: true, messageIcon: ReceiptScan, messageDescription: translate('iou.receiptScanInProgress')}; } - if (TransactionUtils.isPending(transaction)) { + if (isPending(transaction)) { return {shouldShow: true, messageIcon: Expensicons.CreditCardHourglass, messageDescription: translate('iou.transactionPending')}; } - if (TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID, iouReport, policy)) { + if (shouldShowBrokenConnectionViolation(transaction?.transactionID, iouReport, policy)) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('violations.brokenConnection530Error')}; } - if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID))) { + if (hasPendingUI(transaction, getTransactionViolations(transaction?.transactionID))) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; } return {shouldShow: false}; @@ -305,9 +324,9 @@ 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}); + abandonReviewDuplicateTransactions(); + const comparisonResult = compareDuplicateTransactionFields(reviewingTransactionID, transaction?.reportID, transaction?.transactionID ?? reviewingTransactionID); + 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)); @@ -335,8 +354,8 @@ function MoneyRequestPreviewContent({ { - PaymentMethods.clearWalletTermsError(); - Report.clearIOUError(chatReportID); + clearWalletTermsError(); + clearIOUError(chatReportID); }} errorRowStyles={[styles.mbn1]} needsOffscreenAlphaCompositing diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index f2cec506ddc8..513f33d1a78c 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -18,26 +18,56 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useViolations from '@hooks/useViolations'; import type {ViolationField} from '@hooks/useViolations'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {cleanUpMoneyRequest, updateMoneyRequestBillable} from '@libs/actions/IOU'; +import {navigateToConciergeChatAndDeleteReport} from '@libs/actions/Report'; +import {clearAllRelatedReportActionErrors} from '@libs/actions/ReportActions'; +import {clearError} from '@libs/actions/Transaction'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import {isTaxTrackingEnabled} from '@libs/PolicyUtils'; -import * as ReceiptUtils from '@libs/ReceiptUtils'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {hasEnabledOptions} from '@libs/OptionsListUtils'; +import {getTagLists, hasDependentTags, isTaxTrackingEnabled} from '@libs/PolicyUtils'; +import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; +import {getOriginalMessage, isMoneyRequestAction, isPayAction} from '@libs/ReportActionsUtils'; +import { + canEditFieldOfMoneyRequest, + canEditMoneyRequest, + canUserPerformWriteAction as canUserPerformWriteActionUtil, + getAddWorkspaceRoomOrChatReportErrors, + getTransactionDetails, + getTripIDFromTransactionParentReportID, + isInvoiceReport, + isMoneyRequestReport, + isPaidGroupPolicy, + isReportApproved, + isReportInGroupPolicy, + isSettled as isSettledUtil, + isTrackExpenseReport, +} from '@libs/ReportUtils'; import type {TransactionDetails} from '@libs/ReportUtils'; -import * as TagsOptionsListUtils from '@libs/TagsOptionsListUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {hasEnabledTags} from '@libs/TagsOptionsListUtils'; +import { + didReceiptScanSucceed as didReceiptScanSucceedUtil, + getBillable, + getCardName, + getDescription, + getDistanceInMeters, + getTagForDisplay, + getTaxName, + getTransactionViolations, + hasMissingSmartscanFields, + hasReceipt as hasReceiptUtil, + hasReservationList, + hasRoute as hasRouteUtil, + isCardTransaction as isCardTransactionUtil, + isDistanceRequest as isDistanceRequestUtil, + isReceiptBeingScanned as isReceiptBeingScannedUtil, + shouldShowAttendees as shouldShowAttendeesUtil, +} from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; -import * as IOU from '@userActions/IOU'; -import * as Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import * as Report from '@src/libs/actions/Report'; -import * as ReportActions from '@src/libs/actions/ReportActions'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; @@ -72,7 +102,7 @@ const receiptFieldViolationNames: OnyxTypes.ViolationName[] = [CONST.VIOLATIONS. const getTransactionID = (report: OnyxEntry, parentReportActions: OnyxEntry) => { const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? CONST.DEFAULT_NUMBER_ID]; - const originalMessage = parentReportAction && ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction) : undefined; + const originalMessage = parentReportAction && isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction) : undefined; return originalMessage?.IOUTransactionID ?? undefined; }; @@ -95,13 +125,13 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID ?? CONST.DEFAULT_NUMBER_ID}`, { canEvict: false, }); - const transactionViolations = TransactionUtils.getTransactionViolations(getTransactionID(report, parentReportActions) ?? undefined); + const transactionViolations = getTransactionViolations(getTransactionID(report, parentReportActions) ?? undefined); const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? CONST.DEFAULT_NUMBER_ID]; - const isTrackExpense = ReportUtils.isTrackExpenseReport(report); + const isTrackExpense = isTrackExpenseReport(report); const moneyRequestReport = parentReport; const linkedTransactionID = useMemo(() => { - const originalMessage = parentReportAction && ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction) : undefined; + const originalMessage = parentReportAction && isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction) : undefined; return originalMessage?.IOUTransactionID; }, [parentReportAction]); @@ -122,74 +152,74 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals originalAmount: transactionOriginalAmount, originalCurrency: transactionOriginalCurrency, postedDate: transactionPostedDate, - } = useMemo>(() => ReportUtils.getTransactionDetails(transaction) ?? {}, [transaction]); + } = useMemo>(() => getTransactionDetails(transaction) ?? {}, [transaction]); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); - const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; - const formattedPerAttendeeAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount / (transactionAttendees?.length ?? 1), transactionCurrency) : ''; - const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); - const isCardTransaction = TransactionUtils.isCardTransaction(transaction); - const cardProgramName = TransactionUtils.getCardName(transaction); + const isDistanceRequest = isDistanceRequestUtil(transaction); + const formattedTransactionAmount = transactionAmount ? convertToDisplayString(transactionAmount, transactionCurrency) : ''; + const formattedPerAttendeeAmount = transactionAmount ? convertToDisplayString(transactionAmount / (transactionAttendees?.length ?? 1), transactionCurrency) : ''; + const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); + const isCardTransaction = isCardTransactionUtil(transaction); + const cardProgramName = getCardName(transaction); const shouldShowCard = isCardTransaction && cardProgramName; - const isApproved = ReportUtils.isReportApproved(moneyRequestReport); - const isInvoice = ReportUtils.isInvoiceReport(moneyRequestReport); - const isPaidReport = ReportActionsUtils.isPayAction(parentReportAction); + const isApproved = isReportApproved(moneyRequestReport); + const isInvoice = isInvoiceReport(moneyRequestReport); + const isPaidReport = isPayAction(parentReportAction); const taxRates = policy?.taxRates; const formattedTaxAmount = updatedTransaction?.taxAmount - ? CurrencyUtils.convertToDisplayString(Math.abs(updatedTransaction?.taxAmount), transactionCurrency) - : CurrencyUtils.convertToDisplayString(Math.abs(transactionTaxAmount ?? 0), transactionCurrency); + ? convertToDisplayString(Math.abs(updatedTransaction?.taxAmount), transactionCurrency) + : convertToDisplayString(Math.abs(transactionTaxAmount ?? 0), transactionCurrency); const taxRatesDescription = taxRates?.name; - const taxRateTitle = updatedTransaction ? TransactionUtils.getTaxName(policy, updatedTransaction) : TransactionUtils.getTaxName(policy, transaction); + const taxRateTitle = updatedTransaction ? getTaxName(policy, updatedTransaction) : getTaxName(policy, transaction); - const isSettled = ReportUtils.isSettled(moneyRequestReport?.reportID); + const isSettled = isSettledUtil(moneyRequestReport?.reportID); const isCancelled = moneyRequestReport && moneyRequestReport?.isCancelledIOU; // Flags for allowing or disallowing editing an expense // Used for non-restricted fields such as: description, category, tag, billable, etc... - const canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(report) && !readonly; - const canEdit = ReportActionsUtils.isMoneyRequestAction(parentReportAction) && ReportUtils.canEditMoneyRequest(parentReportAction, transaction) && canUserPerformWriteAction; + const canUserPerformWriteAction = !!canUserPerformWriteActionUtil(report) && !readonly; + const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, transaction) && canUserPerformWriteAction; const canEditTaxFields = canEdit && !isDistanceRequest; - const canEditAmount = canUserPerformWriteAction && ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.AMOUNT); - const canEditMerchant = canUserPerformWriteAction && ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.MERCHANT); - const canEditDate = canUserPerformWriteAction && ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE); - const canEditReceipt = canUserPerformWriteAction && ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); - const hasReceipt = TransactionUtils.hasReceipt(updatedTransaction ?? transaction); - const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(updatedTransaction ?? transaction); - const didReceiptScanSucceed = hasReceipt && TransactionUtils.didReceiptScanSucceed(transaction); - const canEditDistance = canUserPerformWriteAction && ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); - const canEditDistanceRate = canUserPerformWriteAction && ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE_RATE); + const canEditAmount = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.AMOUNT); + const canEditMerchant = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.MERCHANT); + const canEditDate = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE); + const canEditReceipt = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); + const hasReceipt = hasReceiptUtil(updatedTransaction ?? transaction); + const isReceiptBeingScanned = hasReceipt && isReceiptBeingScannedUtil(updatedTransaction ?? transaction); + const didReceiptScanSucceed = hasReceipt && didReceiptScanSucceedUtil(transaction); + const canEditDistance = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); + const canEditDistanceRate = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE_RATE); const isAdmin = policy?.role === 'admin'; - const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID; + const isApprover = isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID; const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isRequestor = currentUserPersonalDetails.accountID === parentReportAction?.actorAccountID; // A flag for verifying that the current report is a sub-report of a workspace chat // if the policy of the report is either Collect or Control, then this report must be tied to workspace chat - const isPolicyExpenseChat = ReportUtils.isReportInGroupPolicy(report); + const isPolicyExpenseChat = isReportInGroupPolicy(report); - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTagList), [policyTagList]); + const policyTagLists = useMemo(() => getTagLists(policyTagList), [policyTagList]); const iouType = isTrackExpense ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT; // Flags for showing categories and tags // transactionCategory can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || OptionsListUtils.hasEnabledOptions(policyCategories ?? {})); + const shouldShowCategory = isPolicyExpenseChat && (transactionCategory || hasEnabledOptions(policyCategories ?? {})); // transactionTag can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const shouldShowTag = isPolicyExpenseChat && (transactionTag || TagsOptionsListUtils.hasEnabledTags(policyTagLists)); + const shouldShowTag = isPolicyExpenseChat && (transactionTag || hasEnabledTags(policyTagLists)); const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable); - const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]); + const shouldShowAttendees = useMemo(() => shouldShowAttendeesUtil(iouType, policy), [iouType, policy]); const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest); - const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.parentReportID); - const shouldShowViewTripDetails = TransactionUtils.hasReservationList(transaction) && !!tripID; + const tripID = getTripIDFromTransactionParentReportID(parentReport?.parentReportID); + const shouldShowViewTripDetails = hasReservationList(transaction) && !!tripID; - const {getViolationsForField} = useViolations(transactionViolations ?? [], isReceiptBeingScanned || !ReportUtils.isPaidGroupPolicy(report)); + const {getViolationsForField} = useViolations(transactionViolations ?? [], isReceiptBeingScanned || !isPaidGroupPolicy(report)); const hasViolations = useCallback( (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string): boolean => getViolationsForField(field, data, policyHasDependentTags, tagValue).length > 0, @@ -199,15 +229,15 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals let amountDescription = `${translate('iou.amount')}`; let dateDescription = `${translate('common.date')}`; - const hasRoute = TransactionUtils.hasRoute(transactionBackup ?? transaction, isDistanceRequest); + const hasRoute = hasRouteUtil(transactionBackup ?? transaction, isDistanceRequest); const {unit, rate} = DistanceRequestUtils.getRate({transaction, policy}); - const distance = TransactionUtils.getDistanceInMeters(transactionBackup ?? transaction, unit); + const distance = getDistanceInMeters(transactionBackup ?? transaction, unit); const currency = transactionCurrency ?? CONST.CURRENCY.USD; const rateToDisplay = DistanceRequestUtils.getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, isOffline); const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); let merchantTitle = isEmptyMerchant ? '' : transactionMerchant; let amountTitle = formattedTransactionAmount ? formattedTransactionAmount.toString() : ''; - if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { + if (hasReceiptUtil(transaction) && isReceiptBeingScannedUtil(transaction)) { merchantTitle = translate('iou.receiptStatusTitle'); amountTitle = translate('iou.receiptStatusTitle'); } @@ -216,7 +246,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals if (!updatedTransaction) { return undefined; } - return TransactionUtils.getDescription(updatedTransaction ?? null); + return getDescription(updatedTransaction ?? null); }, [updatedTransaction]); const isEmptyUpdatedMerchant = updatedTransaction?.modifiedMerchant === '' || updatedTransaction?.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const updatedMerchantTitle = isEmptyUpdatedMerchant ? '' : updatedTransaction?.modifiedMerchant ?? merchantTitle; @@ -224,10 +254,10 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const saveBillable = useCallback( (newBillable: boolean) => { // If the value hasn't changed, don't request to save changes on the server and just close the modal - if (newBillable === TransactionUtils.getBillable(transaction)) { + if (newBillable === getBillable(transaction)) { return; } - IOU.updateMoneyRequestBillable(transaction?.transactionID, report?.reportID, newBillable, policy, policyTagList, policyCategories); + updateMoneyRequestBillable(transaction?.transactionID, report?.reportID, newBillable, policy, policyTagList, policyCategories); }, [transaction, report, policy, policyTagList, policyCategories], ); @@ -256,9 +286,9 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals } let receiptURIs; - const hasErrors = TransactionUtils.hasMissingSmartscanFields(transaction); + const hasErrors = hasMissingSmartscanFields(transaction); if (hasReceipt) { - receiptURIs = ReceiptUtils.getThumbnailAndImageURIs(updatedTransaction ?? transaction); + receiptURIs = getThumbnailAndImageURIs(updatedTransaction ?? transaction); } const pendingAction = transaction?.pendingAction; // Need to return undefined when we have pendingAction to avoid the duplicate pending action @@ -358,7 +388,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const isReceiptAllowed = !isPaidReport && !isInvoice; const shouldShowReceiptEmptyState = - isReceiptAllowed && !hasReceipt && !isApproved && !isSettled && (canEditReceipt || isAdmin || isApprover || isRequestor) && (canEditReceipt || ReportUtils.isPaidGroupPolicy(report)); + isReceiptAllowed && !hasReceipt && !isApproved && !isSettled && (canEditReceipt || isAdmin || isApprover || isRequestor) && (canEditReceipt || isPaidGroupPolicy(report)); const [receiptImageViolations, receiptViolations] = useMemo(() => { const imageViolations = []; @@ -382,8 +412,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals // Whether to show receipt audit result (e.g.`Verified`, `Issue Found`) and messages (e.g. `Receipt not verified. Please confirm accuracy.`) // `!!(receiptViolations.length || didReceiptScanSucceed)` is for not showing `Verified` when `receiptViolations` is empty and `didReceiptScanSucceed` is false. - const shouldShowAuditMessage = - !isReceiptBeingScanned && (hasReceipt || receiptRequiredViolation) && !!(receiptViolations.length || didReceiptScanSucceed) && ReportUtils.isPaidGroupPolicy(report); + const shouldShowAuditMessage = !isReceiptBeingScanned && (hasReceipt || receiptRequiredViolation) && !!(receiptViolations.length || didReceiptScanSucceed) && isPaidGroupPolicy(report); const shouldShowReceiptAudit = isReceiptAllowed && (shouldShowReceiptEmptyState || hasReceipt); const errors = { @@ -392,8 +421,8 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals }; const tagList = policyTagLists.map(({name, orderWeight, tags}, index) => { - const tagForDisplay = TransactionUtils.getTagForDisplay(updatedTransaction ?? transaction, index); - const shouldShow = !!tagForDisplay || OptionsListUtils.hasEnabledOptions(tags); + const tagForDisplay = getTagForDisplay(updatedTransaction ?? transaction, index); + const shouldShow = !!tagForDisplay || hasEnabledOptions(tags); if (!shouldShow) { return null; } @@ -404,7 +433,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals tagListIndex: index, tagListName: name, }, - PolicyUtils.hasDependentTags(policy, policyTagList), + hasDependentTags(policy, policyTagList), tagForDisplay, ); return ( @@ -463,17 +492,17 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals } if (transaction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { - if (chatReport?.reportID && ReportUtils.getAddWorkspaceRoomOrChatReportErrors(chatReport)) { - Report.navigateToConciergeChatAndDeleteReport(chatReport.reportID, true, true); + if (chatReport?.reportID && getAddWorkspaceRoomOrChatReportErrors(chatReport)) { + navigateToConciergeChatAndDeleteReport(chatReport.reportID, true, true); return; } if (parentReportAction) { - IOU.cleanUpMoneyRequest(transaction?.transactionID ?? linkedTransactionID, parentReportAction, true); + cleanUpMoneyRequest(transaction?.transactionID ?? linkedTransactionID, parentReportAction, true); return; } } - Transaction.clearError(transaction?.transactionID ?? linkedTransactionID); - ReportActions.clearAllRelatedReportActionErrors(report?.reportID, parentReportAction); + clearError(transaction?.transactionID ?? linkedTransactionID); + clearAllRelatedReportActionErrors(report?.reportID, parentReportAction); }} > {hasReceipt && ( From 486acb530cb149d4d059afa24c05b368a0c24675 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 16 Jan 2025 00:22:53 +0530 Subject: [PATCH 017/145] revert unnecessary changes. Signed-off-by: krishna2323 --- src/libs/actions/Transaction.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 9baeadff4232..5eb426b0435f 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -433,13 +433,18 @@ function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmiss failureData.push(...failureDataTransaction); failureData.push(...failureReportActions); - const successData: OnyxUpdate[] = transactionsReportActions.map((action, index) => ({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? CONST.DEFAULT_NUMBER_ID}`, - value: { - [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? CONST.DEFAULT_NUMBER_ID]: null, - }, - })); + const successData: OnyxUpdate[] = transactionsReportActions.map((action, index) => { + const optimisticDissmidedViolationReportAction = optimisticDissmidedViolationReportActions.at(index); + return { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID}`, + value: optimisticDissmidedViolationReportAction + ? { + [optimisticDissmidedViolationReportAction.reportActionID]: null, + } + : undefined, + }; + }); // We are creating duplicate resolved report actions for each duplicate transactions and all the report actions // should be correctly linked with their parent report but the BE is sometimes linking report actions to different From 0a0cbc31a67f77e336fb1cda75a201f23be3a70e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 16 Jan 2025 00:43:29 +0530 Subject: [PATCH 018/145] fix ESLint. Signed-off-by: krishna2323 --- src/components/MoneyRequestHeader.tsx | 2 -- .../ReportActionItem/ReportPreview.tsx | 2 +- src/libs/actions/ReportActions.ts | 18 ++++++++--------- src/libs/actions/Transaction.ts | 1 - .../DebugTransactionViolations.tsx | 4 ++-- .../DebugTransactionViolationCreatePage.tsx | 8 ++++---- .../DebugTransactionViolationPage.tsx | 8 ++++---- src/pages/TransactionDuplicate/Review.tsx | 20 +++++++++---------- 8 files changed, 30 insertions(+), 33 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 0929a9ad8751..372245070380 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -8,7 +8,6 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {dismissHoldUseExplanation} from '@libs/actions/IOU'; import {markAsCash as markAsCashUtil} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import {isPolicyAdmin} from '@libs/PolicyUtils'; @@ -27,7 +26,6 @@ import { shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationUtil, } from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import {markAsCash as markAsCashAction} from '@userActions/Transaction'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 8d1383cd89db..796fbd60a9df 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -237,7 +237,7 @@ function ReportPreview({ hasActionsWithErrors(iouReportID); const lastThreeTransactions = allTransactions.slice(-3); const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...getThumbnailAndImageURIs(transaction), transaction})); - const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(allTransactions.at(0), getTransactionViolations(allTransactions.at(0)?.transactionID, transactionViolations)); + const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(allTransactions.at(0), getTransactionViolations(allTransactions.at(0)?.transactionID)); const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(allTransactions.at(0)?.transactionID, iouReport, policy); let formattedMerchant = numberOfRequests === 1 ? getMerchant(allTransactions.at(0)) : null; const formattedDescription = numberOfRequests === 1 ? getDescription(allTransactions.at(0)) : null; diff --git a/src/libs/actions/ReportActions.ts b/src/libs/actions/ReportActions.ts index 93752c3f02ab..1c3da0d97a85 100644 --- a/src/libs/actions/ReportActions.ts +++ b/src/libs/actions/ReportActions.ts @@ -1,12 +1,12 @@ import type {OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import * as ReportActionUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getLinkedTransactionID, getReportAction, getReportActionMessage, isCreatedTaskReportAction} from '@libs/ReportActionsUtils'; +import {getOriginalReportID} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type ReportAction from '@src/types/onyx/ReportAction'; -import * as Report from './Report'; +import {deleteReport} from './Report'; type IgnoreDirection = 'parent' | 'child'; @@ -27,7 +27,7 @@ Onyx.connect({ }); function clearReportActionErrors(reportID: string, reportAction: ReportAction, keys?: string[]) { - const originalReportID = ReportUtils.getOriginalReportID(reportID, reportAction); + const originalReportID = getOriginalReportID(reportID, reportAction); if (!reportAction?.reportActionID) { return; @@ -41,16 +41,16 @@ function clearReportActionErrors(reportID: string, reportAction: ReportAction, k // If there's a linked transaction, delete that too // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const linkedTransactionID = ReportActionUtils.getLinkedTransactionID(reportAction.reportActionID, originalReportID); + const linkedTransactionID = getLinkedTransactionID(reportAction.reportActionID, originalReportID); if (linkedTransactionID) { Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID}`, null); Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportAction.childReportID}`, null); } // Delete the failed task report too - const taskReportID = ReportActionUtils.getReportActionMessage(reportAction)?.taskReportID; - if (taskReportID && ReportActionUtils.isCreatedTaskReportAction(reportAction)) { - Report.deleteReport(taskReportID); + const taskReportID = getReportActionMessage(reportAction)?.taskReportID; + if (taskReportID && isCreatedTaskReportAction(reportAction)) { + deleteReport(taskReportID); } return; } @@ -94,7 +94,7 @@ function clearAllRelatedReportActionErrors(reportID: string | undefined, reportA const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; if (report?.parentReportID && report?.parentReportActionID && ignore !== 'parent') { - const parentReportAction = ReportActionUtils.getReportAction(report.parentReportID, report.parentReportActionID); + const parentReportAction = getReportAction(report.parentReportID, report.parentReportActionID); const parentErrorKeys = Object.keys(parentReportAction?.errors ?? {}).filter((err) => errorKeys.includes(err)); clearAllRelatedReportActionErrors(report.parentReportID, parentReportAction, 'child', parentErrorKeys); diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 5eb426b0435f..29bd11fd2247 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -445,7 +445,6 @@ function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmiss : undefined, }; }); - // We are creating duplicate resolved report actions for each duplicate transactions and all the report actions // should be correctly linked with their parent report but the BE is sometimes linking report actions to different // parent reports than the one we set optimistically, resulting in duplicate report actions. Therefore, we send the BE diff --git a/src/pages/Debug/Transaction/DebugTransactionViolations.tsx b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx index 5d0c846b40d6..5190d6c76e50 100644 --- a/src/pages/Debug/Transaction/DebugTransactionViolations.tsx +++ b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx @@ -6,7 +6,7 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {getTransactionViolations} from '@libs/TransactionUtils'; import ROUTES from '@src/ROUTES'; import type {TransactionViolation} from '@src/types/onyx'; @@ -16,7 +16,7 @@ type DebugTransactionViolationsProps = { }; function DebugTransactionViolations({transactionID}: DebugTransactionViolationsProps) { - const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); + const transactionViolations = getTransactionViolations(transactionID); const styles = useThemeStyles(); const {translate} = useLocalize(); diff --git a/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx b/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx index 8d9df99a7dfa..452925da8b86 100644 --- a/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx +++ b/src/pages/Debug/TransactionViolation/DebugTransactionViolationCreatePage.tsx @@ -9,11 +9,11 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DebugUtils from '@libs/DebugUtils'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {getTransactionViolations} from '@libs/TransactionUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import Debug from '@userActions/Debug'; import CONST from '@src/CONST'; @@ -62,7 +62,7 @@ function DebugTransactionViolationCreatePage({ }: DebugTransactionViolationCreatePageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); + const transactionViolations = getTransactionViolations(transactionID); const [draftTransactionViolation, setDraftTransactionViolation] = useState(() => getInitialTransactionViolation()); const [error, setError] = useState(); @@ -95,7 +95,7 @@ function DebugTransactionViolationCreatePage({ {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx b/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx index 46e09df43dfc..1b26b0c5f72a 100644 --- a/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx +++ b/src/pages/Debug/TransactionViolation/DebugTransactionViolationPage.tsx @@ -6,13 +6,13 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Debug from '@libs/actions/Debug'; import DebugUtils from '@libs/DebugUtils'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import type {DebugTabNavigatorRoutes} from '@libs/Navigation/DebugTabNavigator'; import DebugTabNavigator from '@libs/Navigation/DebugTabNavigator'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DebugParamList} from '@libs/Navigation/types'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {getTransactionViolations} from '@libs/TransactionUtils'; import DebugDetails from '@pages/Debug/DebugDetails'; import DebugJSON from '@pages/Debug/DebugJSON'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; @@ -29,7 +29,7 @@ function DebugTransactionViolationPage({ }, }: DebugTransactionViolationPageProps) { const {translate} = useLocalize(); - const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); + const transactionViolations = getTransactionViolations(transactionID); const transactionViolation = useMemo(() => transactionViolations?.[Number(index)], [index, transactionViolations]); const styles = useThemeStyles(); @@ -84,7 +84,7 @@ function DebugTransactionViolationPage({ {({safeAreaPaddingBottomStyle}) => ( diff --git a/src/pages/TransactionDuplicate/Review.tsx b/src/pages/TransactionDuplicate/Review.tsx index 5f010c4133ac..883b7e2d8031 100644 --- a/src/pages/TransactionDuplicate/Review.tsx +++ b/src/pages/TransactionDuplicate/Review.tsx @@ -10,13 +10,13 @@ import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {dismissDuplicateTransactionViolation} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/types'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import * as Transaction from '@userActions/Transaction'; +import {getLinkedTransactionID, getReportAction} from '@libs/ReportActionsUtils'; +import {isReportApproved, isSettled} from '@libs/ReportUtils'; +import {getTransaction, getTransactionViolations} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -28,9 +28,9 @@ function TransactionDuplicateReview() { const route = useRoute>(); const currentPersonalDetails = useCurrentUserPersonalDetails(); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.threadReportID}`); - const reportAction = ReportActionsUtils.getReportAction(report?.parentReportID, report?.parentReportActionID); - const transactionID = ReportActionsUtils.getLinkedTransactionID(reportAction, report?.reportID) ?? undefined; - const transactionViolations = TransactionUtils.getTransactionViolations(transactionID); + const reportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); + const transactionID = getLinkedTransactionID(reportAction, report?.reportID) ?? undefined; + const transactionViolations = getTransactionViolations(transactionID); const duplicateTransactionIDs = useMemo( () => transactionViolations?.find((violation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION)?.data?.duplicates ?? [], @@ -38,14 +38,14 @@ function TransactionDuplicateReview() { ); const transactionIDs = transactionID ? [transactionID, ...duplicateTransactionIDs] : [...duplicateTransactionIDs]; - const transactions = transactionIDs.map((item) => TransactionUtils.getTransaction(item)).sort((a, b) => new Date(a?.created ?? '').getTime() - new Date(b?.created ?? '').getTime()); + const transactions = transactionIDs.map((item) => getTransaction(item)).sort((a, b) => new Date(a?.created ?? '').getTime() - new Date(b?.created ?? '').getTime()); const keepAll = () => { - Transaction.dismissDuplicateTransactionViolation(transactionIDs, currentPersonalDetails); + dismissDuplicateTransactionViolation(transactionIDs, currentPersonalDetails); Navigation.goBack(); }; - const hasSettledOrApprovedTransaction = transactions.some((transaction) => ReportUtils.isSettled(transaction?.reportID) || ReportUtils.isReportApproved(transaction?.reportID)); + const hasSettledOrApprovedTransaction = transactions.some((transaction) => isSettled(transaction?.reportID) || isReportApproved(transaction?.reportID)); return ( From 55f73fa15a46123b85a10de9b022ae28db466051 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 16 Jan 2025 00:49:08 +0530 Subject: [PATCH 019/145] fix ESLint. Signed-off-by: krishna2323 --- .../iou/request/step/IOURequestStepAttendees.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepAttendees.tsx b/src/pages/iou/request/step/IOURequestStepAttendees.tsx index 6672306f5221..2e64e55fb8a1 100644 --- a/src/pages/iou/request/step/IOURequestStepAttendees.tsx +++ b/src/pages/iou/request/step/IOURequestStepAttendees.tsx @@ -4,10 +4,10 @@ import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; +import {setMoneyRequestAttendees, updateMoneyRequestAttendees} from '@libs/actions/IOU'; import Navigation from '@libs/Navigation/Navigation'; -import * as TransactionUtils from '@libs/TransactionUtils'; +import {getAttendees, getTransactionViolations} from '@libs/TransactionUtils'; import MoneyRequestAttendeeSelector from '@pages/iou/request/MoneyRequestAttendeeSelector'; -import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -40,19 +40,19 @@ function IOURequestStepAttendees({ }: IOURequestStepAttendeesProps) { const isEditing = action === CONST.IOU.ACTION.EDIT; const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || CONST.DEFAULT_NUMBER_ID}`); - const [attendees, setAttendees] = useState(() => TransactionUtils.getAttendees(transaction)); + const [attendees, setAttendees] = useState(() => getAttendees(transaction)); const previousAttendees = usePrevious(attendees); const {translate} = useLocalize(); - const violations = TransactionUtils.getTransactionViolations(transactionID); + const violations = getTransactionViolations(transactionID); const saveAttendees = useCallback(() => { if (attendees.length <= 0) { return; } if (!lodashIsEqual(previousAttendees, attendees)) { - IOU.setMoneyRequestAttendees(transactionID, attendees, !isEditing); + setMoneyRequestAttendees(transactionID, attendees, !isEditing); if (isEditing) { - IOU.updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations ?? undefined); + updateMoneyRequestAttendees(transactionID, reportID, attendees, policy, policyTags, policyCategories, violations ?? undefined); } } From a5194c88a9373051e93197c3fb77b665e084257a Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 16 Jan 2025 02:48:20 +0530 Subject: [PATCH 020/145] minor update. Signed-off-by: krishna2323 --- src/libs/TransactionUtils/index.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 9c6193dfaa9d..e6cdacc81aa8 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -901,14 +901,17 @@ function isDuplicate(transactionID: string | undefined, checkDismissed = false): if (!transactionID) { return false; } - const hasDuplicatedViolation = !!allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]?.some( + const duplicateViolation = allTransactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]?.find( (violation: TransactionViolation) => violation.name === CONST.VIOLATIONS.DUPLICATED_TRANSACTION, ); + + const hasDuplicatedViolation = !!duplicateViolation; if (!checkDismissed) { - return hasDuplicatedViolation; + return !!duplicateViolation; } - const didDismissedViolation = - allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]?.comment?.dismissedViolations?.duplicatedTransaction?.[currentUserEmail] === `${currentUserAccountID}`; + + const didDismissedViolation = isViolationDismissed(transactionID, duplicateViolation); + return hasDuplicatedViolation && !didDismissedViolation; } @@ -937,8 +940,8 @@ function isOnHoldByTransactionID(transactionID: string | undefined | null): bool /** * Checks if a violation is dismissed for the given transaction */ -function isViolationDismissed(transactionID: string | undefined, violation: TransactionViolation): boolean { - if (!transactionID) { +function isViolationDismissed(transactionID: string | undefined, violation: TransactionViolation | undefined): boolean { + if (!transactionID || !violation) { return false; } return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]?.comment?.dismissedViolations?.[violation.name]?.[currentUserEmail] === `${currentUserAccountID}`; From 12285a727a716437b9ff086f970813d2215763d6 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 16 Jan 2025 15:31:27 +0700 Subject: [PATCH 021/145] fix lint --- src/ROUTES.ts | 21 ++++++++++++++++--- .../rules/IndividualExpenseRulesSection.tsx | 2 +- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7f8b75f353e1..7fa032082bf5 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1011,7 +1011,12 @@ const ROUTES = { }, WORKSPACE_CATEGORIES: { route: 'settings/workspaces/:policyID/categories', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const, + getRoute: (policyID: string | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID while building route WORKSPACE_CATEGORIES'); + } + return `settings/workspaces/${policyID}/categories` as const; + }, }, WORKSPACE_CATEGORY_SETTINGS: { route: 'settings/workspaces/:policyID/category/:categoryName', @@ -1076,11 +1081,21 @@ const ROUTES = { }, WORKSPACE_MORE_FEATURES: { route: 'settings/workspaces/:policyID/more-features', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const, + getRoute: (policyID: string | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID while building route WORKSPACE_MORE_FEATURES'); + } + return `settings/workspaces/${policyID}/more-features` as const; + }, }, WORKSPACE_TAGS: { route: 'settings/workspaces/:policyID/tags', - getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags` as const, + getRoute: (policyID: string | undefined) => { + if (!policyID) { + Log.warn('Invalid policyID while building route WORKSPACE_TAGS'); + } + return `settings/workspaces/${policyID}/tags` as const; + }, }, WORKSPACE_TAG_CREATE: { route: 'settings/workspaces/:policyID/tags/new', diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx index 365fa083ec6f..817355cff5b8 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx @@ -39,7 +39,7 @@ type IndividualExpenseRulesMenuItem = { }; function IndividualExpenseRulesSectionSubtitle({policy, translate, styles}: IndividualExpenseRulesSectionSubtitleProps) { - const policyID = `${policy?.id ?? CONST.DEFAULT_NUMBER_ID}`; + const policyID = policy?.id; const handleOnPressCategoriesLink = () => { if (policy?.areCategoriesEnabled) { From f2a9d1989c8663e2773c5de1a71c3f87634d1aeb Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 16 Jan 2025 16:02:23 +0700 Subject: [PATCH 022/145] fix: Nothing happens when clicking on tracking options --- src/libs/ReportUtils.ts | 2 +- src/libs/actions/IOU.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 57951ece5c21..d6d5d5a46226 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8437,7 +8437,7 @@ function createDraftTransactionAndNavigateToParticipantSelector( actionName: IOUAction, reportActionID: string | undefined, ): void { - if (!transactionID || !reportID || !reportActionID) { + if (!transactionID || !reportID) { return; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 0ac396709b07..11282037f8cf 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -225,7 +225,7 @@ type CategorizeTrackedExpenseReportInformation = { moneyRequestPreviewReportActionID: string; moneyRequestReportID: string; moneyRequestCreatedReportActionID: string; - actionableWhisperReportActionID: string; + actionableWhisperReportActionID: string | undefined; linkedTrackedExpenseReportAction: OnyxTypes.ReportAction; linkedTrackedExpenseReportID: string; transactionThreadReportID: string; @@ -4240,7 +4240,7 @@ function trackExpense( switch (action) { case CONST.IOU.ACTION.CATEGORIZE: { - if (!linkedTrackedExpenseReportAction || !actionableWhisperReportActionID || !linkedTrackedExpenseReportID) { + if (!linkedTrackedExpenseReportAction || !linkedTrackedExpenseReportID) { return; } const transactionParams: CategorizeTrackedExpenseTransactionParams = { @@ -4283,7 +4283,7 @@ function trackExpense( break; } case CONST.IOU.ACTION.SHARE: { - if (!linkedTrackedExpenseReportAction || !actionableWhisperReportActionID || !linkedTrackedExpenseReportID) { + if (!linkedTrackedExpenseReportAction || !linkedTrackedExpenseReportID) { return; } shareTrackedExpense( From 81896db9605ede1ce4ad2456938237d431991683 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 16 Jan 2025 16:35:21 +0700 Subject: [PATCH 023/145] fix lint --- .../rules/IndividualExpenseRulesSection.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx index 817355cff5b8..b41e0fe3ef9f 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx @@ -10,11 +10,11 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {openExternalLink} from '@libs/actions/Link'; +import {setWorkspaceEReceiptsEnabled} from '@libs/actions/Policy/Policy'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {ThemeStyles} from '@styles/index'; -import * as Link from '@userActions/Link'; -import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; @@ -92,7 +92,7 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection return ''; } - return CurrencyUtils.convertToDisplayString(policy?.maxExpenseAmountNoReceipt, policyCurrency); + return convertToDisplayString(policy?.maxExpenseAmountNoReceipt, policyCurrency); }, [policy?.maxExpenseAmountNoReceipt, policyCurrency]); const maxExpenseAmountText = useMemo(() => { @@ -100,7 +100,7 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection return ''; } - return CurrencyUtils.convertToDisplayString(policy?.maxExpenseAmount, policyCurrency); + return convertToDisplayString(policy?.maxExpenseAmount, policyCurrency); }, [policy?.maxExpenseAmount, policyCurrency]); const maxExpenseAgeText = useMemo(() => { @@ -180,7 +180,7 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection PolicyActions.setWorkspaceEReceiptsEnabled(policyID, !areEReceiptsEnabled)} + onToggle={() => setWorkspaceEReceiptsEnabled(policyID, !areEReceiptsEnabled)} disabled={policyCurrency !== CONST.CURRENCY.USD} /> @@ -189,7 +189,7 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection {translate('workspace.rules.individualExpenseRules.eReceiptsHint')}{' '} Link.openExternalLink(CONST.DEEP_DIVE_ERECEIPTS)} + onPress={() => openExternalLink(CONST.DEEP_DIVE_ERECEIPTS)} > {translate('workspace.rules.individualExpenseRules.eReceiptsHintLink')} From 332efdf319463b535c92acd4c56b3881d9b06c4b Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 17 Jan 2025 04:45:06 +0530 Subject: [PATCH 024/145] fix merge conflicts. Signed-off-by: krishna2323 --- .../BrokenConnectionDescription.tsx | 4 +- src/components/MoneyRequestHeader.tsx | 8 +-- .../MoneyRequestPreviewContent.tsx | 54 +++++++++---------- .../ReportActionItem/ReportPreview.tsx | 3 +- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index 14265030bf75..90f47feb69ad 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import {isInstantSubmitEnabled, isPolicyAdmin as isPolicyAdminUtil} from '@libs/PolicyUtils'; +import {isInstantSubmitEnabled, isPolicyAdmin as isPolicyAdminPolicyUtils} from '@libs/PolicyUtils'; import {isCurrentUserSubmitter, isProcessingReport, isReportApproved, isReportManuallyReimbursed} from '@libs/ReportUtils'; import {getTransactionViolations} from '@libs/TransactionUtils'; import Navigation from '@navigation/Navigation'; @@ -29,7 +29,7 @@ function BrokenConnectionDescription({transactionID, policy, report}: BrokenConn const brokenConnection530Error = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530); const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION); - const isPolicyAdmin = isPolicyAdminUtil(policy); + const isPolicyAdmin = isPolicyAdminPolicyUtils(policy); if (!brokenConnection530Error && !brokenConnectionError) { return ''; diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 372245070380..68dbd297a177 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -23,7 +23,7 @@ import { isOnHold as isOnHoldUtil, isPending, isReceiptBeingScanned, - shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationUtil, + shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, } from '@libs/TransactionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -77,10 +77,10 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth; + const transactionIDList = transaction ? [transaction.transactionID] : []; + const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDList); - const hasAllPendingRTERViolations = allHavePendingRTERViolation(transaction?.transactionID ? [transaction?.transactionID] : []); - - const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationUtil(transaction?.transactionID, parentReport, policy); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transaction?.transactionID, parentReport, policy); const shouldShowMarkAsCashButton = hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(parentReport?.reportID))); diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index d59f56ec53a8..4147e3e73e7b 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -36,13 +36,13 @@ import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/type import {getAvatarsForAccountIDs} from '@libs/OptionsListUtils'; import {getCleanedTagName, getPolicy} from '@libs/PolicyUtils'; import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; -import {getOriginalMessage, getReportAction, isMessageDeleted, isMoneyRequestAction as isMoneyRequestActionUtil} from '@libs/ReportActionsUtils'; +import {getOriginalMessage, getReportAction, isMessageDeleted, isMoneyRequestAction as isMoneyRequestActionReportActionUtils} from '@libs/ReportActionsUtils'; import { getTransactionDetails, getWorkspaceIcon, isPaidGroupPolicy, isPaidGroupPolicyExpenseReport, - isPolicyExpenseChat as isPolicyExpenseChatUtil, + isPolicyExpenseChat as isPolicyExpenseChatReportUtils, isReportApproved, isSettled as isSettledUtil, } from '@libs/ReportUtils'; @@ -52,17 +52,17 @@ import { compareDuplicateTransactionFields, getTransactionViolations, hasMissingSmartscanFields, - hasNoticeTypeViolation, + hasNoticeTypeViolation as hasNoticeTypeViolationTransactionUtils, hasPendingUI, - hasReceipt as hasReceiptUtil, - hasViolation, - hasWarningTypeViolation, - isAmountMissing as isAmountMissingUtil, - isCardTransaction as isCardTransactionUtil, - isDistanceRequest as isDistanceRequestUtil, - isFetchingWaypointsFromServer as isFetchingWaypointsFromServerUtil, - isMerchantMissing as isMerchantMissingUtil, - isOnHold as isOnHoldUtil, + hasReceipt as hasReceiptTransactionUtils, + hasViolation as hasViolationTransactionUtils, + hasWarningTypeViolation as hasWarningTypeViolationTransactionUtils, + isAmountMissing as isAmountMissingTransactionUtils, + isCardTransaction as isCardTransactionTransactionUtils, + isDistanceRequest as isDistanceRequestTransactionUtils, + isFetchingWaypointsFromServer as isFetchingWaypointsFromServerTransactionUtils, + isMerchantMissing as isMerchantMissingTransactionUtils, + isOnHold as isOnHoldTransactionUtils, isPending, isReceiptBeingScanned, removeSettledAndApprovedTransactions, @@ -106,7 +106,7 @@ function MoneyRequestPreviewContent({ const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || CONST.DEFAULT_NUMBER_ID}`); const policy = getPolicy(iouReport?.policyID); - const isMoneyRequestAction = isMoneyRequestActionUtil(action); + const isMoneyRequestAction = isMoneyRequestActionReportActionUtils(action); const transactionID = isMoneyRequestAction ? getOriginalMessage(action)?.IOUTransactionID : undefined; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS); @@ -116,9 +116,9 @@ function MoneyRequestPreviewContent({ const sessionAccountID = session?.accountID; const managerID = iouReport?.managerID ?? CONST.DEFAULT_NUMBER_ID; const ownerAccountID = iouReport?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; - const isPolicyExpenseChat = isPolicyExpenseChatUtil(chatReport); + const isPolicyExpenseChat = isPolicyExpenseChatReportUtils(chatReport); - const participantAccountIDs = isMoneyRequestActionUtil(action) && isBillSplit ? getOriginalMessage(action)?.participantAccountIDs ?? [] : [managerID, ownerAccountID]; + const participantAccountIDs = isMoneyRequestActionReportActionUtils(action) && isBillSplit ? getOriginalMessage(action)?.participantAccountIDs ?? [] : [managerID, ownerAccountID]; const participantAvatars = getAvatarsForAccountIDs(participantAccountIDs, personalDetails ?? {}); const sortedParticipantAvatars = lodashSortBy(participantAvatars, (avatar) => avatar.id); if (isPolicyExpenseChat && isBillSplit) { @@ -139,18 +139,18 @@ function MoneyRequestPreviewContent({ const description = truncate(StringUtils.lineBreaksToSpaces(requestComment), {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); - const hasReceipt = hasReceiptUtil(transaction); + const hasReceipt = hasReceiptTransactionUtils(transaction); const isScanning = hasReceipt && isReceiptBeingScanned(transaction); - const isOnHold = isOnHoldUtil(transaction); + const isOnHold = isOnHoldTransactionUtils(transaction); const isSettlementOrApprovalPartial = !!iouReport?.pendingFields?.partial; const isPartialHold = isSettlementOrApprovalPartial && isOnHold; - const hasViolations = hasViolation(transaction?.transactionID, allViolations, true); - const hasNoticeTypeViolations = hasNoticeTypeViolation(transaction?.transactionID, allViolations, true) && isPaidGroupPolicy(iouReport); - const hasWarningTypeViolations = hasWarningTypeViolation(transaction?.transactionID, allViolations, true); + const hasViolations = hasViolationTransactionUtils(transaction?.transactionID, allViolations, true); + const hasNoticeTypeViolations = hasNoticeTypeViolationTransactionUtils(transaction?.transactionID, allViolations, true) && isPaidGroupPolicy(iouReport); + const hasWarningTypeViolations = hasWarningTypeViolationTransactionUtils(transaction?.transactionID, allViolations, true); const hasFieldErrors = hasMissingSmartscanFields(transaction); - const isDistanceRequest = isDistanceRequestUtil(transaction); - const isFetchingWaypointsFromServer = isFetchingWaypointsFromServerUtil(transaction); - const isCardTransaction = isCardTransactionUtil(transaction); + const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); + const isFetchingWaypointsFromServer = isFetchingWaypointsFromServerTransactionUtils(transaction); + const isCardTransaction = isCardTransactionTransactionUtils(transaction); const isSettled = isSettledUtil(iouReport?.reportID); const isApproved = isReportApproved(iouReport); const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; @@ -180,7 +180,7 @@ function MoneyRequestPreviewContent({ const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`); const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); - const reviewingTransactionID = isMoneyRequestActionUtil(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined; + const reviewingTransactionID = isMoneyRequestActionReportActionUtils(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined; /* Show the merchant for IOUs and expenses only if: @@ -247,8 +247,8 @@ function MoneyRequestPreviewContent({ return `${message} ${CONST.DOT_SEPARATOR} ${isTooLong || hasViolationsAndFieldErrors ? translate('violations.reviewRequired') : violationMessage}`; } if (hasFieldErrors) { - const isMerchantMissing = isMerchantMissingUtil(transaction); - const isAmountMissing = isAmountMissingUtil(transaction); + const isMerchantMissing = isMerchantMissingTransactionUtils(transaction); + const isAmountMissing = isAmountMissingTransactionUtils(transaction); if (isAmountMissing && isMerchantMissing) { message += ` ${CONST.DOT_SEPARATOR} ${translate('violations.reviewRequired')}`; } else if (isAmountMissing) { @@ -301,7 +301,7 @@ function MoneyRequestPreviewContent({ }; const getDisplayDeleteAmountText = (): string => { - const iouOriginalMessage: OnyxEntry = isMoneyRequestActionUtil(action) ? getOriginalMessage(action) ?? undefined : undefined; + const iouOriginalMessage: OnyxEntry = isMoneyRequestActionReportActionUtils(action) ? getOriginalMessage(action) ?? undefined : undefined; return convertToDisplayString(iouOriginalMessage?.amount, iouOriginalMessage?.currency); }; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 796fbd60a9df..6c9219ec05c5 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -238,7 +238,8 @@ function ReportPreview({ const lastThreeTransactions = allTransactions.slice(-3); const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...getThumbnailAndImageURIs(transaction), transaction})); const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(allTransactions.at(0), getTransactionViolations(allTransactions.at(0)?.transactionID)); - const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(allTransactions.at(0)?.transactionID, iouReport, policy); + const transactionIDList = [allTransactions.at(0)?.transactionID].filter((transactionID): transactionID is string => transactionID !== undefined); + const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(transactionIDList, iouReport, policy); let formattedMerchant = numberOfRequests === 1 ? getMerchant(allTransactions.at(0)) : null; const formattedDescription = numberOfRequests === 1 ? getDescription(allTransactions.at(0)) : null; From 1ef4be56b9114ae44ba7a011db5f14bf7a57322e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 17 Jan 2025 04:49:07 +0530 Subject: [PATCH 025/145] minor import fix. Signed-off-by: krishna2323 --- .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index 4147e3e73e7b..510d1bec903c 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -44,7 +44,7 @@ import { isPaidGroupPolicyExpenseReport, isPolicyExpenseChat as isPolicyExpenseChatReportUtils, isReportApproved, - isSettled as isSettledUtil, + isSettled as isSettledReportUtils, } from '@libs/ReportUtils'; import type {TransactionDetails} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -151,7 +151,7 @@ function MoneyRequestPreviewContent({ const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const isFetchingWaypointsFromServer = isFetchingWaypointsFromServerTransactionUtils(transaction); const isCardTransaction = isCardTransactionTransactionUtils(transaction); - const isSettled = isSettledUtil(iouReport?.reportID); + const isSettled = isSettledReportUtils(iouReport?.reportID); const isApproved = isReportApproved(iouReport); const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; const isReviewDuplicateTransactionPage = route.name === SCREENS.TRANSACTION_DUPLICATE.REVIEW; @@ -258,9 +258,9 @@ function MoneyRequestPreviewContent({ } return message; } - } else if (hasNoticeTypeViolations && transaction && !isReportApproved(iouReport) && !isSettledUtil(iouReport?.reportID)) { + } else if (hasNoticeTypeViolations && transaction && !isReportApproved(iouReport) && !isSettledReportUtils(iouReport?.reportID)) { message += ` • ${translate('violations.reviewRequired')}`; - } else if (isPaidGroupPolicyExpenseReport(iouReport) && isReportApproved(iouReport) && !isSettledUtil(iouReport?.reportID) && !isPartialHold) { + } else if (isPaidGroupPolicyExpenseReport(iouReport) && isReportApproved(iouReport) && !isSettledReportUtils(iouReport?.reportID) && !isPartialHold) { message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.approved')}`; } else if (iouReport?.isCancelledIOU) { message += ` ${CONST.DOT_SEPARATOR} ${translate('iou.canceled')}`; @@ -410,7 +410,7 @@ function MoneyRequestPreviewContent({ > {displayAmount} - {isSettledUtil(iouReport?.reportID) && !isPartialHold && !isBillSplit && ( + {isSettledReportUtils(iouReport?.reportID) && !isPartialHold && !isBillSplit && ( Date: Fri, 17 Jan 2025 05:03:23 +0530 Subject: [PATCH 026/145] remove duplicate imports. Signed-off-by: krishna2323 --- .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index b89bb61506cf..b3b43aaa6e80 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -23,9 +23,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {clearWalletTermsError} from '@libs/actions/PaymentMethods'; -import {clearIOUError} from '@libs/actions/Report'; -import {abandonReviewDuplicateTransactions, setReviewDuplicatesKey} from '@libs/actions/Transaction'; import ControlSelection from '@libs/ControlSelection'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; From 05747b2d83c33053cd718e831d8e672f6032b106 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 17 Jan 2025 05:07:30 +0530 Subject: [PATCH 027/145] minor fix. Signed-off-by: krishna2323 --- src/components/BrokenConnectionDescription.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/BrokenConnectionDescription.tsx b/src/components/BrokenConnectionDescription.tsx index f3a5a3db51b1..ed5ecf41078a 100644 --- a/src/components/BrokenConnectionDescription.tsx +++ b/src/components/BrokenConnectionDescription.tsx @@ -13,8 +13,7 @@ import TextLink from './TextLink'; type BrokenConnectionDescriptionProps = { /** Transaction id of the corresponding report */ - transactionID: string | undefined;https://github.com/Expensify/App/pull/54455/conflicts - + transactionID: string | undefined; /** Current report */ report: OnyxEntry; From d87095684e03a9a912187543f93a3b6ceedde167 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 17 Jan 2025 05:22:32 +0530 Subject: [PATCH 028/145] fix typescript issue. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 2 +- src/libs/SearchUIUtils.ts | 2 +- src/libs/TransactionUtils/index.ts | 9 ++------- src/libs/actions/IOU.ts | 11 +++-------- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 7882308044fc..7a1b08b99378 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -242,7 +242,7 @@ function ReportPreview({ const isArchived = isArchivedReport(iouReport); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const shouldShowSubmitButton = canSubmitReport(iouReport, policy, transactionIDList, transactionViolations); + const shouldShowSubmitButton = canSubmitReport(iouReport, policy, transactionIDList); const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(iouReport); diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 58dcf4932b71..f8441c274940 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -341,7 +341,7 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr // We check for isAllowedToApproveExpenseReport because if the policy has preventSelfApprovals enabled, we disable the Submit action and in that case we want to show the View action instead const transactionIDList = allReportTransactions.map((reportTransaction) => reportTransaction.transactionID); - if (canSubmitReport(report, policy, transactionIDList, allViolations) && isAllowedToApproveExpenseReport) { + if (canSubmitReport(report, policy, transactionIDList) && isAllowedToApproveExpenseReport) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 93aa6b920961..8846de0f067c 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -791,13 +791,8 @@ function hasBrokenConnectionViolation(transactionID?: string): boolean { /** * Check if user should see broken connection violation warning. */ -function shouldShowBrokenConnectionViolation( - transactionIDList: string[] | undefined, - report: OnyxEntry | SearchReport, - policy: OnyxEntry | SearchPolicy, - allViolations?: OnyxCollection, -): boolean { - const transactionsWithBrokenConnectionViolation = transactionIDList?.map((transactionID) => hasBrokenConnectionViolation(transactionID, allViolations)) ?? []; +function shouldShowBrokenConnectionViolation(transactionIDList: string[] | undefined, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean { + const transactionsWithBrokenConnectionViolation = transactionIDList?.map((transactionID) => hasBrokenConnectionViolation(transactionID)) ?? []; return ( transactionsWithBrokenConnectionViolation.length > 0 && transactionsWithBrokenConnectionViolation?.some((value) => value === true) && diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index e757ea4c0128..212f7af0d8d3 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7446,19 +7446,14 @@ function canIOUBePaid( ); } -function canSubmitReport( - report: OnyxEntry | SearchReport, - policy: OnyxEntry | SearchPolicy, - transactionIDList: string[], - allViolations?: OnyxCollection, -) { +function canSubmitReport(report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy, transactionIDList: string[]) { const currentUserAccountID = getCurrentUserAccountID(); const isOpenExpenseReport = isOpenExpenseReportReportUtils(report); const isArchived = isArchivedReport(report); const {reimbursableSpend} = getMoneyRequestSpendBreakdown(report); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDList, allViolations); - const hasBrokenConnectionViolation = shouldShowBrokenConnectionViolation(transactionIDList, report, policy, allViolations); + const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDList); + const hasBrokenConnectionViolation = shouldShowBrokenConnectionViolation(transactionIDList, report, policy); return ( isOpenExpenseReport && From 89ecbf545feb74b14caa39171a5a3cd6efc9756f Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Fri, 17 Jan 2025 11:32:00 +0700 Subject: [PATCH 029/145] fix lint --- src/libs/API/parameters/CategorizeTrackedExpenseParams.ts | 2 +- src/libs/API/parameters/ShareTrackedExpenseParams.ts | 2 +- src/libs/actions/IOU.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts index 78eb0adecc5e..149124cd7cae 100644 --- a/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts +++ b/src/libs/API/parameters/CategorizeTrackedExpenseParams.ts @@ -11,7 +11,7 @@ type CategorizeTrackedExpenseParams = { moneyRequestPreviewReportActionID: string; moneyRequestReportID: string; moneyRequestCreatedReportActionID: string; - actionableWhisperReportActionID: string; + actionableWhisperReportActionID?: string; modifiedExpenseReportActionID: string; reportPreviewReportActionID: string; category?: string; diff --git a/src/libs/API/parameters/ShareTrackedExpenseParams.ts b/src/libs/API/parameters/ShareTrackedExpenseParams.ts index cee4bc40d9ac..96f5345885fe 100644 --- a/src/libs/API/parameters/ShareTrackedExpenseParams.ts +++ b/src/libs/API/parameters/ShareTrackedExpenseParams.ts @@ -11,7 +11,7 @@ type ShareTrackedExpenseParams = { moneyRequestPreviewReportActionID: string; moneyRequestReportID: string; moneyRequestCreatedReportActionID: string; - actionableWhisperReportActionID: string; + actionableWhisperReportActionID?: string; modifiedExpenseReportActionID: string; reportPreviewReportActionID: string; category?: string; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 11282037f8cf..20f25feb82fb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3723,7 +3723,7 @@ function updateMoneyRequestDistanceRate( const getConvertTrackedExpenseInformation = ( transactionID: string, - actionableWhisperReportActionID: string, + actionableWhisperReportActionID: string | undefined, moneyRequestReportID: string, linkedTrackedExpenseReportAction: OnyxTypes.ReportAction, linkedTrackedExpenseReportID: string, @@ -3893,7 +3893,7 @@ function shareTrackedExpense( moneyRequestPreviewReportActionID: string, moneyRequestReportID: string, moneyRequestCreatedReportActionID: string, - actionableWhisperReportActionID: string, + actionableWhisperReportActionID: string | undefined, linkedTrackedExpenseReportAction: OnyxTypes.ReportAction, linkedTrackedExpenseReportID: string, transactionThreadReportID: string, From 277488056debb1454cac1fbcdb349317effda20c Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Mon, 20 Jan 2025 09:40:10 +0530 Subject: [PATCH 030/145] minor updates. Signed-off-by: krishna2323 --- .../MoneyRequestPreview/MoneyRequestPreviewContent.tsx | 7 +++---- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- src/libs/ReportActionsUtils.ts | 2 +- src/libs/TransactionUtils/index.ts | 8 +++++++- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index b3b43aaa6e80..017c6b6b15f5 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -233,14 +233,13 @@ function MoneyRequestPreviewContent({ } if (shouldShowRBR && transaction) { - const violations = getTransactionViolations(transaction.transactionID); if (shouldShowHoldMessage) { return `${message} ${CONST.DOT_SEPARATOR} ${translate('violations.hold')}`; } - const firstViolation = violations?.at(0); + const firstViolation = transactionViolations?.at(0); if (firstViolation) { const violationMessage = ViolationsUtils.getViolationTranslation(firstViolation, translate); - const violationsCount = violations?.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length ?? 0; + const violationsCount = transactionViolations?.filter((v) => v.type === CONST.VIOLATION_TYPES.VIOLATION).length ?? 0; const isTooLong = violationsCount > 1 || violationMessage.length > 15; const hasViolationsAndFieldErrors = violationsCount > 0 && hasFieldErrors; @@ -280,7 +279,7 @@ function MoneyRequestPreviewContent({ if (shouldShowBrokenConnectionViolation(transaction ? [transaction.transactionID] : [], iouReport, policy)) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('violations.brokenConnection530Error')}; } - if (hasPendingUI(transaction, getTransactionViolations(transaction?.transactionID))) { + if (hasPendingUI(transaction, transactionViolations)) { return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')}; } return {shouldShow: false}; diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 513f33d1a78c..10eb80a09881 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -127,7 +127,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals }); const transactionViolations = getTransactionViolations(getTransactionID(report, parentReportActions) ?? undefined); - const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? CONST.DEFAULT_NUMBER_ID]; + const parentReportAction = report?.parentReportActionID ? parentReportActions?.[report?.parentReportActionID] : undefined; const isTrackExpense = isTrackExpenseReport(report); const moneyRequestReport = parentReport; const linkedTransactionID = useMemo(() => { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index c105908396ca..cb9d0106926f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1603,7 +1603,7 @@ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry { if (!reportID || !transactionID) { - return; + return undefined; } const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const reportActions = getAllReportActions(report?.reportID); diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 8846de0f067c..bb0b487215c3 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -760,7 +760,7 @@ function getTransactionViolations(transactionID: string | undefined): Transactio if (!transactionID) { return null; } - return allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((v) => !isViolationDismissed(transactionID, v)) ?? null; + return allTransactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.filter((violation) => !isViolationDismissed(transactionID, violation)) ?? null; } /** @@ -971,6 +971,9 @@ function hasViolation(transactionID: string | undefined, transactionViolations: * Checks if any violations for the provided transaction are of type 'notice' */ function hasNoticeTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { + if (!transactionID) { + return false; + } return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some( (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.NOTICE && @@ -983,6 +986,9 @@ function hasNoticeTypeViolation(transactionID: string | undefined, transactionVi * Checks if any violations for the provided transaction are of type 'warning' */ function hasWarningTypeViolation(transactionID: string | undefined, transactionViolations: OnyxCollection, showInReview?: boolean): boolean { + if (!transactionID) { + return false; + } const violations = transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]; const warningTypeViolations = violations?.filter( From 078b3fc38efb51db9f9e4430d6ac9ed307b1b14d Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Mon, 20 Jan 2025 09:49:54 +0530 Subject: [PATCH 031/145] fix merge conflicts. Signed-off-by: krishna2323 --- .../ReportActionItem/MoneyRequestView.tsx | 38 +++++++++---------- src/libs/TransactionUtils/index.ts | 12 ++++++ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 10eb80a09881..4299ba2605ed 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -31,7 +31,7 @@ import {getOriginalMessage, isMoneyRequestAction, isPayAction} from '@libs/Repor import { canEditFieldOfMoneyRequest, canEditMoneyRequest, - canUserPerformWriteAction as canUserPerformWriteActionUtil, + canUserPerformWriteAction as canUserPerformWriteActionReportUtils, getAddWorkspaceRoomOrChatReportErrors, getTransactionDetails, getTripIDFromTransactionParentReportID, @@ -40,13 +40,13 @@ import { isPaidGroupPolicy, isReportApproved, isReportInGroupPolicy, - isSettled as isSettledUtil, + isSettled as isSettledReportUtils, isTrackExpenseReport, } from '@libs/ReportUtils'; import type {TransactionDetails} from '@libs/ReportUtils'; import {hasEnabledTags} from '@libs/TagsOptionsListUtils'; import { - didReceiptScanSucceed as didReceiptScanSucceedUtil, + didReceiptScanSucceed as didReceiptScanSucceedTransactionUtils, getBillable, getCardName, getDescription, @@ -55,13 +55,13 @@ import { getTaxName, getTransactionViolations, hasMissingSmartscanFields, - hasReceipt as hasReceiptUtil, + hasReceipt as hasReceiptTransactionUtils, hasReservationList, - hasRoute as hasRouteUtil, - isCardTransaction as isCardTransactionUtil, - isDistanceRequest as isDistanceRequestUtil, - isReceiptBeingScanned as isReceiptBeingScannedUtil, - shouldShowAttendees as shouldShowAttendeesUtil, + hasRoute as hasRouteTransactionUtils, + isCardTransaction as isCardTransactionTransactionUtils, + isDistanceRequest as isDistanceRequestTransactionUtils, + isReceiptBeingScanned as isReceiptBeingScannedTransactionUtils, + shouldShowAttendees as shouldShowAttendeesTransactionUtils, } from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; @@ -154,11 +154,11 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals postedDate: transactionPostedDate, } = useMemo>(() => getTransactionDetails(transaction) ?? {}, [transaction]); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const isDistanceRequest = isDistanceRequestUtil(transaction); + const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const formattedTransactionAmount = transactionAmount ? convertToDisplayString(transactionAmount, transactionCurrency) : ''; const formattedPerAttendeeAmount = transactionAmount ? convertToDisplayString(transactionAmount / (transactionAttendees?.length ?? 1), transactionCurrency) : ''; const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); - const isCardTransaction = isCardTransactionUtil(transaction); + const isCardTransaction = isCardTransactionTransactionUtils(transaction); const cardProgramName = getCardName(transaction); const shouldShowCard = isCardTransaction && cardProgramName; const isApproved = isReportApproved(moneyRequestReport); @@ -172,12 +172,12 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const taxRatesDescription = taxRates?.name; const taxRateTitle = updatedTransaction ? getTaxName(policy, updatedTransaction) : getTaxName(policy, transaction); - const isSettled = isSettledUtil(moneyRequestReport?.reportID); + const isSettled = isSettledReportUtils(moneyRequestReport?.reportID); const isCancelled = moneyRequestReport && moneyRequestReport?.isCancelledIOU; // Flags for allowing or disallowing editing an expense // Used for non-restricted fields such as: description, category, tag, billable, etc... - const canUserPerformWriteAction = !!canUserPerformWriteActionUtil(report) && !readonly; + const canUserPerformWriteAction = !!canUserPerformWriteActionReportUtils(report) && !readonly; const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, transaction) && canUserPerformWriteAction; const canEditTaxFields = canEdit && !isDistanceRequest; @@ -185,9 +185,9 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const canEditMerchant = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.MERCHANT); const canEditDate = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE); const canEditReceipt = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT); - const hasReceipt = hasReceiptUtil(updatedTransaction ?? transaction); - const isReceiptBeingScanned = hasReceipt && isReceiptBeingScannedUtil(updatedTransaction ?? transaction); - const didReceiptScanSucceed = hasReceipt && didReceiptScanSucceedUtil(transaction); + const hasReceipt = hasReceiptTransactionUtils(updatedTransaction ?? transaction); + const isReceiptBeingScanned = hasReceipt && isReceiptBeingScannedTransactionUtils(updatedTransaction ?? transaction); + const didReceiptScanSucceed = hasReceipt && didReceiptScanSucceedTransactionUtils(transaction); const canEditDistance = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE); const canEditDistanceRate = canUserPerformWriteAction && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE_RATE); @@ -213,7 +213,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const shouldShowTag = isPolicyExpenseChat && (transactionTag || hasEnabledTags(policyTagLists)); const shouldShowBillable = isPolicyExpenseChat && (!!transactionBillable || !(policy?.disabledFields?.defaultBillable ?? true) || !!updatedTransaction?.billable); - const shouldShowAttendees = useMemo(() => shouldShowAttendeesUtil(iouType, policy), [iouType, policy]); + const shouldShowAttendees = useMemo(() => shouldShowAttendeesTransactionUtils(iouType, policy), [iouType, policy]); const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest); const tripID = getTripIDFromTransactionParentReportID(parentReport?.parentReportID); @@ -229,7 +229,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals let amountDescription = `${translate('iou.amount')}`; let dateDescription = `${translate('common.date')}`; - const hasRoute = hasRouteUtil(transactionBackup ?? transaction, isDistanceRequest); + const hasRoute = hasRouteTransactionUtils(transactionBackup ?? transaction, isDistanceRequest); const {unit, rate} = DistanceRequestUtils.getRate({transaction, policy}); const distance = getDistanceInMeters(transactionBackup ?? transaction, unit); const currency = transactionCurrency ?? CONST.CURRENCY.USD; @@ -237,7 +237,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); let merchantTitle = isEmptyMerchant ? '' : transactionMerchant; let amountTitle = formattedTransactionAmount ? formattedTransactionAmount.toString() : ''; - if (hasReceiptUtil(transaction) && isReceiptBeingScannedUtil(transaction)) { + if (hasReceiptTransactionUtils(transaction) && isReceiptBeingScannedTransactionUtils(transaction)) { merchantTitle = translate('iou.receiptStatusTitle'); amountTitle = translate('iou.receiptStatusTitle'); } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index bb0b487215c3..484f972299d2 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -959,6 +959,10 @@ function hasViolation(transactionID: string | undefined, transactionViolations: if (!transactionID) { return false; } + const transaction = getTransaction(transactionID); + if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { + return false; + } return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some( (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.VIOLATION && @@ -974,6 +978,10 @@ function hasNoticeTypeViolation(transactionID: string | undefined, transactionVi if (!transactionID) { return false; } + const transaction = getTransaction(transactionID); + if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { + return false; + } return !!transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some( (violation: TransactionViolation) => violation.type === CONST.VIOLATION_TYPES.NOTICE && @@ -989,6 +997,10 @@ function hasWarningTypeViolation(transactionID: string | undefined, transactionV if (!transactionID) { return false; } + const transaction = getTransaction(transactionID); + if (isExpensifyCardTransaction(transaction) && isPending(transaction)) { + return false; + } const violations = transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]; const warningTypeViolations = violations?.filter( From a39a2f5d08fb8e0239aa8073fdfef449bc9c0d6e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Mon, 20 Jan 2025 10:02:51 +0530 Subject: [PATCH 032/145] minor import names update. Signed-off-by: krishna2323 --- src/components/MoneyRequestHeader.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 224e342be056..193edaa829d0 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -18,9 +18,9 @@ import { getTransactionViolations, hasPendingRTERViolation, hasReceipt, - isDuplicate as isDuplicateUtil, + isDuplicate as isDuplicateTransactionUtils, isExpensifyCardTransaction, - isOnHold as isOnHoldUtil, + isOnHold as isOnHoldTransactionUtils, isPending, isReceiptBeingScanned, shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, @@ -71,8 +71,8 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const isOnHold = isOnHoldUtil(transaction); - const isDuplicate = isDuplicateUtil(transaction?.transactionID); + const isOnHold = isOnHoldTransactionUtils(transaction); + const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID); const reportID = report?.reportID; const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP; From e45c28514847d6af9b53b11c9c996d3a2c7044e4 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Tue, 21 Jan 2025 12:07:23 +0100 Subject: [PATCH 033/145] fix: clear user select upon changing LHN tab --- .../createCustomBottomTabNavigator/BottomTabBar.tsx | 2 ++ src/libs/clearSelectedText.ts | 7 +++++++ src/pages/home/sidebar/BottomTabAvatar.tsx | 2 ++ 3 files changed, 11 insertions(+) create mode 100644 src/libs/clearSelectedText.ts diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 503b263d7cfa..3f8a3b67ff32 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -32,6 +32,7 @@ import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import DebugTabView from './DebugTabView'; +import clearSelectedText from "@libs/clearSelectedText"; type BottomTabBarProps = { selectedTab: string | undefined; @@ -107,6 +108,7 @@ function BottomTabBar({selectedTab}: BottomTabBarProps) { if (selectedTab === SCREENS.SEARCH.BOTTOM_TAB) { return; } + clearSelectedText(); interceptAnonymousUser(() => { const rootState = navigationRef.getRootState() as State; const lastSearchRoute = rootState.routes.filter((route) => route.name === SCREENS.SEARCH.CENTRAL_PANE).at(-1); diff --git a/src/libs/clearSelectedText.ts b/src/libs/clearSelectedText.ts new file mode 100644 index 000000000000..ad0988c53c3a --- /dev/null +++ b/src/libs/clearSelectedText.ts @@ -0,0 +1,7 @@ +/** Clears text that user selected by double-clicking - + * it's not tied to virtual DOM, so sometimes it has to be cleared manually */ +function clearSelectedText() { + window.getSelection()?.removeAllRanges(); +} + +export default clearSelectedText; diff --git a/src/pages/home/sidebar/BottomTabAvatar.tsx b/src/pages/home/sidebar/BottomTabAvatar.tsx index 32aa1d455b5e..d25bd6613930 100644 --- a/src/pages/home/sidebar/BottomTabAvatar.tsx +++ b/src/pages/home/sidebar/BottomTabAvatar.tsx @@ -5,6 +5,7 @@ import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import clearSelectedText from '@libs/clearSelectedText'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; @@ -36,6 +37,7 @@ function BottomTabAvatar({isCreateMenuOpen = false, isSelected = false}: BottomT return; } + clearSelectedText(); interceptAnonymousUser(() => Navigation.navigate(ROUTES.SETTINGS)); }, [isCreateMenuOpen]); From cab445011886d46266439323bbcb247de5bae13c Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Tue, 21 Jan 2025 12:17:48 +0100 Subject: [PATCH 034/145] fix: prettier, linter --- .../createCustomBottomTabNavigator/BottomTabBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 3f8a3b67ff32..230b8066fb60 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -14,6 +14,7 @@ import useCurrentReportID from '@hooks/useCurrentReportID'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import clearSelectedText from '@libs/clearSelectedText'; import getPlatform from '@libs/getPlatform'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; @@ -32,7 +33,6 @@ import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import DebugTabView from './DebugTabView'; -import clearSelectedText from "@libs/clearSelectedText"; type BottomTabBarProps = { selectedTab: string | undefined; From 5238a97e29cce54bedb18059f5576a86c2a06a74 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Wed, 22 Jan 2025 09:59:51 +0100 Subject: [PATCH 035/145] fix: native crash --- .../createCustomBottomTabNavigator/BottomTabBar.tsx | 2 +- src/libs/clearSelectedText/clearSelectedText.ts | 5 +++++ .../clearSelectedText.website.ts} | 0 src/pages/home/sidebar/BottomTabAvatar.tsx | 2 +- 4 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 src/libs/clearSelectedText/clearSelectedText.ts rename src/libs/{clearSelectedText.ts => clearSelectedText/clearSelectedText.website.ts} (100%) diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx index 230b8066fb60..07d29758d26d 100644 --- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx @@ -14,7 +14,7 @@ import useCurrentReportID from '@hooks/useCurrentReportID'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import clearSelectedText from '@libs/clearSelectedText'; +import clearSelectedText from '@libs/clearSelectedText/clearSelectedText'; import getPlatform from '@libs/getPlatform'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; diff --git a/src/libs/clearSelectedText/clearSelectedText.ts b/src/libs/clearSelectedText/clearSelectedText.ts new file mode 100644 index 000000000000..c4c30835fd74 --- /dev/null +++ b/src/libs/clearSelectedText/clearSelectedText.ts @@ -0,0 +1,5 @@ +function clearSelectedText() { + return {}; +} + +export default clearSelectedText; diff --git a/src/libs/clearSelectedText.ts b/src/libs/clearSelectedText/clearSelectedText.website.ts similarity index 100% rename from src/libs/clearSelectedText.ts rename to src/libs/clearSelectedText/clearSelectedText.website.ts diff --git a/src/pages/home/sidebar/BottomTabAvatar.tsx b/src/pages/home/sidebar/BottomTabAvatar.tsx index d25bd6613930..88ae760531dc 100644 --- a/src/pages/home/sidebar/BottomTabAvatar.tsx +++ b/src/pages/home/sidebar/BottomTabAvatar.tsx @@ -5,7 +5,7 @@ import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import clearSelectedText from '@libs/clearSelectedText'; +import clearSelectedText from '@libs/clearSelectedText/clearSelectedText'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; From 93d33a2b50e74c753181a9cb97da87a90c0a54a2 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Fri, 24 Jan 2025 09:11:57 +0530 Subject: [PATCH 036/145] add unit tests for violation util functions. Signed-off-by: krishna2323 --- src/libs/TransactionUtils/index.ts | 1 + src/types/onyx/Transaction.ts | 2 +- tests/unit/ViolationUtilsTest.ts | 120 +++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 9a83dc53c67e..ec9752fdf9a7 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1484,6 +1484,7 @@ export { getFormattedPostedDate, getCategoryTaxCodeAndAmount, isPerDiemRequest, + isViolationDismissed, }; export type {TransactionChanges}; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 968b9a4dea4b..a573bce74e27 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -82,7 +82,7 @@ type Comment = { splits?: Split[]; /** Violations that were dismissed */ - dismissedViolations?: Record>; + dismissedViolations?: Partial>>; }; /** Model of transaction custom unit */ diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 659240cc0c30..35d6a8034131 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -1,9 +1,12 @@ import {beforeEach} from '@jest/globals'; import Onyx from 'react-native-onyx'; +import {getTransactionViolations, hasWarningTypeViolation, isViolationDismissed} from '@libs/TransactionUtils'; import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyTagLists, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; +import type {TransactionViolationsCollectionDataSet} from '@src/types/onyx/TransactionViolation'; const categoryOutOfPolicyViolation = { name: CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, @@ -30,6 +33,16 @@ const tagOutOfPolicyViolation = { type: CONST.VIOLATION_TYPES.VIOLATION, }; +const smartScanFailedViolation = { + name: CONST.VIOLATIONS.SMARTSCAN_FAILED, + type: CONST.VIOLATION_TYPES.WARNING, +}; + +const duplicatedTransactionViolation = { + name: CONST.VIOLATIONS.DUPLICATED_TRANSACTION, + type: CONST.VIOLATION_TYPES.WARNING, +}; + describe('getViolationsOnyxData', () => { let transaction: Transaction; let transactionViolations: TransactionViolation[]; @@ -349,3 +362,110 @@ describe('getViolationsOnyxData', () => { }); }); }); + +const getFakeTransaction = (transactionID: string, comment?: Transaction['comment']) => ({ + transactionID, + attendees: [{email: 'text@expensify.com'}], + reportID: '1234', + amount: 100, + comment: comment ?? {}, + created: '2023-07-24 13:46:20', + merchant: 'United Airlines', + currency: 'USD', +}); + +const CARLOS_EMAIL = 'cmartins@expensifail.com'; +const CARLOS_ACCOUNT_ID = 1; + +describe('getViolations', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + initialKeyStates: { + [ONYXKEYS.SESSION]: { + email: CARLOS_EMAIL, + accountID: CARLOS_ACCOUNT_ID, + }, + }, + }); + }); + + afterEach(() => Onyx.clear()); + + it('should check if violation is dismissed or not', async () => { + const transaction1 = getFakeTransaction('123', { + dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + }; + + await Onyx.multiSet({...transactionCollectionDataSet}); + + const isSmartScanDismissed = isViolationDismissed(transaction1.transactionID, smartScanFailedViolation); + const isDuplicateViolationDismissed = isViolationDismissed(transaction1.transactionID, duplicatedTransactionViolation); + + expect(isSmartScanDismissed).toBeTruthy(); + expect(isDuplicateViolationDismissed).toBeFalsy(); + }); + + it('should return filtered out dismissed violations', async () => { + const transaction1 = getFakeTransaction('123', { + dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + }; + + const transactionViolationsCollection: TransactionViolationsCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction1.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], + }; + + await Onyx.multiSet({...transactionCollectionDataSet, ...transactionViolationsCollection}); + + const filteredViolations = getTransactionViolations(transaction1.transactionID); + expect(filteredViolations).toEqual([duplicatedTransactionViolation, tagOutOfPolicyViolation]); + }); + + it('should return filtered out dismissed violations', async () => { + const transaction1 = getFakeTransaction('123', { + dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + }; + + const transactionViolationsCollection: TransactionViolationsCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction1.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], + }; + + await Onyx.multiSet({...transactionCollectionDataSet, ...transactionViolationsCollection}); + + // Should filter out the smartScanFailedViolation + const filteredViolations = getTransactionViolations(transaction1.transactionID); + expect(filteredViolations).toEqual([duplicatedTransactionViolation, tagOutOfPolicyViolation]); + }); + + it('checks if transaction has notice type violation after filtering dismissed violations', async () => { + const transaction1 = getFakeTransaction('123', { + dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, + }); + + const transactionCollectionDataSet: TransactionCollectionDataSet = { + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + }; + + const transactionViolationsCollection = { + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction1.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], + }; + + await Onyx.multiSet({...transactionCollectionDataSet}); + + // Should filter out the smartScanFailedViolation and return true, duplicatedTransactionViolation is a warning type violation but it's not considered in hasWarningTypeViolation + const hasWarningTypeViolationRes = hasWarningTypeViolation(transaction1.transactionID, transactionViolationsCollection); + expect(hasWarningTypeViolationRes).toBeFalsy(); + }); +}); From 8aa6ef02d31ac52ac45fcdb9d66890c53d56f99e Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:37:44 +1300 Subject: [PATCH 037/145] Bump react-native-live-markdown to 0.1.223 --- ios/Podfile.lock | 8 ++++---- package-lock.json | 9 ++++----- package.json | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c674513b9f73..f23b5cdb4e85 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2491,7 +2491,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.210): + - RNLiveMarkdown (0.1.223): - DoubleConversion - glog - hermes-engine @@ -2511,10 +2511,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.210) + - RNLiveMarkdown/newarch (= 0.1.223) - RNReanimated/worklets - Yoga - - RNLiveMarkdown/newarch (0.1.210): + - RNLiveMarkdown/newarch (0.1.223): - DoubleConversion - glog - hermes-engine @@ -3383,7 +3383,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 364e6862a112045bb5c5d35601f0bdb0304af979 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 19826569be35bada5c0f21a0c48b5bc780051501 + RNLiveMarkdown: 5c76c659b125006ff525a095b65184ecb72392f3 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: d184c8d3213acf4c97ec71fbbb6f9d4954552d80 RNPermissions: 0b1429b55af59d1d08b75a8be2459f65a8ac3f28 diff --git a/package-lock.json b/package-lock.json index 82299434a3b1..23670af1fbe4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "@dotlottie/react-player": "^1.6.3", "@expensify/react-native-background-task": "file:./modules/background-task", - "@expensify/react-native-live-markdown": "0.1.210", + "@expensify/react-native-live-markdown": "0.1.223", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -3641,10 +3641,9 @@ "link": true }, "node_modules/@expensify/react-native-live-markdown": { - "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, + "version": "0.1.223", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.223.tgz", + "integrity": "sha512-rE5cQ9lBDP2tqtR4Tta3PNx2i5K83sdht1meYMvmLPqFVy7C9A743wzZe6oudVnhSDem8MbU4NMJStadp9xn6Q==", "license": "MIT", "workspaces": [ "./example", diff --git a/package.json b/package.json index 393f670098fa..cc58de81b21e 100644 --- a/package.json +++ b/package.json @@ -76,8 +76,8 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.210", "@expensify/react-native-background-task": "file:./modules/background-task", + "@expensify/react-native-live-markdown": "0.1.223", "@expo/metro-runtime": "^4.0.0", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", From 27a285151065760b31243c30b6e2439aae0dc71c Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 27 Jan 2025 11:41:03 -0700 Subject: [PATCH 038/145] Update code, add debug --- .github/workflows/verifyHybridApp.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index b3db2c37d4d7..3cfa31ee441c 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -1,7 +1,6 @@ name: Verify HybridApp build on: - workflow_call: pull_request: types: [opened, synchronize] branches-ignore: [staging, production] @@ -18,6 +17,8 @@ on: - 'android/AndroidManifest.xml' - 'ios/Podfile.lock' - 'ios/project.pbxproj' + # TODO: Remove, just here for debugging + - '*.yml' concurrency: group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-verify-main @@ -34,12 +35,11 @@ jobs: submodules: true ref: ${{ github.event.pull_request.head.sha }} token: ${{ secrets.OS_BOTIFY_TOKEN }} - # fetch-depth: 0 is required in order to fetch the correct submodule branch - fetch-depth: 0 - name: Update submodule to match main run: | git submodule update --init --remote + cd Mobile-Expensify git fetch git checkout main @@ -71,12 +71,11 @@ jobs: submodules: true ref: ${{ github.event.pull_request.head.sha }} token: ${{ secrets.OS_BOTIFY_TOKEN }} - # fetch-depth: 0 is required in order to fetch the correct submodule branch - fetch-depth: 0 - name: Update submodule to match main run: | git submodule update --init --remote + cd Mobile-Expensify git fetch git checkout main From 463e3ebb2db96317081273fe3c1dc4fbdd418559 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 27 Jan 2025 11:42:21 -0700 Subject: [PATCH 039/145] Tweak debug --- .github/workflows/verifyHybridApp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 3cfa31ee441c..80211d83a71c 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -18,7 +18,7 @@ on: - 'ios/Podfile.lock' - 'ios/project.pbxproj' # TODO: Remove, just here for debugging - - '*.yml' + - '**.yml' concurrency: group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-verify-main From 4cf0923cd3523ecf792750afb2f686501c4af760 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Tue, 28 Jan 2025 08:11:30 +0700 Subject: [PATCH 040/145] fix: pause lottie animation on mweb to prevent memory leak --- src/components/Lottie/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx index a6b1374b1c8f..974fa60979f2 100644 --- a/src/components/Lottie/index.tsx +++ b/src/components/Lottie/index.tsx @@ -9,6 +9,7 @@ import useAppState from '@hooks/useAppState'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; +import {isMobile} from '@libs/Browser'; import isSideModalNavigator from '@libs/Navigation/isSideModalNavigator'; import CONST from '@src/CONST'; import {useSplashScreenStateContext} from '@src/SplashScreenStateContext'; @@ -74,7 +75,7 @@ function Lottie({source, webStyle, shouldLoadAfterInteractions, ...props}: Props const unsubscribeNavigationBlur = navigator.addListener('blur', () => { const state = navigationContainerRef.getRootState(); const targetRouteName = state?.routes?.[state?.index ?? 0]?.name; - if (!isSideModalNavigator(targetRouteName)) { + if (!isSideModalNavigator(targetRouteName) || isMobile()) { setHasNavigatedAway(true); } }); From 82d5a6b075cd9d7f54e9bf326fae9fd095e9562c Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Tue, 28 Jan 2025 08:15:54 +0700 Subject: [PATCH 041/145] fix: eslint fail --- src/components/Lottie/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/Lottie/index.tsx b/src/components/Lottie/index.tsx index 974fa60979f2..9fb61ea99f75 100644 --- a/src/components/Lottie/index.tsx +++ b/src/components/Lottie/index.tsx @@ -8,8 +8,7 @@ import type DotLottieAnimation from '@components/LottieAnimations/types'; import useAppState from '@hooks/useAppState'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Browser from '@libs/Browser'; -import {isMobile} from '@libs/Browser'; +import {getBrowser, isMobile} from '@libs/Browser'; import isSideModalNavigator from '@libs/Navigation/isSideModalNavigator'; import CONST from '@src/CONST'; import {useSplashScreenStateContext} from '@src/SplashScreenStateContext'; @@ -52,7 +51,7 @@ function Lottie({source, webStyle, shouldLoadAfterInteractions, ...props}: Props const aspectRatioStyle = styles.aspectRatioLottie(source); - const browser = Browser.getBrowser(); + const browser = getBrowser(); const [hasNavigatedAway, setHasNavigatedAway] = React.useState(false); const navigationContainerRef = useContext(NavigationContainerRefContext); const navigator = useContext(NavigationContext); From 156a3aac5cb7f86979193651bebcba13ba6a745e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 28 Jan 2025 14:09:42 +0530 Subject: [PATCH 042/145] minor NAB fixes. Signed-off-by: krishna2323 --- tests/unit/ViolationUtilsTest.ts | 45 +++++++++----------------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index 35d6a8034131..b161ea8fe40c 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -393,79 +393,60 @@ describe('getViolations', () => { afterEach(() => Onyx.clear()); it('should check if violation is dismissed or not', async () => { - const transaction1 = getFakeTransaction('123', { + const transaction = getFakeTransaction('123', { dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, }); const transactionCollectionDataSet: TransactionCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, }; await Onyx.multiSet({...transactionCollectionDataSet}); - const isSmartScanDismissed = isViolationDismissed(transaction1.transactionID, smartScanFailedViolation); - const isDuplicateViolationDismissed = isViolationDismissed(transaction1.transactionID, duplicatedTransactionViolation); + const isSmartScanDismissed = isViolationDismissed(transaction.transactionID, smartScanFailedViolation); + const isDuplicateViolationDismissed = isViolationDismissed(transaction.transactionID, duplicatedTransactionViolation); expect(isSmartScanDismissed).toBeTruthy(); expect(isDuplicateViolationDismissed).toBeFalsy(); }); it('should return filtered out dismissed violations', async () => { - const transaction1 = getFakeTransaction('123', { + const transaction = getFakeTransaction('123', { dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, }); const transactionCollectionDataSet: TransactionCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, }; const transactionViolationsCollection: TransactionViolationsCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction1.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], - }; - - await Onyx.multiSet({...transactionCollectionDataSet, ...transactionViolationsCollection}); - - const filteredViolations = getTransactionViolations(transaction1.transactionID); - expect(filteredViolations).toEqual([duplicatedTransactionViolation, tagOutOfPolicyViolation]); - }); - - it('should return filtered out dismissed violations', async () => { - const transaction1 = getFakeTransaction('123', { - dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, - }); - - const transactionCollectionDataSet: TransactionCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, - }; - - const transactionViolationsCollection: TransactionViolationsCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction1.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], }; await Onyx.multiSet({...transactionCollectionDataSet, ...transactionViolationsCollection}); // Should filter out the smartScanFailedViolation - const filteredViolations = getTransactionViolations(transaction1.transactionID); + const filteredViolations = getTransactionViolations(transaction.transactionID); expect(filteredViolations).toEqual([duplicatedTransactionViolation, tagOutOfPolicyViolation]); }); - it('checks if transaction has notice type violation after filtering dismissed violations', async () => { - const transaction1 = getFakeTransaction('123', { + it('checks if transaction has warning type violation after filtering dismissed violations', async () => { + const transaction = getFakeTransaction('123', { dismissedViolations: {smartscanFailed: {[CARLOS_EMAIL]: CARLOS_ACCOUNT_ID.toString()}}, }); const transactionCollectionDataSet: TransactionCollectionDataSet = { - [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction1.transactionID}`]: transaction1, + [`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`]: transaction, }; const transactionViolationsCollection = { - [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction1.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], + [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`]: [duplicatedTransactionViolation, smartScanFailedViolation, tagOutOfPolicyViolation], }; await Onyx.multiSet({...transactionCollectionDataSet}); // Should filter out the smartScanFailedViolation and return true, duplicatedTransactionViolation is a warning type violation but it's not considered in hasWarningTypeViolation - const hasWarningTypeViolationRes = hasWarningTypeViolation(transaction1.transactionID, transactionViolationsCollection); + const hasWarningTypeViolationRes = hasWarningTypeViolation(transaction.transactionID, transactionViolationsCollection); expect(hasWarningTypeViolationRes).toBeFalsy(); }); }); From ed89588fa58ddf83aaa0c97450265ac0bfd9efa0 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Tue, 28 Jan 2025 14:59:45 +0530 Subject: [PATCH 043/145] fix ESLint. Signed-off-by: krishna2323 --- src/libs/TransactionUtils/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 173279801a34..652e41126de1 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1399,7 +1399,7 @@ function getAllSortedTransactions(iouReportID?: string): Array Date: Tue, 28 Jan 2025 12:41:42 -0700 Subject: [PATCH 044/145] Use new npm command and fastfile --- .github/workflows/verifyHybridApp.yml | 11 +---------- fastlane/Fastfile | 10 ++++++++-- package.json | 1 + 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 80211d83a71c..7827bd9f6553 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -124,16 +124,7 @@ jobs: export RCT_NO_LAUNCH_PACKAGER=1 # Build iOS using xcodebuild - if ! xcodebuild \ - -workspace Mobile-Expensify/iOS/Expensify.xcworkspace \ - -scheme Expensify \ - -configuration Debug \ - -sdk iphonesimulator \ - -arch x86_64 \ - CODE_SIGN_IDENTITY="" \ - CODE_SIGNING_REQUIRED=NO \ - CODE_SIGNING_ALLOWED=NO \ - build | xcpretty + if ! npm run ios-hybrid-build then echo "❌ iOS HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve." exit 1 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 806ffe574031..a36f0e30b6b5 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -458,7 +458,10 @@ platform :ios do ENV["ENVFILE"]=".env.production" build_app( workspace: "./ios/NewExpensify.xcworkspace", - scheme: "New Expensify" + scheme: "New Expensify", + configuration: "Debug", + sdk: "iphonesimulator", + skip_codesigning: true ) setIOSBuildOutputsInEnv() end @@ -468,7 +471,10 @@ platform :ios do ENV["ENVFILE"]="./Mobile-Expensify/.env.production.hybridapp.ios" build_app( workspace: "./Mobile-Expensify/iOS/Expensify.xcworkspace", - scheme: "Expensify" + scheme: "Expensify", + configuration: "Debug", + sdk: "iphonesimulator", + skip_codesigning: true ) setIOSBuildOutputsInEnv() end diff --git a/package.json b/package.json index 43c38ed9b904..d87a3319f608 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "detectRedirectCycle": "ts-node .github/scripts/detectRedirectCycle.ts", "desktop-build-adhoc": "./scripts/build-desktop.sh adhoc", "ios-build": "bundle exec fastlane ios build_unsigned", + "ios-hybrid-build": "bundle exec fastlane ios build_unsigned_hybrid", "android-build": "bundle exec fastlane android build_local", "test": "TZ=utc NODE_OPTIONS=--experimental-vm-modules jest", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", From b4e188cc1feab0dd62b3e2f244d3e1dee1f9c925 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Tue, 28 Jan 2025 14:30:58 -0700 Subject: [PATCH 045/145] Add Android script too --- .github/workflows/verifyHybridApp.yml | 2 +- fastlane/Fastfile | 6 +++--- package.json | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 7827bd9f6553..2bfff47ccf80 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -55,7 +55,7 @@ jobs: - name: Build Android Debug working-directory: Mobile-Expensify/Android run: | - if ! ./gradlew assembleDebug + if ! npm run android-hybrid-build then echo "❌ Android HybridApp failed to build: Please reach out to Contributor+ and/or Expensify engineers for help in #expensify-open-source to resolve." exit 1 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 76e11d2a9664..462062383e3f 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -120,8 +120,7 @@ platform :android do gradle( project_dir: 'Mobile-Expensify/Android', task: 'assemble', - flavor: 'Production', - build_type: 'Release', + build_type: 'Debug', ) setGradleOutputsInEnv() end @@ -474,7 +473,8 @@ platform :ios do scheme: "Expensify", configuration: "Debug", sdk: "iphonesimulator", - skip_codesigning: true + skip_codesigning: true, + export_method: "development" ) setIOSBuildOutputsInEnv() end diff --git a/package.json b/package.json index 08bbd87247d2..6fdf38ed69ec 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "ios-build": "bundle exec fastlane ios build_unsigned", "ios-hybrid-build": "bundle exec fastlane ios build_unsigned_hybrid", "android-build": "bundle exec fastlane android build_local", + "android-hybrid-build": "bundle exec fastlane android build_local_hybrid", "test": "TZ=utc NODE_OPTIONS=--experimental-vm-modules jest", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc", From 65cb6910563dc286d94c4db3c74a869ac066eb4a Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 29 Jan 2025 16:00:28 +0530 Subject: [PATCH 046/145] minor fix. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 2 +- src/libs/TransactionUtils/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index baecdbf4e495..c9965cd51dcb 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -237,7 +237,7 @@ function ReportPreview({ const lastThreeTransactions = transactions?.slice(-3) ?? []; const lastTransaction = transactions?.at(0); const lastThreeReceipts = lastThreeTransactions.map((transaction) => ({...getThumbnailAndImageURIs(transaction), transaction})); - const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, getTransactionViolations(lastTransaction?.transactionID, transactionViolations)); + const showRTERViolationMessage = numberOfRequests === 1 && hasPendingUI(lastTransaction, getTransactionViolations(lastTransaction?.transactionID)); const transactionIDList = [lastTransaction?.transactionID].filter((transactionID): transactionID is string => transactionID !== undefined); const shouldShowBrokenConnectionViolation = numberOfRequests === 1 && shouldShowBrokenConnectionViolationTransactionUtils(transactionIDList, iouReport, policy); let formattedMerchant = numberOfRequests === 1 ? getMerchant(lastTransaction) : null; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 8e4970ceec56..ec9752fdf9a7 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -1484,7 +1484,7 @@ export { getFormattedPostedDate, getCategoryTaxCodeAndAmount, isPerDiemRequest, - isViolationDismissed + isViolationDismissed, }; export type {TransactionChanges}; From e2a084dbbf2f42af2f767accb1651b22525a2184 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Fri, 6 Dec 2024 11:52:00 +0100 Subject: [PATCH 047/145] poc: masked input --- ios/Podfile.lock | 41 ++++++++++++++++--- package-lock.json | 18 ++++++++ package.json | 1 + .../SearchFiltersAmountPage.tsx | 9 +++- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b0e84d3d033f..c830f5b97339 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -166,6 +166,7 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - "GoogleUtilities/NSData+zlib (~> 7.7)" - fmt (9.1.0) + - ForkInputMask (7.3.3) - FullStory (1.52.0) - fullstory_react-native (1.7.2): - DoubleConversion @@ -1604,6 +1605,28 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-advanced-input-mask (1.1.4): + - DoubleConversion + - ForkInputMask (~> 7.3.2) + - glog + - hermes-engine + - RCT-Folly (= 2024.01.01.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-NativeModulesApple + - React-RCTFabric + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-airship (19.2.1): - AirshipFrameworkProxy (= 7.1.2) - DoubleConversion @@ -2880,6 +2903,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-advanced-input-mask (from `../node_modules/react-native-advanced-input-mask`) - "react-native-airship (from `../node_modules/@ua/react-native-airship`)" - react-native-app-logs (from `../node_modules/react-native-app-logs`) - react-native-blob-util (from `../node_modules/react-native-blob-util`) @@ -2968,6 +2992,7 @@ SPEC REPOS: - FirebaseInstallations - FirebasePerformance - FirebaseRemoteConfig + - ForkInputMask - GoogleAppMeasurement - GoogleDataTransport - GoogleSignIn @@ -3093,6 +3118,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" React-microtasksnativemodule: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-advanced-input-mask: + :path: "../node_modules/react-native-advanced-input-mask" react-native-airship: :path: "../node_modules/@ua/react-native-airship" react-native-app-logs: @@ -3248,8 +3275,8 @@ SPEC CHECKSUMS: AirshipServiceExtension: 9c73369f426396d9fb9ff222d86d842fac76ba46 AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0 - boost: d7090b1a93a9798c029277a8288114f2948f471c - DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 + boost: 26992d1adf73c1c7676360643e687aee6dda994b + DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 EXAV: 9773c9799767c9925547b05e41a26a0240bb8ef2 EXImageLoader: 759063a65ab016b836f73972d3bb25404888713d expensify-react-native-background-task: 6f797cf470b627912c246514b1631a205794775d @@ -3269,7 +3296,8 @@ SPEC CHECKSUMS: FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b - fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be + fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 + ForkInputMask: 55e3fbab504b22da98483e9f9a6514b98fdd2f3c FullStory: c8a10b2358c0d33c57be84d16e4c440b0434b33d fullstory_react-native: 63a803cca04b0447a71daa73e4df3f7b56e1919d glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a @@ -3294,7 +3322,7 @@ SPEC CHECKSUMS: onfido-react-native-sdk: 4ccfdeb10f9ccb4a5799d2555cdbc2a068a42c0d Plaid: c32f22ffce5ec67c9e6147eaf6c4d7d5f8086d89 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648 + RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 RCTDeprecation: 2c5e1000b04ab70b53956aa498bf7442c3c6e497 RCTRequired: 5f785a001cf68a551c5f5040fb4c415672dbb481 RCTTypeSafety: 6b98db8965005d32449605c0d005ecb4fee8a0f7 @@ -3323,6 +3351,7 @@ SPEC CHECKSUMS: React-logger: 26155dc23db5c9038794db915f80bd2044512c2e React-Mapbuffer: ad1ba0205205a16dbff11b8ade6d1b3959451658 React-microtasksnativemodule: e771eb9eb6ace5884ee40a293a0e14a9d7a4343c + react-native-advanced-input-mask: 5ef08be0877500034332486f29b60fed8b9db670 react-native-airship: e10f6823d8da49bbcb2db4bdb16ff954188afccc react-native-app-logs: ee32b6e80bf8d1b883dfc5ac96efa7c1bd9a06a5 react-native-blob-util: 221c61c98ae507b758472ac4d2d489119d1a6c44 @@ -3400,8 +3429,8 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c - Yoga: 3deb2471faa9916c8a82dda2a22d3fba2620ad37 + Yoga: f6dc1b6029519815d5516a1241821c6a9074af6d PODFILE CHECKSUM: 6fc95cc1e80a55665a376c27ca23105e1eda8c64 -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3 diff --git a/package-lock.json b/package-lock.json index f8b051c85f97..eea7dcb1f018 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,6 +79,7 @@ "react-fast-pdf": "^1.0.22", "react-map-gl": "^7.1.3", "react-native": "0.76.3", + "react-native-advanced-input-mask": "^1.1.4", "react-native-android-location-enabler": "^2.0.1", "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", @@ -32027,6 +32028,23 @@ } } }, + "node_modules/react-native-advanced-input-mask": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/react-native-advanced-input-mask/-/react-native-advanced-input-mask-1.1.4.tgz", + "integrity": "sha512-paDcVx4796tjoT6jzuMBJaQE9Pgs7J5RWsrwvgyqn+DjzmRzDZBOoYPRhTmmZ7ufUyptH+9Nw1zmals0SBcQ3Q==", + "license": "MIT", + "workspaces": [ + "example", + "WebExample" + ], + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/react-native-android-location-enabler": { "version": "2.0.1", "license": "MIT", diff --git a/package.json b/package.json index 04fc9e7027d1..00cea75c64d8 100644 --- a/package.json +++ b/package.json @@ -144,6 +144,7 @@ "react-fast-pdf": "^1.0.22", "react-map-gl": "^7.1.3", "react-native": "0.76.3", + "react-native-advanced-input-mask": "^1.1.4", "react-native-android-location-enabler": "^2.0.1", "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx index 6473da9fc9f5..9260aa99a871 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx @@ -18,6 +18,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/SearchAdvancedFiltersForm'; +import TextInputMask from "react-native-text-input-mask"; +import { MaskedTextInput } from 'react-native-advanced-input-mask'; + function SearchFiltersAmountPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -73,7 +76,11 @@ function SearchFiltersAmountPage() { Date: Wed, 15 Jan 2025 20:03:52 +0100 Subject: [PATCH 048/145] feat: continue implementation --- src/components/AmountWithoutCurrencyInput.tsx | 46 +++++++++++++++++++ src/components/RNMaskedTextInput.tsx | 39 ++++++++++++++++ .../BaseTextInput/implementations.ts | 11 +++++ .../TextInput/BaseTextInput/index.native.tsx | 11 +++-- .../TextInput/BaseTextInput/index.tsx | 11 +++-- .../TextInput/BaseTextInput/types.ts | 12 +++-- .../PrivateNotes/PrivateNotesEditPage.tsx | 2 +- src/pages/RoomDescriptionPage.tsx | 2 +- .../SearchFiltersAmountPage.tsx | 12 ++--- .../step/IOURequestStepDescription.tsx | 2 +- src/pages/tasks/NewTaskDescriptionPage.tsx | 2 +- src/pages/tasks/NewTaskDetailsPage.tsx | 2 +- src/pages/tasks/TaskDescriptionPage.tsx | 2 +- src/pages/workspace/WorkspaceNewRoomPage.tsx | 2 +- .../WorkspaceProfileDescriptionPage.tsx | 2 +- 15 files changed, 128 insertions(+), 30 deletions(-) create mode 100644 src/components/AmountWithoutCurrencyInput.tsx create mode 100644 src/components/RNMaskedTextInput.tsx create mode 100644 src/components/TextInput/BaseTextInput/implementations.ts diff --git a/src/components/AmountWithoutCurrencyInput.tsx b/src/components/AmountWithoutCurrencyInput.tsx new file mode 100644 index 000000000000..44e4c3d24693 --- /dev/null +++ b/src/components/AmountWithoutCurrencyInput.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import type {ForwardedRef} from 'react'; +import CONST from '@src/CONST'; +import TextInput from './TextInput'; +import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; + +type AmountFormProps = { + /** Amount supplied by the FormProvider */ + value?: string; + + /** Callback to update the amount in the FormProvider */ + onInputChange?: (value: string) => void; + + /** Should we allow negative number as valid input */ + shouldAllowNegative?: boolean; +} & Partial; + +function AmountWithoutCurrencyForm( + {value: amount, onInputChange, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, + ref: ForwardedRef, +) { + return ( + + ); +} + +AmountWithoutCurrencyForm.displayName = 'AmountWithoutCurrencyForm'; + +export default React.forwardRef(AmountWithoutCurrencyForm); diff --git a/src/components/RNMaskedTextInput.tsx b/src/components/RNMaskedTextInput.tsx new file mode 100644 index 000000000000..22a69d2c7fbd --- /dev/null +++ b/src/components/RNMaskedTextInput.tsx @@ -0,0 +1,39 @@ +import type {ForwardedRef} from 'react'; +import React from 'react'; +import type {TextInput} from 'react-native'; +import type {MaskedTextInputProps} from 'react-native-advanced-input-mask'; +import {MaskedTextInput} from 'react-native-advanced-input-mask'; +import Animated from 'react-native-reanimated'; +import useTheme from '@hooks/useTheme'; + +// Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet +const AnimatedTextInput = Animated.createAnimatedComponent(MaskedTextInput); + +type AnimatedTextInputRef = typeof AnimatedTextInput & TextInput & HTMLInputElement; + +function RNMaskedTextInputWithRef(props: MaskedTextInputProps, ref: ForwardedRef) { + const theme = useTheme(); + + return ( + { + if (typeof ref !== 'function') { + return; + } + ref(refHandle as AnimatedTextInputRef); + }} + // eslint-disable-next-line + {...props} + /> + ); +} + +RNMaskedTextInputWithRef.displayName = 'RNMaskedTextInputWithRef'; + +export default React.forwardRef(RNMaskedTextInputWithRef); +export type {AnimatedTextInputRef}; diff --git a/src/components/TextInput/BaseTextInput/implementations.ts b/src/components/TextInput/BaseTextInput/implementations.ts new file mode 100644 index 000000000000..7c638bf4e265 --- /dev/null +++ b/src/components/TextInput/BaseTextInput/implementations.ts @@ -0,0 +1,11 @@ +import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; +import RNMaskedTextInput from '@components/RNMaskedTextInput'; +import RNTextInput from '@components/RNTextInput'; + +const InputComponentMap = new Map([ + ['default', RNTextInput], + ['mask', RNMaskedTextInput], + ['markdown', RNMarkdownTextInput], +]); + +export default InputComponentMap; diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 8d1800ce3b65..98c074c41713 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -10,9 +10,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; -import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; -import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; import TextInputClearButton from '@components/TextInput/TextInputClearButton'; @@ -26,6 +24,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import InputComponentMap from './implementations'; import type {BaseTextInputProps, BaseTextInputRef} from './types'; function BaseTextInput( @@ -62,7 +61,7 @@ function BaseTextInput( prefixCharacter = '', suffixCharacter = '', inputID, - isMarkdownEnabled = false, + type = 'default', excludedMarkdownStyles = [], shouldShowClearButton = false, prefixContainerStyle = [], @@ -71,11 +70,13 @@ function BaseTextInput( suffixStyle = [], contentWidth, loadingSpinnerStyle, + uncontrolled, ...props }: BaseTextInputProps, ref: ForwardedRef, ) { - const InputComponent = isMarkdownEnabled ? RNMarkdownTextInput : RNTextInput; + const InputComponent = InputComponentMap.get(type); + const isMarkdownEnabled = type === 'markdown'; const isAutoGrowHeightMarkdown = isMarkdownEnabled && autoGrowHeight; const inputProps = {shouldSaveDraft: false, shouldUseDefaultValue: false, ...props}; @@ -379,7 +380,7 @@ function BaseTextInput( showSoftInputOnFocus={!disableKeyboard} keyboardType={inputProps.keyboardType} inputMode={!disableKeyboard ? inputProps.inputMode : CONST.INPUT_MODE.NONE} - value={value} + value={uncontrolled ? undefined : value} selection={inputProps.selection} readOnly={isReadOnly} defaultValue={defaultValue} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 4581338bb38e..43c01f3a14ca 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -10,9 +10,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; -import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; -import RNTextInput from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; @@ -29,6 +27,7 @@ import {scrollToRight} from '@libs/InputUtils'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import InputComponentMap from './implementations'; import type {BaseTextInputProps, BaseTextInputRef} from './types'; function BaseTextInput( @@ -65,7 +64,7 @@ function BaseTextInput( prefixCharacter = '', suffixCharacter = '', inputID, - isMarkdownEnabled = false, + type = 'default', excludedMarkdownStyles = [], shouldShowClearButton = false, shouldUseDisabledStyles = true, @@ -76,11 +75,13 @@ function BaseTextInput( contentWidth, loadingSpinnerStyle, placeholderTextColor, + uncontrolled = false, ...inputProps }: BaseTextInputProps, ref: ForwardedRef, ) { - const InputComponent = isMarkdownEnabled ? RNMarkdownTextInput : RNTextInput; + const InputComponent = InputComponentMap.get(type); + const isMarkdownEnabled = type === 'markdown'; const isAutoGrowHeightMarkdown = isMarkdownEnabled && autoGrowHeight; const theme = useTheme(); @@ -383,7 +384,7 @@ function BaseTextInput( onPressOut={inputProps.onPress} showSoftInputOnFocus={!disableKeyboard} inputMode={inputProps.inputMode} - value={value} + value={uncontrolled ? undefined : value} selection={inputProps.selection} readOnly={isReadOnly} defaultValue={defaultValue} diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index eef2d471a0d7..20bddd14ab8c 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -3,6 +3,7 @@ import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewSt import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type IconAsset from '@src/types/utils/IconAsset'; +type InputType = 'markdown' | 'mask' | 'default'; type CustomBaseTextInputProps = { /** Input label */ label?: string; @@ -116,9 +117,6 @@ type CustomBaseTextInputProps = { /** Type of autocomplete */ autoCompleteType?: string; - /** Should live markdown be enabled. Changes RNTextInput component to RNMarkdownTextInput */ - isMarkdownEnabled?: boolean; - /** List of markdowns that won't be styled as a markdown */ excludedMarkdownStyles?: Array; @@ -145,10 +143,16 @@ type CustomBaseTextInputProps = { /** The width of inner content */ contentWidth?: number; + + /** The type (internal implementation) of input. Cab one of: `default`, `mask`, `markdown` */ + type?: InputType; + + /** Whether the input should be enforced to be uncontrolled. Default is `false` */ + uncontrolled?: boolean; }; type BaseTextInputRef = HTMLFormElement | AnimatedTextInputRef; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; -export type {BaseTextInputProps, BaseTextInputRef, CustomBaseTextInputProps}; +export type {BaseTextInputProps, BaseTextInputRef, CustomBaseTextInputProps, InputType}; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index deab122e3006..83543557f36f 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -159,7 +159,7 @@ function PrivateNotesEditPage({route, report, accountID}: PrivateNotesEditPagePr } privateNotesInput.current = el; }} - isMarkdownEnabled + type="markdown" /> diff --git a/src/pages/RoomDescriptionPage.tsx b/src/pages/RoomDescriptionPage.tsx index 69faef68f766..210f529e566f 100644 --- a/src/pages/RoomDescriptionPage.tsx +++ b/src/pages/RoomDescriptionPage.tsx @@ -118,7 +118,7 @@ function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) { value={description} onChangeText={handleReportDescriptionChange} autoCapitalize="none" - isMarkdownEnabled + type="markdown" /> diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx index 9260aa99a871..a813c6333359 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import AmountWithoutCurrencyForm from '@components/AmountWithoutCurrencyForm'; +import AmountWithoutCurrencyInput from '@components/AmountWithoutCurrencyInput'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormOnyxValues} from '@components/Form/types'; @@ -18,9 +19,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/SearchAdvancedFiltersForm'; -import TextInputMask from "react-native-text-input-mask"; -import { MaskedTextInput } from 'react-native-advanced-input-mask'; - function SearchFiltersAmountPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -76,17 +74,15 @@ function SearchFiltersAmountPage() { console.log('paste', e.nativeEvent)} name={INPUT_IDS.LESS_THAN} defaultValue={lessThanFormattedAmount} label={translate('search.filters.amount.lessThan')} accessibilityLabel={translate('search.filters.amount.lessThan')} role={CONST.ROLE.PRESENTATION} + uncontrolled /> diff --git a/src/pages/iou/request/step/IOURequestStepDescription.tsx b/src/pages/iou/request/step/IOURequestStepDescription.tsx index f4a8ee827ff1..4d151d17ba28 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.tsx +++ b/src/pages/iou/request/step/IOURequestStepDescription.tsx @@ -164,7 +164,7 @@ function IOURequestStepDescription({ autoGrowHeight maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight} shouldSubmitForm - isMarkdownEnabled + type="markdown" excludedMarkdownStyles={!isReportInGroupPolicy ? ['mentionReport'] : []} ref={inputCallbackRef} /> diff --git a/src/pages/tasks/NewTaskDescriptionPage.tsx b/src/pages/tasks/NewTaskDescriptionPage.tsx index 4b2c08c95fb1..b4bd5851a747 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.tsx +++ b/src/pages/tasks/NewTaskDescriptionPage.tsx @@ -91,7 +91,7 @@ function NewTaskDescriptionPage({task, route}: NewTaskDescriptionPageProps) { autoGrowHeight maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight} shouldSubmitForm - isMarkdownEnabled + type="markdown" /> diff --git a/src/pages/tasks/NewTaskDetailsPage.tsx b/src/pages/tasks/NewTaskDetailsPage.tsx index ffb199c6108d..4ec1964f4316 100644 --- a/src/pages/tasks/NewTaskDetailsPage.tsx +++ b/src/pages/tasks/NewTaskDetailsPage.tsx @@ -136,7 +136,7 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { defaultValue={Parser.htmlToMarkdown(Parser.replace(taskDescription))} value={taskDescription} onValueChange={setTaskDescription} - isMarkdownEnabled + type="markdown" /> diff --git a/src/pages/tasks/TaskDescriptionPage.tsx b/src/pages/tasks/TaskDescriptionPage.tsx index 86a7cbc54a23..80ba61c2368c 100644 --- a/src/pages/tasks/TaskDescriptionPage.tsx +++ b/src/pages/tasks/TaskDescriptionPage.tsx @@ -132,7 +132,7 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti autoGrowHeight maxAutoGrowHeight={variables.textInputAutoGrowMaxHeight} shouldSubmitForm - isMarkdownEnabled + type="markdown" /> diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index c64c53306a1f..6fe676dd619b 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -289,7 +289,7 @@ function WorkspaceNewRoomPage() { maxLength={CONST.REPORT_DESCRIPTION.MAX_LENGTH} autoCapitalize="none" shouldInterceptSwipe - isMarkdownEnabled + type="markdown" /> diff --git a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx index f6a9e3649b22..9e5ba5bd12f2 100644 --- a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx +++ b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx @@ -108,7 +108,7 @@ function WorkspaceProfileDescriptionPage({policy}: Props) { autoFocus onChangeText={setDescription} autoGrowHeight - isMarkdownEnabled + type="markdown" ref={(el: BaseTextInputRef | null): void => { if (!isInputInitializedRef.current) { updateMultilineInputRange(el); From 0323f42686e87e94771ab409ac8e9374be103cd1 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Fri, 17 Jan 2025 11:59:47 +0100 Subject: [PATCH 049/145] fix: ts/lint --- src/components/AmountWithoutCurrencyInput.tsx | 1 - src/components/TextInput/BaseTextInput/implementations.ts | 7 +++++-- src/components/TextInput/BaseTextInput/index.native.tsx | 3 ++- src/components/TextInput/BaseTextInput/index.tsx | 3 ++- src/components/TextInput/BaseTextInput/types.ts | 8 +++++++- .../SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx | 1 - 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/components/AmountWithoutCurrencyInput.tsx b/src/components/AmountWithoutCurrencyInput.tsx index 44e4c3d24693..7a8e1deca420 100644 --- a/src/components/AmountWithoutCurrencyInput.tsx +++ b/src/components/AmountWithoutCurrencyInput.tsx @@ -31,7 +31,6 @@ function AmountWithoutCurrencyForm( keyboardType={!shouldAllowNegative ? CONST.KEYBOARD_TYPE.DECIMAL_PAD : undefined} type="mask" mask="[09999999].[99]" - customNotations={[{character: '.', characterSet: '.', isOptional: true}, {character: ',', characterSet: ',', isOptional: true}]} // On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag. // See https://github.com/Expensify/App/issues/51868 for more information autoCapitalize="words" diff --git a/src/components/TextInput/BaseTextInput/implementations.ts b/src/components/TextInput/BaseTextInput/implementations.ts index 7c638bf4e265..80b91ace7cff 100644 --- a/src/components/TextInput/BaseTextInput/implementations.ts +++ b/src/components/TextInput/BaseTextInput/implementations.ts @@ -1,10 +1,13 @@ import RNMarkdownTextInput from '@components/RNMarkdownTextInput'; import RNMaskedTextInput from '@components/RNMaskedTextInput'; import RNTextInput from '@components/RNTextInput'; +import type {BaseTextInputProps, InputType} from './types'; -const InputComponentMap = new Map([ +type InputComponentType = React.ComponentType; + +const InputComponentMap = new Map([ ['default', RNTextInput], - ['mask', RNMaskedTextInput], + ['mask', RNMaskedTextInput as InputComponentType], ['markdown', RNMarkdownTextInput], ]); diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 98c074c41713..af537a981cee 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; +import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; import TextInputClearButton from '@components/TextInput/TextInputClearButton'; @@ -75,7 +76,7 @@ function BaseTextInput( }: BaseTextInputProps, ref: ForwardedRef, ) { - const InputComponent = InputComponentMap.get(type); + const InputComponent = InputComponentMap.get(type) ?? RNTextInput; const isMarkdownEnabled = type === 'markdown'; const isAutoGrowHeightMarkdown = isMarkdownEnabled && autoGrowHeight; diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 43c01f3a14ca..dff9b951cec7 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import type {AnimatedMarkdownTextInputRef} from '@components/RNMarkdownTextInput'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; +import RNTextInput from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; @@ -80,7 +81,7 @@ function BaseTextInput( }: BaseTextInputProps, ref: ForwardedRef, ) { - const InputComponent = InputComponentMap.get(type); + const InputComponent = InputComponentMap.get(type) ?? RNTextInput; const isMarkdownEnabled = type === 'markdown'; const isAutoGrowHeightMarkdown = isMarkdownEnabled && autoGrowHeight; diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 20bddd14ab8c..9a07b7773463 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,5 +1,6 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import type {MaskedTextInputOwnProps} from 'react-native-advanced-input-mask/lib/typescript/src/types'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -120,6 +121,8 @@ type CustomBaseTextInputProps = { /** List of markdowns that won't be styled as a markdown */ excludedMarkdownStyles?: Array; + markdownStyle?: MarkdownStyle; + /** Whether the clear button should be displayed */ shouldShowClearButton?: boolean; @@ -144,9 +147,12 @@ type CustomBaseTextInputProps = { /** The width of inner content */ contentWidth?: number; - /** The type (internal implementation) of input. Cab one of: `default`, `mask`, `markdown` */ + /** The type (internal implementation) of input. Can be one of: `default`, `mask`, `markdown` */ type?: InputType; + /** The mask of the masked input */ + mask?: MaskedTextInputOwnProps['mask']; + /** Whether the input should be enforced to be uncontrolled. Default is `false` */ uncontrolled?: boolean; }; diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx index a813c6333359..2f06c4299d78 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx @@ -76,7 +76,6 @@ function SearchFiltersAmountPage() { console.log('paste', e.nativeEvent)} name={INPUT_IDS.LESS_THAN} defaultValue={lessThanFormattedAmount} label={translate('search.filters.amount.lessThan')} From c856778ac58251c2ab70b5b36a4ed4398faa8ada Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Mon, 20 Jan 2025 15:19:12 +0100 Subject: [PATCH 050/145] fix: changes before review --- src/components/AmountWithoutCurrencyInput.tsx | 8 ++++---- .../SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/AmountWithoutCurrencyInput.tsx b/src/components/AmountWithoutCurrencyInput.tsx index 7a8e1deca420..a85a7ec05e9b 100644 --- a/src/components/AmountWithoutCurrencyInput.tsx +++ b/src/components/AmountWithoutCurrencyInput.tsx @@ -15,7 +15,7 @@ type AmountFormProps = { shouldAllowNegative?: boolean; } & Partial; -function AmountWithoutCurrencyForm( +function AmountWithoutCurrencyInput( {value: amount, onInputChange, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, ref: ForwardedRef, ) { @@ -30,7 +30,7 @@ function AmountWithoutCurrencyForm( ref={ref} keyboardType={!shouldAllowNegative ? CONST.KEYBOARD_TYPE.DECIMAL_PAD : undefined} type="mask" - mask="[09999999].[99]" + mask="[09999999].[09]" // On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag. // See https://github.com/Expensify/App/issues/51868 for more information autoCapitalize="words" @@ -40,6 +40,6 @@ function AmountWithoutCurrencyForm( ); } -AmountWithoutCurrencyForm.displayName = 'AmountWithoutCurrencyForm'; +AmountWithoutCurrencyInput.displayName = 'AmountWithoutCurrencyForm'; -export default React.forwardRef(AmountWithoutCurrencyForm); +export default React.forwardRef(AmountWithoutCurrencyInput); diff --git a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx index 2f06c4299d78..913387703053 100644 --- a/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx +++ b/src/pages/Search/SearchAdvancedFiltersPage/SearchFiltersAmountPage.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import AmountWithoutCurrencyForm from '@components/AmountWithoutCurrencyForm'; import AmountWithoutCurrencyInput from '@components/AmountWithoutCurrencyInput'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -62,7 +61,7 @@ function SearchFiltersAmountPage() { > From 975d7f600b7fa6d8e2bf864352ecb105cb0eb1d9 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Mon, 20 Jan 2025 15:30:01 +0100 Subject: [PATCH 051/145] fix: resolve merge conflicts --- ios/Podfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index c830f5b97339..eea6bb15a18b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3275,8 +3275,8 @@ SPEC CHECKSUMS: AirshipServiceExtension: 9c73369f426396d9fb9ff222d86d842fac76ba46 AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0 - boost: 26992d1adf73c1c7676360643e687aee6dda994b - DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5 + boost: d7090b1a93a9798c029277a8288114f2948f471c + DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385 EXAV: 9773c9799767c9925547b05e41a26a0240bb8ef2 EXImageLoader: 759063a65ab016b836f73972d3bb25404888713d expensify-react-native-background-task: 6f797cf470b627912c246514b1631a205794775d @@ -3296,7 +3296,7 @@ SPEC CHECKSUMS: FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b - fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 + fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be ForkInputMask: 55e3fbab504b22da98483e9f9a6514b98fdd2f3c FullStory: c8a10b2358c0d33c57be84d16e4c440b0434b33d fullstory_react-native: 63a803cca04b0447a71daa73e4df3f7b56e1919d @@ -3322,7 +3322,7 @@ SPEC CHECKSUMS: onfido-react-native-sdk: 4ccfdeb10f9ccb4a5799d2555cdbc2a068a42c0d Plaid: c32f22ffce5ec67c9e6147eaf6c4d7d5f8086d89 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 + RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648 RCTDeprecation: 2c5e1000b04ab70b53956aa498bf7442c3c6e497 RCTRequired: 5f785a001cf68a551c5f5040fb4c415672dbb481 RCTTypeSafety: 6b98db8965005d32449605c0d005ecb4fee8a0f7 @@ -3429,8 +3429,8 @@ SPEC CHECKSUMS: SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c - Yoga: f6dc1b6029519815d5516a1241821c6a9074af6d + Yoga: 3deb2471faa9916c8a82dda2a22d3fba2620ad37 PODFILE CHECKSUM: 6fc95cc1e80a55665a376c27ca23105e1eda8c64 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 From 065fd5f2db61fe6543f08bf07981e0adfcd1ea48 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Mon, 20 Jan 2025 15:45:49 +0100 Subject: [PATCH 052/145] fix: eslint --- .../PrivateNotes/PrivateNotesEditPage.tsx | 18 +++++++++--------- src/pages/RoomDescriptionPage.tsx | 10 +++++----- .../request/step/IOURequestStepDescription.tsx | 18 +++++++++--------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 83543557f36f..42e83dbff6b0 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -18,12 +18,12 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {PrivateNotesNavigatorParamList} from '@libs/Navigation/types'; import Parser from '@libs/Parser'; -import * as ReportUtils from '@libs/ReportUtils'; +import {goBackFromPrivateNotes, navigateToDetailsPage} from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import type {WithReportAndPrivateNotesOrNotFoundProps} from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; import variables from '@styles/variables'; -import * as ReportActions from '@userActions/Report'; +import {clearPrivateNotesError, getDraftPrivateNote, handleUserDeletedLinksInHtml, savePrivateNotesDraft, updatePrivateNotes} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -46,7 +46,7 @@ function PrivateNotesEditPage({route, report, accountID}: PrivateNotesEditPagePr // We need to edit the note in markdown format, but display it in HTML format const [privateNote, setPrivateNote] = useState( - () => ReportActions.getDraftPrivateNote(report.reportID).trim() || Parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), + () => getDraftPrivateNote(report.reportID).trim() || Parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), ); /** @@ -56,7 +56,7 @@ function PrivateNotesEditPage({route, report, accountID}: PrivateNotesEditPagePr const debouncedSavePrivateNote = useMemo( () => lodashDebounce((text: string) => { - ReportActions.savePrivateNotesDraft(report.reportID, text); + savePrivateNotesDraft(report.reportID, text); }, 1000), [report.reportID], ); @@ -85,8 +85,8 @@ function PrivateNotesEditPage({route, report, accountID}: PrivateNotesEditPagePr const originalNote = report?.privateNotes?.[Number(route.params.accountID)]?.note ?? ''; let editedNote = ''; if (privateNote.trim() !== originalNote.trim()) { - editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), Parser.htmlToMarkdown(originalNote).trim()); - ReportActions.updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote); + editedNote = handleUserDeletedLinksInHtml(privateNote.trim(), Parser.htmlToMarkdown(originalNote).trim()); + updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote); } // We want to delete saved private note draft after saving the note @@ -94,7 +94,7 @@ function PrivateNotesEditPage({route, report, accountID}: PrivateNotesEditPagePr Keyboard.dismiss(); if (!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { - ReportUtils.navigateToDetailsPage(report, backTo); + navigateToDetailsPage(report, backTo); } else { Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID, backTo))); } @@ -108,7 +108,7 @@ function PrivateNotesEditPage({route, report, accountID}: PrivateNotesEditPagePr > ReportUtils.goBackFromPrivateNotes(report, accountID, backTo)} + onBackButtonPress={() => goBackFromPrivateNotes(report, accountID, backTo)} shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> @@ -130,7 +130,7 @@ function PrivateNotesEditPage({route, report, accountID}: PrivateNotesEditPagePr errors={{ ...(report?.privateNotes?.[Number(route.params.accountID)]?.errors ?? ''), }} - onClose={() => ReportActions.clearPrivateNotesError(report.reportID, Number(route.params.accountID))} + onClose={() => clearPrivateNotesError(report.reportID, Number(route.params.accountID))} style={[styles.mb3]} > >(); const backTo = route.params.backTo; const styles = useThemeStyles(); - const [description, setDescription] = useState(() => Parser.htmlToMarkdown(ReportUtils.getReportDescription(report))); + const [description, setDescription] = useState(() => Parser.htmlToMarkdown(getReportDescription(report))); const reportDescriptionInputRef = useRef(null); const focusTimeoutRef = useRef | null>(null); const {translate} = useLocalize(); @@ -58,7 +58,7 @@ function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) { const previousValue = report?.description ?? ''; const newValue = description.trim(); - Report.updateDescription(report.reportID, previousValue, newValue); + updateDescription(report.reportID, previousValue, newValue); goBack(); }, [report.reportID, report.description, description, goBack]); @@ -76,7 +76,7 @@ function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) { }, []), ); - const canEdit = ReportUtils.canEditReportDescription(report, policy); + const canEdit = canEditReportDescription(report, policy); return ( CONST.DESCRIPTION_LIMIT) { - ErrorUtils.addErrorMessage( + addErrorMessage( errors, 'moneyRequestComment', translate('common.error.characterLimitExceedCounter', {length: values.moneyRequestComment.length, limit: CONST.DESCRIPTION_LIMIT}), @@ -116,7 +116,7 @@ function IOURequestStepDescription({ navigateBack(); return; } - const isTransactionDraft = IOUUtils.shouldUseTransactionDraft(action); + const isTransactionDraft = shouldUseTransactionDraft(action); IOU.setMoneyRequestDescription(transaction?.transactionID, newComment, isTransactionDraft); @@ -129,9 +129,9 @@ function IOURequestStepDescription({ const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; - const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && TransactionUtils.areRequiredFieldsEmpty(transaction); + const canEditSplitBill = isSplitBill && reportAction && session?.accountID === reportAction.actorAccountID && areRequiredFieldsEmpty(transaction); // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = isEditing && (isSplitBill ? !canEditSplitBill : !ReportActionsUtils.isMoneyRequestAction(reportAction) || !ReportUtils.canEditMoneyRequest(reportAction)); + const shouldShowNotFoundPage = isEditing && (isSplitBill ? !canEditSplitBill : !isMoneyRequestAction(reportAction) || !canEditMoneyRequest(reportAction)); const isReportInGroupPolicy = !!report?.policyID && report.policyID !== CONST.POLICY.ID_FAKE; return ( From c0e7f99a056f9206dfad1bd83af2c6b216446d38 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Wed, 22 Jan 2025 12:22:06 +0100 Subject: [PATCH 053/145] fix: do not allow to enter forbidden characters --- ios/Podfile.lock | 4 ++-- package-lock.json | 8 ++++---- package.json | 2 +- src/components/AmountWithoutCurrencyInput.tsx | 1 + src/components/TextInput/BaseTextInput/types.ts | 3 +++ 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index eea6bb15a18b..6c45241fff61 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1605,7 +1605,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-advanced-input-mask (1.1.4): + - react-native-advanced-input-mask (1.2.0): - DoubleConversion - ForkInputMask (~> 7.3.2) - glog @@ -3351,7 +3351,7 @@ SPEC CHECKSUMS: React-logger: 26155dc23db5c9038794db915f80bd2044512c2e React-Mapbuffer: ad1ba0205205a16dbff11b8ade6d1b3959451658 React-microtasksnativemodule: e771eb9eb6ace5884ee40a293a0e14a9d7a4343c - react-native-advanced-input-mask: 5ef08be0877500034332486f29b60fed8b9db670 + react-native-advanced-input-mask: 133eb22a94337d3cd99c5e831a4ee306db5ae6e5 react-native-airship: e10f6823d8da49bbcb2db4bdb16ff954188afccc react-native-app-logs: ee32b6e80bf8d1b883dfc5ac96efa7c1bd9a06a5 react-native-blob-util: 221c61c98ae507b758472ac4d2d489119d1a6c44 diff --git a/package-lock.json b/package-lock.json index eea7dcb1f018..3a7f790713e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,7 +79,7 @@ "react-fast-pdf": "^1.0.22", "react-map-gl": "^7.1.3", "react-native": "0.76.3", - "react-native-advanced-input-mask": "^1.1.4", + "react-native-advanced-input-mask": "^1.2.0", "react-native-android-location-enabler": "^2.0.1", "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", @@ -32029,9 +32029,9 @@ } }, "node_modules/react-native-advanced-input-mask": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/react-native-advanced-input-mask/-/react-native-advanced-input-mask-1.1.4.tgz", - "integrity": "sha512-paDcVx4796tjoT6jzuMBJaQE9Pgs7J5RWsrwvgyqn+DjzmRzDZBOoYPRhTmmZ7ufUyptH+9Nw1zmals0SBcQ3Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-native-advanced-input-mask/-/react-native-advanced-input-mask-1.2.0.tgz", + "integrity": "sha512-Pk7Yau14Zm5PZhJgvDDYOZb224MxzgMv86hiXIZja7QOXaLlWUwm+Ko1s4mW20p1RKIPba7QsRfr9CzXuLDrLg==", "license": "MIT", "workspaces": [ "example", diff --git a/package.json b/package.json index 00cea75c64d8..afad6cdb1168 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "react-fast-pdf": "^1.0.22", "react-map-gl": "^7.1.3", "react-native": "0.76.3", - "react-native-advanced-input-mask": "^1.1.4", + "react-native-advanced-input-mask": "^1.2.0", "react-native-android-location-enabler": "^2.0.1", "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", diff --git a/src/components/AmountWithoutCurrencyInput.tsx b/src/components/AmountWithoutCurrencyInput.tsx index a85a7ec05e9b..f68f4b102c61 100644 --- a/src/components/AmountWithoutCurrencyInput.tsx +++ b/src/components/AmountWithoutCurrencyInput.tsx @@ -31,6 +31,7 @@ function AmountWithoutCurrencyInput( keyboardType={!shouldAllowNegative ? CONST.KEYBOARD_TYPE.DECIMAL_PAD : undefined} type="mask" mask="[09999999].[09]" + allowedKeys="0123456789.," // On android autoCapitalize="words" is necessary when keyboardType="decimal-pad" or inputMode="decimal" to prevent input lag. // See https://github.com/Expensify/App/issues/51868 for more information autoCapitalize="words" diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 9a07b7773463..e01a9a8ce7af 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -153,6 +153,9 @@ type CustomBaseTextInputProps = { /** The mask of the masked input */ mask?: MaskedTextInputOwnProps['mask']; + /** A set of permitted characters for the input */ + allowedKeys?: MaskedTextInputOwnProps['allowedKeys']; + /** Whether the input should be enforced to be uncontrolled. Default is `false` */ uncontrolled?: boolean; }; From f1dea18f63c97d8a578148404d6e2c8ab35c40da Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Thu, 23 Jan 2025 11:46:46 +0100 Subject: [PATCH 054/145] fix: eslint --- .../step/IOURequestStepDescription.tsx | 8 ++--- src/pages/tasks/NewTaskDescriptionPage.tsx | 12 ++++---- src/pages/tasks/NewTaskDetailsPage.tsx | 29 +++++++------------ src/pages/tasks/TaskDescriptionPage.tsx | 4 +-- 4 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepDescription.tsx b/src/pages/iou/request/step/IOURequestStepDescription.tsx index 4e8f55f2b420..7006b66ed3b9 100644 --- a/src/pages/iou/request/step/IOURequestStepDescription.tsx +++ b/src/pages/iou/request/step/IOURequestStepDescription.tsx @@ -18,7 +18,7 @@ import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {canEditMoneyRequest} from '@libs/ReportUtils'; import {areRequiredFieldsEmpty} from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import * as IOU from '@userActions/IOU'; +import {setDraftSplitTransaction, setMoneyRequestDescription, updateMoneyRequestDescription} from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -112,16 +112,16 @@ function IOURequestStepDescription({ // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value if (isEditingSplitBill) { - IOU.setDraftSplitTransaction(transaction?.transactionID, {comment: newComment}); + setDraftSplitTransaction(transaction?.transactionID, {comment: newComment}); navigateBack(); return; } const isTransactionDraft = shouldUseTransactionDraft(action); - IOU.setMoneyRequestDescription(transaction?.transactionID, newComment, isTransactionDraft); + setMoneyRequestDescription(transaction?.transactionID, newComment, isTransactionDraft); if (action === CONST.IOU.ACTION.EDIT) { - IOU.updateMoneyRequestDescription(transaction?.transactionID, reportID, newComment, policy, policyTags, policyCategories); + updateMoneyRequestDescription(transaction?.transactionID, reportID, newComment, policy, policyTags, policyCategories); } navigateBack(); diff --git a/src/pages/tasks/NewTaskDescriptionPage.tsx b/src/pages/tasks/NewTaskDescriptionPage.tsx index b4bd5851a747..e70b6ea15d20 100644 --- a/src/pages/tasks/NewTaskDescriptionPage.tsx +++ b/src/pages/tasks/NewTaskDescriptionPage.tsx @@ -11,15 +11,15 @@ import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {NewTaskNavigatorParamList} from '@libs/Navigation/types'; import Parser from '@libs/Parser'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getCommentLength} from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import variables from '@styles/variables'; -import * as TaskActions from '@userActions/Task'; +import {setDescriptionValue} from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -41,15 +41,15 @@ function NewTaskDescriptionPage({task, route}: NewTaskDescriptionPageProps) { const goBack = () => Navigation.goBack(ROUTES.NEW_TASK.getRoute(route.params?.backTo)); const onSubmit = (values: FormOnyxValues) => { - TaskActions.setDescriptionValue(values.taskDescription); + setDescriptionValue(values.taskDescription); goBack(); }; const validate = (values: FormOnyxValues): FormInputErrors => { const errors = {}; - const taskDescriptionLength = ReportUtils.getCommentLength(values.taskDescription); + const taskDescriptionLength = getCommentLength(values.taskDescription); if (taskDescriptionLength > CONST.DESCRIPTION_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'taskDescription', translate('common.error.characterLimitExceedCounter', {length: taskDescriptionLength, limit: CONST.DESCRIPTION_LIMIT})); + addErrorMessage(errors, 'taskDescription', translate('common.error.characterLimitExceedCounter', {length: taskDescriptionLength, limit: CONST.DESCRIPTION_LIMIT})); } return errors; diff --git a/src/pages/tasks/NewTaskDetailsPage.tsx b/src/pages/tasks/NewTaskDetailsPage.tsx index 4ec1964f4316..65f78ac94e49 100644 --- a/src/pages/tasks/NewTaskDetailsPage.tsx +++ b/src/pages/tasks/NewTaskDetailsPage.tsx @@ -11,15 +11,15 @@ import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {NewTaskNavigatorParamList} from '@libs/Navigation/types'; import Parser from '@libs/Parser'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getCommentLength} from '@libs/ReportUtils'; import playSound, {SOUNDS} from '@libs/Sound'; import variables from '@styles/variables'; -import * as TaskActions from '@userActions/Task'; +import {createTaskAndNavigate, dismissModalAndClearOutTaskInfo, setDetailsValue, setShareDestinationValue} from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -56,13 +56,13 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { if (!values.taskTitle) { // We error if the user doesn't enter a task name - ErrorUtils.addErrorMessage(errors, 'taskTitle', translate('newTaskPage.pleaseEnterTaskName')); + addErrorMessage(errors, 'taskTitle', translate('newTaskPage.pleaseEnterTaskName')); } else if (values.taskTitle.length > CONST.TITLE_CHARACTER_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'taskTitle', translate('common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT})); + addErrorMessage(errors, 'taskTitle', translate('common.error.characterLimitExceedCounter', {length: values.taskTitle.length, limit: CONST.TITLE_CHARACTER_LIMIT})); } - const taskDescriptionLength = ReportUtils.getCommentLength(values.taskDescription); + const taskDescriptionLength = getCommentLength(values.taskDescription); if (taskDescriptionLength > CONST.DESCRIPTION_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'taskDescription', translate('common.error.characterLimitExceedCounter', {length: taskDescriptionLength, limit: CONST.DESCRIPTION_LIMIT})); + addErrorMessage(errors, 'taskDescription', translate('common.error.characterLimitExceedCounter', {length: taskDescriptionLength, limit: CONST.DESCRIPTION_LIMIT})); } return errors; @@ -71,19 +71,12 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { // On submit, we want to call the assignTask function and wait to validate // the response const onSubmit = (values: FormOnyxValues) => { - TaskActions.setDetailsValue(values.taskTitle, values.taskDescription); + setDetailsValue(values.taskTitle, values.taskDescription); if (skipConfirmation) { - TaskActions.setShareDestinationValue(task?.parentReportID ?? '-1'); + setShareDestinationValue(task?.parentReportID ?? '-1'); playSound(SOUNDS.DONE); - TaskActions.createTaskAndNavigate( - task?.parentReportID ?? '-1', - values.taskTitle, - values.taskDescription ?? '', - task?.assignee ?? '', - task.assigneeAccountID, - task.assigneeChatReport, - ); + createTaskAndNavigate(task?.parentReportID ?? '-1', values.taskTitle, values.taskDescription ?? '', task?.assignee ?? '', task.assigneeAccountID, task.assigneeChatReport); } else { Navigation.navigate(ROUTES.NEW_TASK.getRoute(backTo)); } @@ -98,7 +91,7 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { TaskActions.dismissModalAndClearOutTaskInfo(backTo)} + onBackButtonPress={() => dismissModalAndClearOutTaskInfo(backTo)} /> CONST.DESCRIPTION_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'description', translate('common.error.characterLimitExceedCounter', {length: taskDescriptionLength, limit: CONST.DESCRIPTION_LIMIT})); + addErrorMessage(errors, 'description', translate('common.error.characterLimitExceedCounter', {length: taskDescriptionLength, limit: CONST.DESCRIPTION_LIMIT})); } return errors; From 04b9271bf17e2ed3e1410bef577d560c86952687 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Thu, 23 Jan 2025 12:08:51 +0100 Subject: [PATCH 055/145] fix: eslint --- src/pages/tasks/NewTaskDetailsPage.tsx | 2 +- src/pages/tasks/TaskDescriptionPage.tsx | 18 +++---- src/pages/workspace/WorkspaceNewRoomPage.tsx | 54 +++++++++----------- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/src/pages/tasks/NewTaskDetailsPage.tsx b/src/pages/tasks/NewTaskDetailsPage.tsx index 65f78ac94e49..f225727f10c3 100644 --- a/src/pages/tasks/NewTaskDetailsPage.tsx +++ b/src/pages/tasks/NewTaskDetailsPage.tsx @@ -74,7 +74,7 @@ function NewTaskDetailsPage({task, route}: NewTaskDetailsPageProps) { setDetailsValue(values.taskTitle, values.taskDescription); if (skipConfirmation) { - setShareDestinationValue(task?.parentReportID ?? '-1'); + setShareDestinationValue(task?.parentReportID); playSound(SOUNDS.DONE); createTaskAndNavigate(task?.parentReportID ?? '-1', values.taskTitle, values.taskDescription ?? '', task?.assignee ?? '', task.assigneeAccountID, task.assigneeChatReport); } else { diff --git a/src/pages/tasks/TaskDescriptionPage.tsx b/src/pages/tasks/TaskDescriptionPage.tsx index b90a37a59db2..9fdf1757ca96 100644 --- a/src/pages/tasks/TaskDescriptionPage.tsx +++ b/src/pages/tasks/TaskDescriptionPage.tsx @@ -18,12 +18,12 @@ import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportDescriptionNavigatorParamList} from '@libs/Navigation/types'; import Parser from '@libs/Parser'; -import * as ReportUtils from '@libs/ReportUtils'; +import {getCommentLength, getParsedComment, isOpenTaskReport, isTaskReport} from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; import variables from '@styles/variables'; -import * as Task from '@userActions/Task'; +import {canModifyTask, editTask} from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -40,8 +40,8 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { const errors = {}; - const parsedDescription = ReportUtils.getParsedComment(values?.description); - const taskDescriptionLength = ReportUtils.getCommentLength(parsedDescription); + const parsedDescription = getParsedComment(values?.description); + const taskDescriptionLength = getCommentLength(parsedDescription); if (values?.description && taskDescriptionLength > CONST.DESCRIPTION_LIMIT) { addErrorMessage(errors, 'description', translate('common.error.characterLimitExceedCounter', {length: taskDescriptionLength, limit: CONST.DESCRIPTION_LIMIT})); } @@ -56,7 +56,7 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti if (values.description !== Parser.htmlToMarkdown(report?.description ?? '') && !isEmptyObject(report)) { // Set the description of the report in the store and then call EditTask API // to update the description of the report on the server - Task.editTask(report, {description: values.description}); + editTask(report, {description: values.description}); } Navigation.dismissModal(report?.reportID); @@ -64,7 +64,7 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti [report], ); - if (!ReportUtils.isTaskReport(report)) { + if (!isTaskReport(report)) { Navigation.isNavigationReady().then(() => { Navigation.dismissModal(report?.reportID); }); @@ -72,9 +72,9 @@ function TaskDescriptionPage({report, currentUserPersonalDetails}: TaskDescripti const inputRef = useRef(null); const focusTimeoutRef = useRef(null); - const isOpen = ReportUtils.isOpenTaskReport(report); - const canModifyTask = Task.canModifyTask(report, currentUserPersonalDetails.accountID); - const isTaskNonEditable = ReportUtils.isTaskReport(report) && (!canModifyTask || !isOpen); + const isOpen = isOpenTaskReport(report); + const canActuallyModifyTask = canModifyTask(report, currentUserPersonalDetails.accountID); + const isTaskNonEditable = isTaskReport(report) && (!canActuallyModifyTask || !isOpen); useFocusEffect( useCallback(() => { diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index 6fe676dd619b..fd3cc90682f9 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -24,14 +24,14 @@ import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {addErrorMessage} from '@libs/ErrorUtils'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import * as ValidationUtils from '@libs/ValidationUtils'; +import {getActivePolicies} from '@libs/PolicyUtils'; +import {buildOptimisticChatReport, getCommentLength, getParsedComment, isPolicyAdmin} from '@libs/ReportUtils'; +import {isExistingRoomName, isReservedRoomName, isValidRoomName} from '@libs/ValidationUtils'; import variables from '@styles/variables'; -import * as Report from '@userActions/Report'; +import {addPolicyReport, clearNewRoomFormError} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -64,7 +64,7 @@ function WorkspaceNewRoomPage() { const workspaceOptions = useMemo( () => - PolicyUtils.getActivePolicies(policies, session?.email) + getActivePolicies(policies, session?.email) ?.filter((policy) => policy.type !== CONST.POLICY.TYPE.PERSONAL) .map((policy) => ({ label: policy.name, @@ -79,12 +79,12 @@ function WorkspaceNewRoomPage() { } return ''; }); - const isPolicyAdmin = useMemo(() => { + const isAdminPolicy = useMemo(() => { if (!policyID) { return false; } - return ReportUtils.isPolicyAdmin(policyID, policies); + return isPolicyAdmin(policyID, policies); }, [policyID, policies]); const [newRoomReportID, setNewRoomReportID] = useState(); @@ -93,8 +93,8 @@ function WorkspaceNewRoomPage() { */ const submit = (values: FormOnyxValues) => { const participants = [session?.accountID ?? -1]; - const parsedDescription = ReportUtils.getParsedComment(values.reportDescription ?? '', {policyID}); - const policyReport = ReportUtils.buildOptimisticChatReport( + const parsedDescription = getParsedComment(values.reportDescription ?? '', {policyID}); + const policyReport = buildOptimisticChatReport( participants, values.roomName, CONST.REPORT.CHAT_TYPE.POLICY_ROOM, @@ -110,11 +110,11 @@ function WorkspaceNewRoomPage() { parsedDescription, ); setNewRoomReportID(policyReport.reportID); - Report.addPolicyReport(policyReport); + addPolicyReport(policyReport); }; useEffect(() => { - Report.clearNewRoomFormError(); + clearNewRoomFormError(); }, []); useEffect(() => { @@ -140,12 +140,12 @@ function WorkspaceNewRoomPage() { }, [isLoading, errorFields]); useEffect(() => { - if (isPolicyAdmin) { + if (isAdminPolicy) { return; } setWriteCapability(CONST.REPORT.WRITE_CAPABILITIES.ALL); - }, [isPolicyAdmin]); + }, [isAdminPolicy]); /** * @param values - form input values passed by the Form component @@ -157,27 +157,23 @@ function WorkspaceNewRoomPage() { if (!values.roomName || values.roomName === CONST.POLICY.ROOM_PREFIX) { // We error if the user doesn't enter a room name or left blank - ErrorUtils.addErrorMessage(errors, 'roomName', translate('newRoomPage.pleaseEnterRoomName')); - } else if (values.roomName !== CONST.POLICY.ROOM_PREFIX && !ValidationUtils.isValidRoomName(values.roomName)) { + addErrorMessage(errors, 'roomName', translate('newRoomPage.pleaseEnterRoomName')); + } else if (values.roomName !== CONST.POLICY.ROOM_PREFIX && !isValidRoomName(values.roomName)) { // We error if the room name has invalid characters - ErrorUtils.addErrorMessage(errors, 'roomName', translate('newRoomPage.roomNameInvalidError')); - } else if (ValidationUtils.isReservedRoomName(values.roomName)) { + addErrorMessage(errors, 'roomName', translate('newRoomPage.roomNameInvalidError')); + } else if (isReservedRoomName(values.roomName)) { // Certain names are reserved for default rooms and should not be used for policy rooms. - ErrorUtils.addErrorMessage(errors, 'roomName', translate('newRoomPage.roomNameReservedError', {reservedName: values.roomName})); - } else if (ValidationUtils.isExistingRoomName(values.roomName, reports, values.policyID ?? '-1')) { + addErrorMessage(errors, 'roomName', translate('newRoomPage.roomNameReservedError', {reservedName: values.roomName})); + } else if (isExistingRoomName(values.roomName, reports, values.policyID ?? '-1')) { // Certain names are reserved for default rooms and should not be used for policy rooms. - ErrorUtils.addErrorMessage(errors, 'roomName', translate('newRoomPage.roomAlreadyExistsError')); + addErrorMessage(errors, 'roomName', translate('newRoomPage.roomAlreadyExistsError')); } else if (values.roomName.length > CONST.TITLE_CHARACTER_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'roomName', translate('common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT})); + addErrorMessage(errors, 'roomName', translate('common.error.characterLimitExceedCounter', {length: values.roomName.length, limit: CONST.TITLE_CHARACTER_LIMIT})); } - const descriptionLength = ReportUtils.getCommentLength(values.reportDescription, {policyID}); + const descriptionLength = getCommentLength(values.reportDescription, {policyID}); if (descriptionLength > CONST.REPORT_DESCRIPTION.MAX_LENGTH) { - ErrorUtils.addErrorMessage( - errors, - 'reportDescription', - translate('common.error.characterLimitExceedCounter', {length: descriptionLength, limit: CONST.REPORT_DESCRIPTION.MAX_LENGTH}), - ); + addErrorMessage(errors, 'reportDescription', translate('common.error.characterLimitExceedCounter', {length: descriptionLength, limit: CONST.REPORT_DESCRIPTION.MAX_LENGTH})); } if (!values.policyID) { @@ -302,7 +298,7 @@ function WorkspaceNewRoomPage() { onValueChange={(value) => setPolicyID(value as typeof policyID)} /> - {isPolicyAdmin && ( + {isAdminPolicy && ( Date: Thu, 23 Jan 2025 12:26:18 +0100 Subject: [PATCH 056/145] fix: eslint --- src/pages/workspace/WorkspaceNewRoomPage.tsx | 4 ++-- src/pages/workspace/WorkspaceProfileDescriptionPage.tsx | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/WorkspaceNewRoomPage.tsx b/src/pages/workspace/WorkspaceNewRoomPage.tsx index fd3cc90682f9..0ce996d1a2e6 100644 --- a/src/pages/workspace/WorkspaceNewRoomPage.tsx +++ b/src/pages/workspace/WorkspaceNewRoomPage.tsx @@ -92,7 +92,7 @@ function WorkspaceNewRoomPage() { * @param values - form input values passed by the Form component */ const submit = (values: FormOnyxValues) => { - const participants = [session?.accountID ?? -1]; + const participants = [session?.accountID ?? CONST.DEFAULT_NUMBER_ID]; const parsedDescription = getParsedComment(values.reportDescription ?? '', {policyID}); const policyReport = buildOptimisticChatReport( participants, @@ -164,7 +164,7 @@ function WorkspaceNewRoomPage() { } else if (isReservedRoomName(values.roomName)) { // Certain names are reserved for default rooms and should not be used for policy rooms. addErrorMessage(errors, 'roomName', translate('newRoomPage.roomNameReservedError', {reservedName: values.roomName})); - } else if (isExistingRoomName(values.roomName, reports, values.policyID ?? '-1')) { + } else if (isExistingRoomName(values.roomName, reports, values.policyID)) { // Certain names are reserved for default rooms and should not be used for policy rooms. addErrorMessage(errors, 'roomName', translate('newRoomPage.roomAlreadyExistsError')); } else if (values.roomName.length > CONST.TITLE_CHARACTER_LIMIT) { diff --git a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx index 9e5ba5bd12f2..2c5d87df2a25 100644 --- a/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx +++ b/src/pages/workspace/WorkspaceProfileDescriptionPage.tsx @@ -9,12 +9,12 @@ import TextInput from '@components/TextInput'; import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {addErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import variables from '@styles/variables'; -import * as Policy from '@userActions/Policy/Policy'; +import {updateWorkspaceDescription} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import AccessOrNotFoundWrapper from './AccessOrNotFoundWrapper'; @@ -49,7 +49,7 @@ function WorkspaceProfileDescriptionPage({policy}: Props) { const errors = {}; if (values.description.length > CONST.DESCRIPTION_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'description', translate('common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT})); + addErrorMessage(errors, 'description', translate('common.error.characterLimitExceedCounter', {length: values.description.length, limit: CONST.DESCRIPTION_LIMIT})); } return errors; @@ -63,7 +63,7 @@ function WorkspaceProfileDescriptionPage({policy}: Props) { return; } - Policy.updateWorkspaceDescription(policy.id, values.description.trim(), policy.description ?? ''); + updateWorkspaceDescription(policy.id, values.description.trim(), policy.description ?? ''); Keyboard.dismiss(); Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack()); }, From 3a2e53441092b2b6f11f600d4fed9c9e6b28836b Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Thu, 23 Jan 2025 13:30:13 +0100 Subject: [PATCH 057/145] fix: not firing onChange handler --- src/components/AmountWithoutCurrencyInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AmountWithoutCurrencyInput.tsx b/src/components/AmountWithoutCurrencyInput.tsx index f68f4b102c61..4d54258dbef0 100644 --- a/src/components/AmountWithoutCurrencyInput.tsx +++ b/src/components/AmountWithoutCurrencyInput.tsx @@ -16,7 +16,7 @@ type AmountFormProps = { } & Partial; function AmountWithoutCurrencyInput( - {value: amount, onInputChange, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, + {value: amount, shouldAllowNegative = false, inputID, name, defaultValue, accessibilityLabel, role, label, ...rest}: AmountFormProps, ref: ForwardedRef, ) { return ( From c2bbcc5f7d75a9744f96229f3874101bbb426477 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Fri, 24 Jan 2025 16:13:10 +0100 Subject: [PATCH 058/145] fix: review comments --- src/components/TextInput/BaseTextInput/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index e01a9a8ce7af..ced78a5bb4e8 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -121,6 +121,7 @@ type CustomBaseTextInputProps = { /** List of markdowns that won't be styled as a markdown */ excludedMarkdownStyles?: Array; + /** A set of styles for markdown elements (such as link, h1, emoji etc.) */ markdownStyle?: MarkdownStyle; /** Whether the clear button should be displayed */ From 298f8175c5d12fc2a43cfb60d3d6fb83f72523c4 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Wed, 29 Jan 2025 18:11:59 +0100 Subject: [PATCH 059/145] fix: display default value --- ios/Podfile.lock | 4 ++-- package-lock.json | 8 ++++---- package.json | 2 +- src/components/Form/FormProvider.tsx | 3 ++- src/components/Form/types.ts | 1 + 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6c45241fff61..3e2e603c95cc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1605,7 +1605,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-advanced-input-mask (1.2.0): + - react-native-advanced-input-mask (1.2.1): - DoubleConversion - ForkInputMask (~> 7.3.2) - glog @@ -3351,7 +3351,7 @@ SPEC CHECKSUMS: React-logger: 26155dc23db5c9038794db915f80bd2044512c2e React-Mapbuffer: ad1ba0205205a16dbff11b8ade6d1b3959451658 React-microtasksnativemodule: e771eb9eb6ace5884ee40a293a0e14a9d7a4343c - react-native-advanced-input-mask: 133eb22a94337d3cd99c5e831a4ee306db5ae6e5 + react-native-advanced-input-mask: 22e3bd2a0f38fada50b475c98bf39d39053097a3 react-native-airship: e10f6823d8da49bbcb2db4bdb16ff954188afccc react-native-app-logs: ee32b6e80bf8d1b883dfc5ac96efa7c1bd9a06a5 react-native-blob-util: 221c61c98ae507b758472ac4d2d489119d1a6c44 diff --git a/package-lock.json b/package-lock.json index 3a7f790713e6..6f294fdcad54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -79,7 +79,7 @@ "react-fast-pdf": "^1.0.22", "react-map-gl": "^7.1.3", "react-native": "0.76.3", - "react-native-advanced-input-mask": "^1.2.0", + "react-native-advanced-input-mask": "1.2.1", "react-native-android-location-enabler": "^2.0.1", "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", @@ -32029,9 +32029,9 @@ } }, "node_modules/react-native-advanced-input-mask": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/react-native-advanced-input-mask/-/react-native-advanced-input-mask-1.2.0.tgz", - "integrity": "sha512-Pk7Yau14Zm5PZhJgvDDYOZb224MxzgMv86hiXIZja7QOXaLlWUwm+Ko1s4mW20p1RKIPba7QsRfr9CzXuLDrLg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-native-advanced-input-mask/-/react-native-advanced-input-mask-1.2.1.tgz", + "integrity": "sha512-qXK6l8f5zOLrWxhrtA2od4R2UsV8OEcvFlZlX5VTp3sB/JlHW/iJd15m8Rgn/mcJFfvnKlHmVVHJefDrUOJFvA==", "license": "MIT", "workspaces": [ "example", diff --git a/package.json b/package.json index afad6cdb1168..3b821385219f 100644 --- a/package.json +++ b/package.json @@ -144,7 +144,7 @@ "react-fast-pdf": "^1.0.22", "react-map-gl": "^7.1.3", "react-native": "0.76.3", - "react-native-advanced-input-mask": "^1.2.0", + "react-native-advanced-input-mask": "1.2.1", "react-native-android-location-enabler": "^2.0.1", "react-native-app-logs": "0.3.1", "react-native-blob-util": "0.19.4", diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 0b84f0034035..bf3746b61776 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -326,7 +326,8 @@ function FormProvider( value: inputValues[inputID], // As the text input is controlled, we never set the defaultValue prop // as this is already happening by the value prop. - defaultValue: undefined, + // If it's uncontrolled, then we set the `defaultValue` prop to actual value + defaultValue: inputProps.uncontrolled ? inputProps.defaultValue : undefined, onTouched: (event) => { if (!inputProps.shouldSetTouchedOnBlurOnly) { setTimeout(() => { diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index d6bcc28e09bf..02cc4e899b32 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -118,6 +118,7 @@ type InputComponentBaseProps = Input autoGrowHeight?: boolean; blurOnSubmit?: boolean; shouldSubmitForm?: boolean; + uncontrolled?: boolean; }; type FormOnyxValues = Omit; From d42fb2cd27a67aefca9dbb13dbb5839d663c95aa Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 30 Jan 2025 01:06:32 +0530 Subject: [PATCH 060/145] fix: Workflows - Street doesn't get automatically filled out for a specific address. Signed-off-by: krishna2323 --- src/components/AddressSearch/index.tsx | 10 ++++++++-- src/libs/GooglePlacesUtils.ts | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 0feabf9b6092..d3f3cf791798 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -162,7 +162,13 @@ function AddressSearch( // Make sure that the order of keys remains such that the country is always set above the state. // Refer to https://github.com/Expensify/App/issues/15633 for more information. - const {country: countryFallbackLongName = '', state: stateAutoCompleteFallback = '', city: cityAutocompleteFallback = ''} = getPlaceAutocompleteTerms(autocompleteData?.terms ?? []); + const { + country: countryFallbackLongName = '', + state: stateAutoCompleteFallback = '', + city: cityAutocompleteFallback = '', + street: streetAutocompleteFallback, + streetNumber: streetNumberAutocompleteFallback, + } = getPlaceAutocompleteTerms(autocompleteData?.terms ?? []); const countryFallback = Object.keys(CONST.ALL_COUNTRIES).find((country) => country === countryFallbackLongName); @@ -170,7 +176,7 @@ function AddressSearch( const country = countryPrimary || countryFallback || ''; const values = { - street: `${streetNumber} ${streetName}`.trim(), + street: `${streetNumber || streetNumberAutocompleteFallback} ${streetName || streetAutocompleteFallback}`.trim(), name: details.name ?? '', // Autocomplete returns any additional valid address fragments (e.g. Apt #) as subpremise. street2: subpremise, diff --git a/src/libs/GooglePlacesUtils.ts b/src/libs/GooglePlacesUtils.ts index 1f9fd838f8d4..6329939f31df 100644 --- a/src/libs/GooglePlacesUtils.ts +++ b/src/libs/GooglePlacesUtils.ts @@ -33,7 +33,7 @@ function getAddressComponents(addressComponents: AddressComponent[], fieldsToExt } type AddressTerm = {value: string}; -type GetPlaceAutocompleteTermsResultKey = 'country' | 'state' | 'city' | 'street'; +type GetPlaceAutocompleteTermsResultKey = 'country' | 'state' | 'city' | 'street' | 'streetNumber'; type GetPlaceAutocompleteTermsResult = Partial>; /** @@ -41,7 +41,7 @@ type GetPlaceAutocompleteTermsResult = Partial */ function getPlaceAutocompleteTerms(addressTerms: AddressTerm[]): GetPlaceAutocompleteTermsResult { - const fieldsToExtract: GetPlaceAutocompleteTermsResultKey[] = ['country', 'state', 'city', 'street']; + const fieldsToExtract: GetPlaceAutocompleteTermsResultKey[] = ['country', 'state', 'city', 'street', 'streetNumber']; const result: GetPlaceAutocompleteTermsResult = {}; fieldsToExtract.forEach((fieldToExtract, index) => { const fieldTermIndex = addressTerms.length - (index + 1); From fbbe23b5af14019aa888e6a14aa2c49e6f9e702e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 30 Jan 2025 01:40:55 +0530 Subject: [PATCH 061/145] fix jest test. Signed-off-by: krishna2323 --- tests/unit/GooglePlacesUtilsTest.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/GooglePlacesUtilsTest.ts b/tests/unit/GooglePlacesUtilsTest.ts index 9a7649158c8f..b3f448a46785 100644 --- a/tests/unit/GooglePlacesUtilsTest.ts +++ b/tests/unit/GooglePlacesUtilsTest.ts @@ -85,6 +85,7 @@ describe('GooglePlacesUtilsTest', () => { state: 'Bangladesh Border Road', city: '', street: '', + streetNumber: '', }); }); }); From e79c90bc01126c9347dddba23a44ac06f33bac3c Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 30 Jan 2025 14:19:20 +0530 Subject: [PATCH 062/145] fix TS issues. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 2 +- src/libs/SearchUIUtils.ts | 2 +- src/libs/actions/IOU.ts | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index d180eacd1c61..6dd6144c7203 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -250,7 +250,7 @@ function ReportPreview({ const isArchived = isArchivedReportWithID(iouReport?.reportID); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const filteredTransactions = transactions?.filter((transaction) => transaction) ?? []; - const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, transactionViolations); + const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions); const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(iouReport); diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 4721f7c76451..dcd859cb4c10 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -363,7 +363,7 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr } // We check for isAllowedToApproveExpenseReport because if the policy has preventSelfApprovals enabled, we disable the Submit action and in that case we want to show the View action instead - if (canSubmitReport(report, policy, allReportTransactions, allViolations) && isAllowedToApproveExpenseReport) { + if (canSubmitReport(report, policy, allReportTransactions) && isAllowedToApproveExpenseReport) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index e595bd181aee..a0f2814ea4a0 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7918,15 +7918,14 @@ function canSubmitReport( report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy, transactions: OnyxTypes.Transaction[] | SearchTransaction[], - allViolations?: OnyxCollection, ) { const currentUserAccountID = getCurrentUserAccountID(); const isOpenExpenseReport = isOpenExpenseReportReportUtils(report); const isArchived = isArchivedReportWithID(report?.reportID); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const transactionIDList = transactions.map((transaction) => transaction.transactionID); - const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDList, allViolations); - const hasBrokenConnectionViolation = shouldShowBrokenConnectionViolation(transactionIDList, report, policy, allViolations); + const hasAllPendingRTERViolations = allHavePendingRTERViolation(transactionIDList); + const hasBrokenConnectionViolation = shouldShowBrokenConnectionViolation(transactionIDList, report, policy); const hasOnlyPendingCardOrScanFailTransactions = transactions.length > 0 && From f21052ff23b29094fcfe4c47eccd391da8fa23d1 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Thu, 30 Jan 2025 16:33:20 +0300 Subject: [PATCH 063/145] fix canAddOrDeleteTransactions logic --- src/libs/ReportUtils.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5e50f69e28fa..028c2087c79e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2007,14 +2007,12 @@ function getChildReportNotificationPreference(reportAction: OnyxInputOrEntry): boolean { + if (moneyRequestReport?.reportID == '4070865872455813') debugger; if (!isMoneyRequestReport(moneyRequestReport) || isArchivedReportWithID(moneyRequestReport?.reportID)) { return false; } const policy = getPolicy(moneyRequestReport?.policyID); - if (isInstantSubmitEnabled(policy) && isSubmitAndClose(policy) && hasOnlyNonReimbursableTransactions(moneyRequestReport?.reportID)) { - return false; - } if (isInstantSubmitEnabled(policy) && isSubmitAndClose(policy) && !arePaymentsEnabled(policy)) { return false; @@ -2047,6 +2045,11 @@ function canAddTransaction(moneyRequestReport: OnyxEntry): boolean { return false; } + const policy = getPolicy(moneyRequestReport?.policyID); + if (isInstantSubmitEnabled(policy) && isSubmitAndClose(policy) && hasOnlyNonReimbursableTransactions(moneyRequestReport?.reportID)) { + return false; + } + return canAddOrDeleteTransactions(moneyRequestReport); } From cc23003a09835e0af5e3ffd50dad645b06703dd6 Mon Sep 17 00:00:00 2001 From: FitseTLT Date: Thu, 30 Jan 2025 17:29:52 +0300 Subject: [PATCH 064/145] minor fix --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 028c2087c79e..ae5fc41c063d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2007,7 +2007,6 @@ function getChildReportNotificationPreference(reportAction: OnyxInputOrEntry): boolean { - if (moneyRequestReport?.reportID == '4070865872455813') debugger; if (!isMoneyRequestReport(moneyRequestReport) || isArchivedReportWithID(moneyRequestReport?.reportID)) { return false; } From 5d89c621b384363043e6d4998a374fe3c8e56177 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 30 Jan 2025 14:44:44 -0700 Subject: [PATCH 065/145] exclude hidden reports --- src/libs/OptionsListUtils.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index a8e23888db0d..55f08aebff91 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -204,6 +204,7 @@ type GetOptionsConfig = { recentAttendees?: Attendee[]; shouldSeparateWorkspaceChat?: boolean; shouldSeparateSelfDMChat?: boolean; + excludeHiddenReports?: boolean; } & GetValidReportsConfig; type GetUserToInviteConfig = { @@ -1420,6 +1421,7 @@ function getValidOptions( selectedOptions = [], shouldSeparateSelfDMChat = false, shouldSeparateWorkspaceChat = false, + excludeHiddenReports = false, ...config }: GetOptionsConfig = {}, ): Options { @@ -1513,6 +1515,10 @@ function getValidOptions( recentReportOptions = recentReportOptions.filter((option) => !option.isSelfDM); } + if (excludeHiddenReports) { + recentReportOptions = recentReportOptions.filter((option) => option.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN); + } + return { personalDetails: personalDetailsOptions, recentReports: recentReportOptions, @@ -1543,6 +1549,7 @@ function getSearchOptions(options: OptionList, betas: Beta[] = [], isUsedInChatF includeTasks: true, includeSelfDM: true, shouldBoldTitleByDefault: !isUsedInChatFinder, + excludeHiddenReports: true, }); const orderedOptions = orderOptions(optionList); Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); From a482d4e995274aca921549a26c1224077dbe6f5e Mon Sep 17 00:00:00 2001 From: daledah Date: Fri, 31 Jan 2025 11:23:18 +0700 Subject: [PATCH 066/145] refactor: change functions names --- src/libs/Navigation/Navigation.ts | 8 ++++---- src/libs/ReportUtils.ts | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 85dfcdded31f..f68eaf5d57b0 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -428,7 +428,7 @@ function getTopMostCentralPaneRouteFromRootState() { return getTopmostCentralPaneRoute(navigationRef.getRootState() as State); } -function getPreviousTrackReport(reportID?: string) { +function getReportRouteByID(reportID?: string) { if (!reportID) { return null; } @@ -449,7 +449,7 @@ function removeScreenFromNavigationState(screen: Screen) { }); } -function removeScreenByKey(key: string) { +function removeScreenFromNavigationStateByKey(key: string) { const state = navigationRef.getRootState(); const routes = state.routes.filter((item) => item.key !== key); @@ -487,8 +487,8 @@ export default { setNavigationActionToMicrotaskQueue, getTopMostCentralPaneRouteFromRootState, removeScreenFromNavigationState, - getPreviousTrackReport, - removeScreenByKey, + getReportRouteByID, + removeScreenFromNavigationStateByKey, }; export {navigationRef}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 35fcd639b2fb..8747ce8fd80f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4437,7 +4437,7 @@ function goBackToDetailsPage(report: OnyxEntry, backTo?: string) { } } -function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP?: boolean, reportID?: string) { +function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP?: boolean, reportIDToRemove?: string) { if (!backRoute) { return; } @@ -4447,10 +4447,11 @@ function navigateBackOnDeleteTransaction(backRoute: Route | undefined, isFromRHP return; } if (isFromRHP) { - if (reportID) { - const trackReport = Navigation.getPreviousTrackReport(reportID); - if (trackReport?.key) { - Navigation.removeScreenByKey(trackReport.key); + const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportIDToRemove}`]; + if (report && isTrackExpenseReport(report)) { + const trackReportRoute = Navigation.getReportRouteByID(reportIDToRemove); + if (trackReportRoute?.key) { + Navigation.removeScreenFromNavigationStateByKey(trackReportRoute.key); } } Navigation.isNavigationReady().then(() => Navigation.dismissModal()); From e10d4f21126df41f3fee08d846677356ffa4fb6b Mon Sep 17 00:00:00 2001 From: jayeshmangwani Date: Fri, 31 Jan 2025 12:06:45 +0530 Subject: [PATCH 067/145] updated spanish copy to remove duplicate plan name --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 70c2b16975db..a4a1f291988e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4523,7 +4523,7 @@ const translations = { benefit3: 'Flujos de aprobación de varios niveles', benefit4: 'Controles de seguridad mejorados', toUpgrade: 'Para mejorar, haz clic', - selectWorkspace: 'selecciona un espacio de trabajo y cambia el tipo de plan a Controlar.', + selectWorkspace: 'selecciona un espacio de trabajo y cambia el tipo de plan a', }, }, }, @@ -4543,7 +4543,7 @@ const translations = { headsUp: '¡Atención!', multiWorkspaceNote: 'Tendrás que bajar de categoría todos tus espacios de trabajo antes de tu primer pago mensual para comenzar una suscripción con la tasa del plan Recopilar. Haz clic', - selectStep: '> selecciona cada espacio de trabajo > cambia el tipo de plan a Recopilar.', + selectStep: '> selecciona cada espacio de trabajo > cambia el tipo de plan a', }, }, completed: { From 5c48cfc868bfbac43d54372cb1947adc07de6af7 Mon Sep 17 00:00:00 2001 From: Krishna Date: Fri, 31 Jan 2025 12:28:35 +0530 Subject: [PATCH 068/145] Add default value for street and street number --- src/components/AddressSearch/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index d3f3cf791798..d4c56c43835f 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -166,8 +166,8 @@ function AddressSearch( country: countryFallbackLongName = '', state: stateAutoCompleteFallback = '', city: cityAutocompleteFallback = '', - street: streetAutocompleteFallback, - streetNumber: streetNumberAutocompleteFallback, + street: streetAutocompleteFallback = '', + streetNumber: streetNumberAutocompleteFallback = '', } = getPlaceAutocompleteTerms(autocompleteData?.terms ?? []); const countryFallback = Object.keys(CONST.ALL_COUNTRIES).find((country) => country === countryFallbackLongName); From fbe9c896738f2a7a123e46199a1823d0063667d4 Mon Sep 17 00:00:00 2001 From: jayeshmangwani Date: Fri, 31 Jan 2025 15:32:34 +0530 Subject: [PATCH 069/145] updated spanish copies for upgrade/downgrade modal note --- src/languages/es.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index a4a1f291988e..6cd0b4473eec 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4522,7 +4522,7 @@ const translations = { benefit2: 'Reglas inteligentes de gastos', benefit3: 'Flujos de aprobación de varios niveles', benefit4: 'Controles de seguridad mejorados', - toUpgrade: 'Para mejorar, haz clic', + toUpgrade: 'Para mejorar, haz clic en', selectWorkspace: 'selecciona un espacio de trabajo y cambia el tipo de plan a', }, }, @@ -4542,7 +4542,7 @@ const translations = { benefit4: 'Controles de seguridad mejorados', headsUp: '¡Atención!', multiWorkspaceNote: - 'Tendrás que bajar de categoría todos tus espacios de trabajo antes de tu primer pago mensual para comenzar una suscripción con la tasa del plan Recopilar. Haz clic', + 'Tendrás que bajar de categoría todos tus espacios de trabajo antes de tu primer pago mensual para comenzar una suscripción con la tasa del plan Recopilar. Haz clic en', selectStep: '> selecciona cada espacio de trabajo > cambia el tipo de plan a', }, }, From ca5fe70350c44d525ed491b0ee66dfa219b35469 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Thu, 9 Jan 2025 15:20:11 +0100 Subject: [PATCH 070/145] Prevent app from crash when putting app in background from ND --- .../setupCustomAndroidBackHandler/index.android.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts b/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts index d31c3693d495..bdea8c157425 100644 --- a/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts +++ b/src/libs/Navigation/setupCustomAndroidBackHandler/index.android.ts @@ -1,5 +1,5 @@ import {findFocusedRoute, StackActions} from '@react-navigation/native'; -import {BackHandler, NativeModules} from 'react-native'; +import {BackHandler} from 'react-native'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import getTopmostCentralPaneRoute from '@navigation/getTopmostCentralPaneRoute'; import navigationRef from '@navigation/navigationRef'; @@ -22,12 +22,6 @@ function setupCustomAndroidBackHandler() { return false; } - const isLastScreenOnStack = bottomTabRoutes.length === 1 && rootState?.routes?.length === 1; - - if (NativeModules.HybridAppModule && isLastScreenOnStack) { - NativeModules.HybridAppModule.exitApp(); - } - // Handle back press on the search page. // We need to pop two screens, from the central pane and from the bottom tab. if (bottomTabRoutes[bottomTabRoutes.length - 1].name === SCREENS.SEARCH.BOTTOM_TAB && focusedRoute?.name === SCREENS.SEARCH.CENTRAL_PANE) { From d725507b8226c8fdb9e2b306e326eabf40e957ce Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 10 Jan 2025 14:26:37 +0100 Subject: [PATCH 071/145] remove pendingIntent functionality & clean exitApp method --- src/types/modules/react-native.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index cbe3f10cbae8..ae63bf77b2a0 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -9,7 +9,6 @@ type HybridAppModule = { closeReactNativeApp: (shouldSignOut: boolean, shouldSetNVP: boolean) => void; completeOnboarding: (status: boolean) => void; switchAccount: (newDotCurrentAccountEmail: string, authToken: string, policyID: string, accountID: string) => void; - exitApp: () => void; }; type RNTextInputResetModule = { From 27d708fce981ff838961e3a096946fc7f2c06fec Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Tue, 14 Jan 2025 12:37:38 +0100 Subject: [PATCH 072/145] Do not duplicate isNavigationReady call --- .../subscribePushNotification/index.ts | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts index 237a615b570a..59f1384c0ce9 100644 --- a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts +++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts @@ -116,37 +116,35 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati const policyEmployeeAccountIDs = policyID ? getPolicyEmployeeAccountIDs(policyID) : []; const reportBelongsToWorkspace = policyID && !isEmptyObject(report) && doesReportBelongToWorkspace(report, policyEmployeeAccountIDs, policyID); - Navigation.isNavigationReady() - .then(Navigation.waitForProtectedRoutes) - .then(() => { - // The attachment modal remains open when navigating to the report so we need to close it - Modal.close(() => { - try { - // Get rid of the transition screen, if it is on the top of the stack - if (NativeModules.HybridAppModule && Navigation.getActiveRoute().includes(ROUTES.TRANSITION_BETWEEN_APPS)) { - Navigation.goBack(); - } - // If a chat is visible other than the one we are trying to navigate to, then we need to navigate back - if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) { - Navigation.goBack(); - } - - Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); - if (!reportBelongsToWorkspace) { - Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); - } - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); - updateLastVisitedPath(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); - } catch (error) { - let errorMessage = String(error); - if (error instanceof Error) { - errorMessage = error.message; - } - - Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage}); + Navigation.waitForProtectedRoutes().then(() => { + // The attachment modal remains open when navigating to the report so we need to close it + Modal.close(() => { + try { + // Get rid of the transition screen, if it is on the top of the stack + if (NativeModules.HybridAppModule && Navigation.getActiveRoute().includes(ROUTES.TRANSITION_BETWEEN_APPS)) { + Navigation.goBack(); } - }); + // If a chat is visible other than the one we are trying to navigate to, then we need to navigate back + if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) { + Navigation.goBack(); + } + + Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); + if (!reportBelongsToWorkspace) { + Navigation.navigateWithSwitchPolicyID({route: ROUTES.HOME}); + } + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); + updateLastVisitedPath(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); + } catch (error) { + let errorMessage = String(error); + if (error instanceof Error) { + errorMessage = error.message; + } + + Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage}); + } }); + }); return Promise.resolve(); } From 6a587df4566ce868f7cfea751ea14b90a7d96fee Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 20 Jan 2025 15:39:52 +0100 Subject: [PATCH 073/145] fix crashes (util they're fixed on main) --- .../reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt index 47e4196f37c1..a76c45c7a8b7 100644 --- a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt +++ b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt @@ -14,6 +14,7 @@ import android.content.IntentFilter import android.os.Build import android.os.PersistableBundle import android.util.Log +import androidx.core.content.ContextCompat class ReactNativeBackgroundTaskModule internal constructor(context: ReactApplicationContext) : ReactNativeBackgroundTaskSpec(context) { From 88c6c6782c07e5102c43651578a3241aa98b0706 Mon Sep 17 00:00:00 2001 From: war-in Date: Tue, 21 Jan 2025 18:12:03 +0100 Subject: [PATCH 074/145] fix status bar color when returning to OD --- src/pages/settings/InitialSettingsPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index 13d0d1d74802..e5b269a7be6e 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -8,6 +8,7 @@ import type {ValueOf} from 'type-fest'; import AccountSwitcher from '@components/AccountSwitcher'; import AccountSwitcherSkeletonView from '@components/AccountSwitcherSkeletonView'; import ConfirmModal from '@components/ConfirmModal'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -100,6 +101,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const [allConnectionSyncProgresses] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}`); const {setInitialURL} = useContext(InitialURLContext); + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); const subscriptionPlan = useSubscriptionPlan(); @@ -243,6 +245,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr action: () => { NativeModules.HybridAppModule.closeReactNativeApp(false, true); setInitialURL(undefined); + setRootStatusBarEnabled(false); }, } : { @@ -285,7 +288,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr }, ], }; - }, [styles.pt4, signOut, setInitialURL, shouldOpenBookACall, isActingAsDelegate]); + }, [styles.pt4, setInitialURL, setRootStatusBarEnabled, isActingAsDelegate, shouldOpenBookACall, signOut]); /** * Retuns JSX.Element with menu items From cbe158d1f37d69b5975ebcb7a34a10f7a1a3f45e Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 22 Jan 2025 15:58:55 +0100 Subject: [PATCH 075/145] don't show bootsplash when app in background --- src/App.tsx | 5 +++-- src/Expensify.tsx | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f9403e258af1..df50bb1d3988 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,6 +46,7 @@ import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; type AppProps = { /** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ url?: Route; + withoutBootsplash?: boolean; }; LogBox.ignoreLogs([ @@ -61,7 +62,7 @@ const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; -function App({url}: AppProps) { +function App({url, withoutBootsplash}: AppProps) { useDefaultDragAndDrop(); OnyxUpdateManager(); @@ -105,7 +106,7 @@ function App({url}: AppProps) { - + diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 1d0100add00f..0e9643aa6f3b 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -76,7 +76,7 @@ type ExpensifyProps = { /** Last visited path in the app */ lastVisitedPath: OnyxEntry; }; -function Expensify() { +function Expensify({withoutBootsplash = false}) { const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); const [isOnyxMigrated, setIsOnyxMigrated] = useState(false); @@ -294,7 +294,7 @@ function Expensify() { shouldShowRequire2FAModal={shouldShowRequire2FAModal} /> )} - {shouldHideSplash && } + {shouldHideSplash && !withoutBootsplash && } ); } From fd5f2948d7d9b2b83b2a41cabebe074ad26d01a0 Mon Sep 17 00:00:00 2001 From: war-in Date: Wed, 22 Jan 2025 16:19:40 +0100 Subject: [PATCH 076/145] rename new prop --- src/App.tsx | 10 ++++++---- src/Expensify.tsx | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index df50bb1d3988..c8b3744d7a7b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,10 +43,12 @@ import type {Route} from './ROUTES'; import './setup/backgroundTask'; import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; +/** Values passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ type AppProps = { - /** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */ + /** URL containing all necessary data to run React Native app (e.g. login data) */ url?: Route; - withoutBootsplash?: boolean; + /** Specifies if the SplashScreenHider should be mounted */ + withBootsplash?: boolean; }; LogBox.ignoreLogs([ @@ -62,7 +64,7 @@ const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; -function App({url, withoutBootsplash}: AppProps) { +function App({url, withBootsplash}: AppProps) { useDefaultDragAndDrop(); OnyxUpdateManager(); @@ -106,7 +108,7 @@ function App({url, withoutBootsplash}: AppProps) { - + diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 0e9643aa6f3b..cfca5c93c0ef 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -76,7 +76,7 @@ type ExpensifyProps = { /** Last visited path in the app */ lastVisitedPath: OnyxEntry; }; -function Expensify({withoutBootsplash = false}) { +function Expensify({withBootsplash = true}) { const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); const [isOnyxMigrated, setIsOnyxMigrated] = useState(false); @@ -294,7 +294,7 @@ function Expensify({withoutBootsplash = false}) { shouldShowRequire2FAModal={shouldShowRequire2FAModal} /> )} - {shouldHideSplash && !withoutBootsplash && } + {shouldHideSplash && withBootsplash && } ); } From 8a183d30a52cb678b557917e3392c0bc2e1d013d Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 23 Jan 2025 12:51:42 +0100 Subject: [PATCH 077/145] revert bootsplash changes --- src/App.tsx | 6 ++---- src/Expensify.tsx | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c8b3744d7a7b..3513cb23953b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -47,8 +47,6 @@ import {SplashScreenStateContextProvider} from './SplashScreenStateContext'; type AppProps = { /** URL containing all necessary data to run React Native app (e.g. login data) */ url?: Route; - /** Specifies if the SplashScreenHider should be mounted */ - withBootsplash?: boolean; }; LogBox.ignoreLogs([ @@ -64,7 +62,7 @@ const fill = {flex: 1}; const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children; -function App({url, withBootsplash}: AppProps) { +function App({url}: AppProps) { useDefaultDragAndDrop(); OnyxUpdateManager(); @@ -108,7 +106,7 @@ function App({url, withBootsplash}: AppProps) { - + diff --git a/src/Expensify.tsx b/src/Expensify.tsx index cfca5c93c0ef..1d0100add00f 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -76,7 +76,7 @@ type ExpensifyProps = { /** Last visited path in the app */ lastVisitedPath: OnyxEntry; }; -function Expensify({withBootsplash = true}) { +function Expensify() { const appStateChangeListener = useRef(null); const [isNavigationReady, setIsNavigationReady] = useState(false); const [isOnyxMigrated, setIsOnyxMigrated] = useState(false); @@ -294,7 +294,7 @@ function Expensify({withBootsplash = true}) { shouldShowRequire2FAModal={shouldShowRequire2FAModal} /> )} - {shouldHideSplash && withBootsplash && } + {shouldHideSplash && } ); } From 0b1755b649659b117527eb575cc32964866e2fd8 Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 27 Jan 2025 17:05:18 +0100 Subject: [PATCH 078/145] fix status bar on travel page --- src/components/ScreenWrapper.tsx | 3 +++ src/libs/actions/Travel.ts | 8 +++++++- src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx | 5 ++++- src/pages/Search/EmptySearchView.tsx | 6 ++++-- src/pages/Travel/ManageTrips.tsx | 6 ++++-- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index b22b4eac3fc6..3bf199057205 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -5,6 +5,7 @@ import type {StyleProp, ViewStyle} from 'react-native'; import {Keyboard, NativeModules, PanResponder, View} from 'react-native'; import {PickerAvoidingView} from 'react-native-picker-select'; import type {EdgeInsets} from 'react-native-safe-area-context'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -152,6 +153,7 @@ function ScreenWrapper( const {windowHeight} = useWindowDimensions(shouldUseCachedViewportHeight); // since Modals are drawn in separate native view hierarchy we should always add paddings const ignoreInsetsConsumption = !useContext(ModalContext).default; + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for a case where we want to show the offline indicator only on small screens // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth @@ -171,6 +173,7 @@ function ScreenWrapper( UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => { NativeModules.HybridAppModule?.closeReactNativeApp(false, false); + setRootStatusBarEnabled(false); }); const panResponder = useRef( diff --git a/src/libs/actions/Travel.ts b/src/libs/actions/Travel.ts index 1886885587c4..2aeb04b60f1b 100644 --- a/src/libs/actions/Travel.ts +++ b/src/libs/actions/Travel.ts @@ -114,7 +114,12 @@ function provisionDomain(domain: string) { Navigation.navigate(ROUTES.TRAVEL_TCS.getRoute(domain)); } -function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessage: Dispatch>, ctaErrorMessage = ''): void { +function bookATrip( + translate: LocaleContextProps['translate'], + setCtaErrorMessage: Dispatch>, + setRootStatusBarEnabled: (isEnabled: boolean) => void, + ctaErrorMessage = '', +): void { if (!activePolicyID) { return; } @@ -138,6 +143,7 @@ function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessag Log.info('[HybridApp] Returning to OldDot after opening TravelDot'); NativeModules.HybridAppModule.closeReactNativeApp(false, false); + setRootStatusBarEnabled(false); }) ?.catch(() => { setCtaErrorMessage(translate('travel.errorMessage')); diff --git a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx index 12722e87f05a..56cf4041c9c3 100644 --- a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx +++ b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx @@ -1,7 +1,8 @@ -import React, {useMemo, useState} from 'react'; +import React, {useContext, useMemo, useState} from 'react'; import {NativeModules} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -34,6 +35,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE const [onboardingPolicyID] = useOnyx(ONYXKEYS.ONBOARDING_POLICY_ID); const [onboardingAdminsChatReportID] = useOnyx(ONYXKEYS.ONBOARDING_ADMINS_CHAT_REPORT_ID); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const paidGroupPolicy = Object.values(allPolicies ?? {}).find(PolicyUtils.isPaidGroupPolicy); @@ -106,6 +108,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE } NativeModules.HybridAppModule.closeReactNativeApp(false, true); + setRootStatusBarEnabled(false); }} pressOnEnter /> diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index c27edc2e70e2..9b38157dfcbb 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -1,8 +1,9 @@ -import React, {useMemo, useState} from 'react'; +import React, {useContext, useMemo, useState} from 'react'; import {Linking, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {OnyxCollection} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import DotIndicatorMessage from '@components/DotIndicatorMessage'; import EmptyStateComponent from '@components/EmptyStateComponent'; import type {FeatureListItem} from '@components/FeatureList'; @@ -60,6 +61,7 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { const shouldRedirectToExpensifyClassic = useMemo(() => { return areAllGroupPoliciesExpenseChatDisabled((allPolicies as OnyxCollection) ?? {}); }, [allPolicies]); + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const [ctaErrorMessage, setCtaErrorMessage] = useState(''); @@ -133,7 +135,7 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { buttons: [ { buttonText: translate('search.searchResults.emptyTripResults.buttonText'), - buttonAction: () => bookATrip(translate, setCtaErrorMessage, ctaErrorMessage), + buttonAction: () => bookATrip(translate, setCtaErrorMessage, setRootStatusBarEnabled, ctaErrorMessage), success: true, }, ], diff --git a/src/pages/Travel/ManageTrips.tsx b/src/pages/Travel/ManageTrips.tsx index ac427d1d56c9..9a9b59b002c1 100644 --- a/src/pages/Travel/ManageTrips.tsx +++ b/src/pages/Travel/ManageTrips.tsx @@ -1,6 +1,7 @@ -import React, {useState} from 'react'; +import React, {useContext, useState} from 'react'; import {Linking, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import type {FeatureListItem} from '@components/FeatureList'; import FeatureList from '@components/FeatureList'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -34,6 +35,7 @@ function ManageTrips() { const {translate} = useLocalize(); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const policy = usePolicy(activePolicyID); + const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); const [ctaErrorMessage, setCtaErrorMessage] = useState(''); @@ -55,7 +57,7 @@ function ManageTrips() { ctaText={translate('travel.bookTravel')} ctaAccessibilityLabel={translate('travel.bookTravel')} onCtaPress={() => { - bookATrip(translate, setCtaErrorMessage, ctaErrorMessage); + bookATrip(translate, setCtaErrorMessage, setRootStatusBarEnabled, ctaErrorMessage); }} ctaErrorMessage={ctaErrorMessage} illustration={LottieAnimations.TripsEmptyState} From de06e4f31e86bff60d0cb995d668c70c0aaa2c38 Mon Sep 17 00:00:00 2001 From: war-in Date: Mon, 27 Jan 2025 17:41:37 +0100 Subject: [PATCH 079/145] fix lint --- src/components/ScreenWrapper.tsx | 2 +- .../BaseOnboardingEmployees.tsx | 20 +++++++++---------- src/pages/Search/EmptySearchView.tsx | 6 +++--- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 3bf199057205..638ef0737ed5 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -5,7 +5,6 @@ import type {StyleProp, ViewStyle} from 'react-native'; import {Keyboard, NativeModules, PanResponder, View} from 'react-native'; import {PickerAvoidingView} from 'react-native-picker-select'; import type {EdgeInsets} from 'react-native-safe-area-context'; -import CustomStatusBarAndBackgroundContext from '@components/CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import useEnvironment from '@hooks/useEnvironment'; import useInitialDimensions from '@hooks/useInitialWindowDimensions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -20,6 +19,7 @@ import addViewportResizeListener from '@libs/VisualViewport'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; +import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext'; import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen'; import type FocusTrapForScreenProps from './FocusTrap/FocusTrapForScreen/FocusTrapProps'; import HeaderGap from './HeaderGap'; diff --git a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx index 56cf4041c9c3..2d951474f459 100644 --- a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx +++ b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx @@ -13,11 +13,11 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import {completeOnboarding} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; -import * as PolicyUtils from '@libs/PolicyUtils'; -import * as Policy from '@userActions/Policy/Policy'; -import * as Report from '@userActions/Report'; -import * as Welcome from '@userActions/Welcome'; +import {isPaidGroupPolicy} from '@libs/PolicyUtils'; +import {createWorkspace, generatePolicyID} from '@userActions/Policy/Policy'; +import {setOnboardingAdminsChatReportID, setOnboardingCompanySize, setOnboardingPolicyID} from '@userActions/Welcome'; import CONST from '@src/CONST'; import type {OnboardingCompanySize} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -37,7 +37,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext); - const paidGroupPolicy = Object.values(allPolicies ?? {}).find(PolicyUtils.isPaidGroupPolicy); + const paidGroupPolicy = Object.values(allPolicies ?? {}).find(isPaidGroupPolicy); const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout(); const [selectedCompanySize, setSelectedCompanySize] = useState(onboardingCompanySize); @@ -71,19 +71,19 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE setError(translate('onboarding.errorSelection')); return; } - Welcome.setOnboardingCompanySize(selectedCompanySize); + setOnboardingCompanySize(selectedCompanySize); const shouldCreateWorkspace = !onboardingPolicyID && !paidGroupPolicy; // We need `adminsChatReportID` for `Report.completeOnboarding`, but at the same time, we don't want to call `Policy.createWorkspace` more than once. // If we have already created a workspace, we want to reuse the `onboardingAdminsChatReportID` and `onboardingPolicyID`. const {adminsChatReportID, policyID} = shouldCreateWorkspace - ? Policy.createWorkspace(undefined, true, '', Policy.generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM) + ? createWorkspace(undefined, true, '', generatePolicyID(), CONST.ONBOARDING_CHOICES.MANAGE_TEAM) : {adminsChatReportID: onboardingAdminsChatReportID, policyID: onboardingPolicyID}; if (shouldCreateWorkspace) { - Welcome.setOnboardingAdminsChatReportID(adminsChatReportID); - Welcome.setOnboardingPolicyID(policyID); + setOnboardingAdminsChatReportID(adminsChatReportID); + setOnboardingPolicyID(policyID); } // For MICRO companies (1-10 employees), we want to remain on NewDot. @@ -95,7 +95,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE // For other company sizes we want to complete onboarding here. // At this point `onboardingPurposeSelected` should always exist as we set it in `BaseOnboardingPurpose`. if (onboardingPurposeSelected) { - Report.completeOnboarding( + completeOnboarding( onboardingPurposeSelected, CONST.ONBOARDING_MESSAGES[onboardingPurposeSelected], undefined, diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 9b38157dfcbb..8885c7c7be16 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -239,11 +239,11 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { styles.textAlignLeft, styles.emptyStateFolderWebStyles, subtitleComponent, - hasSeenTour, + hasResults, + setRootStatusBarEnabled, ctaErrorMessage, + hasSeenTour, navatticURL, - shouldRedirectToExpensifyClassic, - hasResults, viewTourTaskReport, canModifyTheTask, canActionTheTask, From 1b908ba26f83c13a1cd8ec37040a9972242d8207 Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Sat, 1 Feb 2025 00:10:50 +1300 Subject: [PATCH 080/145] Fix storybook --- src/stories/Composer.stories.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/stories/Composer.stories.tsx b/src/stories/Composer.stories.tsx index a92dc0e789a0..1c2e06199798 100644 --- a/src/stories/Composer.stories.tsx +++ b/src/stories/Composer.stories.tsx @@ -5,7 +5,7 @@ import React, {useState} from 'react'; import {Image, View} from 'react-native'; import type {FileObject} from '@components/AttachmentModal'; import Composer from '@components/Composer'; -import type {ComposerProps} from '@components/Composer/types'; +import type {ComposerProps, CustomSelectionChangeEvent, TextSelection} from '@components/Composer/types'; import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; import withNavigationFallback from '@components/withNavigationFallback'; @@ -33,6 +33,7 @@ function Default(props: ComposerProps) { const [pastedFile, setPastedFile] = useState(null); const [comment, setComment] = useState(props.defaultValue); const renderedHTML = parser.replace(comment ?? ''); + const [selection, setSelection] = useState(() => ({start: props.defaultValue?.length ?? 0, end: props.defaultValue?.length ?? 0, positionX: 0, positionY: 0})); return ( @@ -41,8 +42,13 @@ function Default(props: ComposerProps) { // eslint-disable-next-line react/jsx-props-no-spreading {...props} multiline + value={comment} onChangeText={setComment} onPasteFile={setPastedFile} + selection={selection} + onSelectionChange={(e: CustomSelectionChangeEvent) => { + setSelection(e.nativeEvent.selection); + }} style={[defaultStyles.textInputCompose, defaultStyles.w100, defaultStyles.verticalAlignTop]} /> From 5b95e0aa789ad0359ea6fa9b6dee635c9417ac28 Mon Sep 17 00:00:00 2001 From: QichenZhu <57348009+QichenZhu@users.noreply.github.com> Date: Sat, 1 Feb 2025 00:25:51 +1300 Subject: [PATCH 081/145] Fix storybook --- src/stories/Composer.stories.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/stories/Composer.stories.tsx b/src/stories/Composer.stories.tsx index 1c2e06199798..efb8e05614e2 100644 --- a/src/stories/Composer.stories.tsx +++ b/src/stories/Composer.stories.tsx @@ -28,12 +28,17 @@ const story: Meta = { const parser = new ExpensiMark(); +const DEFAULT_VALUE = `Composer can do the following: + + * It can contain MD e.g. *bold* _italic_ + * Supports Pasted Images via Ctrl+V`; + function Default(props: ComposerProps) { const StyleUtils = useStyleUtils(); const [pastedFile, setPastedFile] = useState(null); - const [comment, setComment] = useState(props.defaultValue); + const [comment, setComment] = useState(DEFAULT_VALUE); const renderedHTML = parser.replace(comment ?? ''); - const [selection, setSelection] = useState(() => ({start: props.defaultValue?.length ?? 0, end: props.defaultValue?.length ?? 0, positionX: 0, positionY: 0})); + const [selection, setSelection] = useState(() => ({start: DEFAULT_VALUE.length, end: DEFAULT_VALUE.length, positionX: 0, positionY: 0})); return ( @@ -79,10 +84,6 @@ Default.args = { autoFocus: true, placeholder: 'Compose Text Here', placeholderTextColor: defaultTheme.placeholderText, - defaultValue: `Composer can do the following: - - * It can contain MD e.g. *bold* _italic_ - * Supports Pasted Images via Ctrl+V`, isDisabled: false, maxLines: 16, }; From 50078abc2f8a827e5b40e380ead05d3b1d2f8922 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Fri, 31 Jan 2025 11:31:11 +0100 Subject: [PATCH 082/145] Adjust after cherry-picks --- .../ReactNativeBackgroundTaskModule.kt | 1 - src/pages/Search/EmptySearchView.tsx | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt index a76c45c7a8b7..47e4196f37c1 100644 --- a/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt +++ b/modules/background-task/android/src/main/java/com/expensify/reactnativebackgroundtask/ReactNativeBackgroundTaskModule.kt @@ -14,7 +14,6 @@ import android.content.IntentFilter import android.os.Build import android.os.PersistableBundle import android.util.Log -import androidx.core.content.ContextCompat class ReactNativeBackgroundTaskModule internal constructor(context: ReactApplicationContext) : ReactNativeBackgroundTaskSpec(context) { diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 8885c7c7be16..7ab8268494f7 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -239,11 +239,12 @@ function EmptySearchView({type, hasResults}: EmptySearchViewProps) { styles.textAlignLeft, styles.emptyStateFolderWebStyles, subtitleComponent, - hasResults, + hasSeenTour, setRootStatusBarEnabled, ctaErrorMessage, - hasSeenTour, navatticURL, + shouldRedirectToExpensifyClassic, + hasResults, viewTourTaskReport, canModifyTheTask, canActionTheTask, From a7474ea99ac6053233e4d725473c9a937617a6f9 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Fri, 31 Jan 2025 13:38:22 +0100 Subject: [PATCH 083/145] Install desktop node-modules only when building desktop app --- .github/actions/composite/setupNode/action.yml | 7 ++++++- .github/workflows/deploy.yml | 2 ++ .github/workflows/testBuild.yml | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml index cfa3f9fc191e..51fed0a6a26d 100644 --- a/.github/actions/composite/setupNode/action.yml +++ b/.github/actions/composite/setupNode/action.yml @@ -6,6 +6,10 @@ inputs: description: "Indicates if node is set up for hybrid app" required: false default: 'false' + IS_DESKTOP_BUILD: + description: "Indicates if node is set up for desktop app" + required: false + default: 'false' outputs: cache-hit: @@ -41,6 +45,7 @@ runs: key: ${{ runner.os }}-node-modules-${{ hashFiles('Mobile-Expensify/package-lock.json', 'Mobile-Expensify/patches/**') }} - id: cache-desktop-node-modules + if: inputs.IS_DESKTOP_BUILD == 'true' uses: actions/cache@v4 with: path: desktop/node_modules @@ -60,7 +65,7 @@ runs: command: npm ci - name: Install node packages for desktop submodule - if: steps.cache-desktop-node-modules.outputs.cache-hit != 'true' + if: inputs.IS_DESKTOP_BUILD == 'true' && steps.cache-desktop-node-modules.outputs.cache-hit != 'true' uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 with: timeout_minutes: 30 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0c6ffd4306f7..e7ee3ac4d973 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -301,6 +301,8 @@ jobs: - name: Setup Node uses: ./.github/actions/composite/setupNode + with: + IS_DESKTOP_BUILD: true - name: Decrypt Developer ID Certificate run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 869db3d04be7..1bd4282b2830 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -247,6 +247,8 @@ jobs: - name: Setup Node uses: ./.github/actions/composite/setupNode + with: + IS_DESKTOP_BUILD: true - name: Decrypt Developer ID Certificate run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg From 6f6d3a947f241393d37538832fd481c2a6402142 Mon Sep 17 00:00:00 2001 From: Joseph Kaufman <54866469+joekaufmanexpensify@users.noreply.github.com> Date: Fri, 31 Jan 2025 12:28:43 -0500 Subject: [PATCH 084/145] [No Q/A] Adding updated commercial feed HelpDot images (take 2) --- .../images/commfeed/commfeed-01-updated.png | Bin 0 -> 40256 bytes .../images/commfeed/commfeed-02-updated.png | Bin 0 -> 27007 bytes .../images/commfeed/commfeed-03-updated.png | Bin 0 -> 52367 bytes .../images/commfeed/commfeed-04-updated.png | Bin 0 -> 44452 bytes .../images/commfeed/commfeed-05-updated.png | Bin 0 -> 43711 bytes .../images/commfeed/commfeed-06-updated.png | Bin 0 -> 46169 bytes .../images/commfeed/commfeed-07-updated.png | Bin 0 -> 37736 bytes 7 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/assets/images/commfeed/commfeed-01-updated.png create mode 100644 docs/assets/images/commfeed/commfeed-02-updated.png create mode 100644 docs/assets/images/commfeed/commfeed-03-updated.png create mode 100644 docs/assets/images/commfeed/commfeed-04-updated.png create mode 100644 docs/assets/images/commfeed/commfeed-05-updated.png create mode 100644 docs/assets/images/commfeed/commfeed-06-updated.png create mode 100644 docs/assets/images/commfeed/commfeed-07-updated.png diff --git a/docs/assets/images/commfeed/commfeed-01-updated.png b/docs/assets/images/commfeed/commfeed-01-updated.png new file mode 100644 index 0000000000000000000000000000000000000000..347847089abea0134960c9a88c08c17bc2e0d5f1 GIT binary patch literal 40256 zcmd>mS6EX)w`f8Oy$I5zBfTRvw1ldHbU{FhA|gsBf`k@2NE4AR5Q-ojk=_+)q7XnS z(v-T94xxpT8~o3`-*+GG)A`QR&3<5J&FX9InKgUQCf3+ckA{++5(EO#+`6f43Iahe zAP~tYIWYk;(wj630uh6Z4a{}OO_fDm%p8+LbE-<)x;uuZC+Ald&rVOa)|b~;7s5(1 zpG5}Pyzn_aJ*jAIxNdcGeQ!PZP2#}t;KtU*>A}hA@6%Jv`N@~l!-R|D{PWXI-08{D z?4S9>{A`HiZ4fU^TF)fJjk~9mF(--3{GQm*&(Y8TzNL1E`YmaVdmgn-9}7wgo7$VO zyIW_tV>%JllIqe~XQs%dR%}nqpD7D?cGW3z-tTYcFFOM1h zjt^nqY#_bJx0#q6^^FMb``UASh|RA4e2P0hJ3T%*!eaNfb~e|x{x0utuOA)k0gE#$ ze}0`D9qexX-CSE*Tb|$90RFE0KHT5g-`Vg^ZoE9HCw`(HU+H?@5On_c*ZT5rRV$OA>)XD4N);v4+&YomDF&hg>?-u61MID2|>c+n7aiaRS+Db>WYBfOogDJ8Nn6k` zJ5$i8G4j12CG~<(C{}ggH{Vo-0$b)xr32^!I5}WwDb1w*x8YARHVHU zveCK*50Ck3oVS|LkKf(?TiH;PT~@dTEGA~A{M=h;-57fdd-oJ#OE?3;u zy)8nIKy@dpzpM?g_@TXB#Qjhb070@3YNy`c8e%}8!pA+F^01+Asi2@zS7zmB)QkKi zoT%Dl{5}U9;vgq1`}1WrJpyD36{-D^kjDdn5OLJc7lg>6Ad%;7!$)**$d0Z?>^xzh z=*QQM4z&v+IK&uR;XZrq{FpI}7WP;1W#F4#O9Fm3-?i^B47fiCm^+>z=7d9d4gxyp zqtV1m+t=}NN(8AU!(;^Tj&^ldtV$Sd*Zcl1t1~(R59mI_(HR94B*A#JLHJYp?RT%* zP$-%hBV4v6_phP)3SXQkG&a>-oYHia9EuV^o(g)aXWM^!$bP~=(7yG3i{brgM6g$! z(<40$7~tqiK6MQiN$F2O6B{$%ZJandah?Oha=B5zAVJL;_M?2Fx)?A$cIxbgmLGhW z6@{*5-ftFaK|9mJFhcucta_0Ii^4jxhE^YMh2^S%q_CN2Dx?IM9CuP|@*u23eL`52 z28JQE#VAhCo@`YVwtz@%F{D8?tpVHjkGNgAb?<`7ThLTEE zW5OaK=Ra2t7O9#$uaUwMNzQ@q`>VtD_f-Lp`SyTQ3(O5FWT3;g0b-^%S(+LJdTInF zIhwnIl-dkx_KDHR)W0uS*}R>36p|u;_l=X7Z!|eH&MtD^fUo?UD0}jsVS$l`i;29r z7oI&>L4%U4#4VZPk*ql((bU^lTaHytM6v79NmuBq{W>SlxhdLx0bJReK8-6(Ine=e zYU9sy*X%|e>CE1l`d7c#999o86~wMD?UfkislP3<6GwDMZ83G9MS<@EViubCiI179 z=}pK_h$!Zo@TayQdcdv#cVqyh-hM}E8<=}+6pKk2=H5!vJ#V=Q=<0#SWo*$D570NB ztjD;EOpXXB!Nw$h(4H@sEBfqjMCMOmx}+nmh<6YgY~>iS)UV8ucE9)O-fII>tDolp zBmIaWFxwrm$pAq<(ziGKATSe?Px9Yw4ia;Y9-nd|lZPX-%cI7H9hAHS^p6-VBkfAf z6-2Q_c`W4Te{Pv|GzNrXgwCVX9~$YGD8X!*TNs?|MW1-cwD_0Psu2ZubK;^qRXh(y z%K36#tQqO4ziR`c*Z|l&(^5_QK)dVM^|9PDmt;&pm*5?5Iuuo;2V9Raz^ z=JiB^hu&C-JPHKWZEIiYxNGob&QE(^Awtfe-p%nz6!?8OD(9`R;?u8tqDgW-nW{hU zN2O1jDG8Lc3zBAo>eOj=UpLg#o=Zl|5z}x=H}DrocDX=cgK+o~N?VIIRydaYN;+2_ z{|1^^oKa+BZ6>fp_cOx*a}5oJ#0G{8#wh?X+*MYl7m0iSoV<|Sw&a2PqS(@9YT*|4 zW%IWW2Y>tag_e-UEs*iN#waP$duj7Y`x_{k!Z<$Ym1M{lL`KP3^R>6!pCjU@$ngoM zu8$cIi%#Yeao97b$$29a9+479gx>Eq*^gOf=+NE7=yi>WG52Yr$DED1G% z3@4DbebHjg2m~Sdn6Cvm@GOiINxm@jd&-x_Q9za`dpoh8d~o%(8(sVtGq-{bPtu*< z@tYQ+siIi#ymTttCR@NOXjv7c0E!XP96k>GHGQ-3=gn;eU2*m`aC`2jV(<9)+{i7^ zHNCDqQ{6icNW``|h-AGOkOnSb@|E*-jK^b13eIXOwMT16(8`lFEs$IqW=~4h;fXk9K(mzH7+algCJ1SUk(;f2sra{KgJ)&QF2~xxebX z{w?KCf@%$jWd7at?%+3OTLs8W=WI?~pB0dIT|G!35L;dDm^*tt9}mQ1u0cO>GjaDU z^_xEb*tO3LQ{)~?>t}^tZ7I}7<-EMDMD|tKAy+5omb-}2)g+CE70`DxU#-;JPUYtL z_{CWA^N)UF*qXhECFY9N8rknkQ6^AjJ+c~YHOxo^F~^|%?x1Fp{xc!YwqqX9uYTu6 z4$P{oZfPK`9XD4lX-3*d;wK{S+&*Zr2PzDA_G+$sv@S4bA+!!ifxuDbX;*>k})sM;Ct zoLB)rj9iF4{89s$al^rf`{yB60Jm-B3ttO>mb}hENvEZaKbUlZG++O1mI%iB`nX{t zfeFUtx>WSk=x3;Ml9}1K$xSWR_XI`KMX^1AfTocu^yseaa@>MBc@QOedPEf}Kl#NF z)V5$emnnXo&*hp95Ir+TFTZ&IadIem&V;5AXDx}sT>iFO@Q-}^tIvV&YsyE!QedO8 zFoKTy2N5x&e}Em(`JwcZ^W$D>NQI~o!rPOefg8}kw(LJrjZ@2?Z$wSbE$lwX ziIl|&#Y)kM&1gUcb%rQr9c7>pVOh{wAj`ZcB17xwn?Dorp9&0(wCK)mTbk2oKP)e$ zk1_vRwugL%d%V>)vBPC>GA@JDV}Fi(gk7(#4z36Fsu$nU|3MtI8jy@44zm!;57;o# z!;N!3bGRRL@XUkvF>}8DhlT{0G#|n-G|eA$pIr`7fr3d$EuoQ;e#qF1_9bx=hXY219OVLBURETPZ%nE2wZqxLm zRSjLz_`s)gm{&cL2!?=%T6*tG9o~dVk~F8--sEq6!=%|6ug{Ps`!owu(?XFi@RC>h z&NZnUg#xAzZck4t_GGez93LA-7;Xq=+HBhR)OmWDM<1-hO@#1=Q8P?WVo>itzj5As z5XDIiiIAjAbB1W@$HvrT9qt%No7EP-`DP}o+euw3v9?Cxtodef>vCeqz6?1&I0X|q z+pR}sL}k%T6_rUVJw|lR`UVjvXSb^)OIuJ;yv@AJB*uTTVzhxD%k3wnex|ju8JWA* zwz_|s4(^q!DU9FE$=7EsV0PzxW94q7AWK}GBAWWOflvm-+wmc+eo4x9&e^gAx^7B1 z@E9)}MoR8Xgbci)%GsjjG3Mp?JrH{9Jz?o4tTu(GeEC$Zv+ln-sPgHCh+c|r!vKv7 z8K*x?}n%_!FR|re!JIeMS!2H?yM~}fs$u|Cc$Pcn}TZ$mw=YSMr zKyv5XCS@sX^;#9HW=FAI*avb2X$gXq+gFhyN7{f)#3$qBS;}*%Vf$21pPv}p5t<-G z)jQdy?Rr8yy^~5ct2H|z=J}`P>7?p-V9K6wiy&fhytssx{vZ;-+f9u=%?c~-9 zVKYKdVCb&*-bZzED9l!I<0t$oiiZhQbAzqlftM6q7eZzhIIrOe^Ut~y8mL2J&T~d`Mb?pO6HeZ8K#gIfr8@hfB5<#G7$qN`srMy(WXT}h$-3kbfU^h%2Dw* z%)Yg#6ilDuY*7}G$e<_5T($6MUC1k0cX`3qOULSQ9^!Cva;oh~uR|waK-j!1ey5!I z`_qjKSMgx=Xp3mFS=0H~zkYHcdRb4|F9OU0_3_YQxbwgM=+OL?`N{9k2pza{{0FJ^ zUv9hKZQHvE!`KwoDjT%`JVsYvDf0GCzHRk2Gx+?R7OFx4;UtGL=84vhH6!%bkaJSX`oHdN23|fx|$u952kXye`%QV>kL{ znB7oNL0D3RJr5t*>kk*`a>48HiO9U5ZYjQL;*b z8e~4brPh;D+&p_VXGU(mZpMxZRPeQn2C0*Gl6#}~f=6)kS0)9{X`;3wQs*HDwm~du zt9m&t&HLKwkeeMFCsy^<5$Gc$wjxQ};R;&|U4Mt)SC5p3NCk&-^m@BI1aSRqS=+6X zcR*lP6`R~)BkUyD8K>?Hw->iD^-pJosMXh*w03^{_@M#uuAxo=3XstslfP#~$ksct zdEc15M7l0cQRvaz56ZJ}`&anZo<^^Oc#`wh*YC{N9A_F9 zC!Wt1Rip)v6=i;br#CeGeAFuFTsWL|>q!op1<{TPcJncZnY2)L5zG=RR6fIE^Q#4RxkqoX^cOJt(9kjv~U5`)N=$fp$QpDQPtvAQ( zC_%EL*Otasj!K7+!)%UtMy-py4;7~OQsKjaLaPA}ji<_05^i$vBiJ9h;-SO@7g?Ww zjVqyo60v^*84t9g6c*vDLfC`K&FE>miN=P8hNJ*ZMOPlQA?8-}b?0DY{U2i+E|Hvg z#5)b3NGs?^Yv?WFvKgP=l`Z4UTKOmMXP@6Zgf$cjZ^U!)^!oZ-gm>69kfmxuDI8j> z%1>ZviKZb__dR37)o0=J8cN4bc!yI5{ABl^Edj3UuLot|LbG1A*Oxh^*y_;>kQVqO z=+*ffYI^)S!8IGNt!0*m4U}(M|AtIeEcUO}wtTw(Abyso(;0}Z$zRJ5(I%11 zrY#E7vXE_6%7EakA($~16&~zaqhUi3wVG;X z9d!D#n8b0TjkVTK#OyUEzG{E3JtI~-&kvM!5OpR_w7~4i3K!LS$sQ94v-yRy0KMST zl#TrpGg39|hw-Z%DHGbtf0k7|4YMGd9d2PNYWIC@a<`2ILA*mF>q_#ac!weg?+}s} zcJXi-YhiD2Yn07#y9d2}TU1`ty2dnJo10EVuRZ97#0?^cw;!7}CXtlF`oq zX_uFtm=J#)cJ0&0l2apR5rD;_w+{ERn2x}=-SFus%|5Bv-Hmep!t;jq*n1#f@zf+b zBx|8n;~$FMVhSGjw@`osF@4v$s}y~$c4MMlUI$ zCp9#$H^&o6W?lR2#tw#Qpo{40z_9c$tl3Il=7JK)`Ye&s^zCzNlv(g1`1v0m$tW)R zSk?n?;f+TE9Mnior(|dZXT9R6UQ2*NSZ;p9Um;Rk$XT0vw_4 z9rOeSE1p;RzP~R+i<=ApQDSsnyscr!+E_7f)-_Ykhd&}C+ zNZsv+nn~TVf(qt!oY(Kjhjm9BSl+}%ljs#C-GD2{SB(^=JvlO;%ab8xzsk>|hx?R( z8z=FE2~mE_YRl0jKO>%YL1x%{6@7yi*_Sh;IdBfk+cXSCsxP(peM|Mu<3y~H%Jd38 z^;u#@uDnhawse~5WXzM)-MJRv_nYn3l^!n+!Sg{rV^VTdRPHZf&n@g{XcU>%o@dv) z9JPdY<;mywz^^!j($ibYhQ6PFRJ=Pj(Ca$9c4_kpGA@>t0 z0iYm{WY)im9Z*hXy_pVBA$PiV(1Z9wDj9W-@%ERK*~prY8upzX(g%!*|B%&QZM950 zG?R4v&lLIsbseWC9{rIJYs!V`iDUz zHb}~WV< zGvg<{^!xo?0iS&lU_79_Yff1IDh7Fj2k@wp;p6=>hms)ZZOkTMP&+}9NGN(|TJ zE&x1y2@k<XPF5CjKuX>j?gxMKC zhwN-r4dGK7aMVMwcws?2ey}P^-&F!ROrASI7iO zfQgUljsVDmo3x#R$qh1VdULeql~@bCeRbYCLwkG9Lrf$Ir_j`mZD5|`x6j5k?KFg; zfBJDdJTKsS5-rFojjPsN-dIf$OLprP?WyE%zx0r5i-~kg58qtWWnOr!@5*I7`o~0r z<6H2_HZ4Y|WE`Af68erVyLOwd<>hHr{VZ7P>MN6EE=uaIU}kJtfO1~cta9glwWg_5 ztUft!FCxG$X<5iX4$usv<>2I0E5rj%F`7F}pf_I>UVXYi^J8X^)vd((iTdMfyjnht zs&_G8PD;?-#xb@O_hpQotYNYw2q&_P98Do2{Es*4lhtGA~ zlUu>R=>wkq@dn!%m2u=zLFMVOa?5yw0XHw>ca|)6*i)%6^cV>|&*zf^1E=T#5GEB9 zaFrxSkR16{=N&JS8}*lL?JD&!X%0I%E+bC;g*Q#v^V7ei%V#5%JV2c$Ci=dddE0l+ z$(FsPnPd#mHp8GsK8OokGqLgRBX7K|4U^s+?6V8G_ z2Zb5xlbkzBW|45xV{h{U1I0=(3U&O^ibG1N9lN!O%-ar;o*eOMpH)PUscUWlP>lUvLHt*TZBts$_d!n&&b9l{Lvl z3PcVKv&&KE(&q+UM#SCmKFdGZrXGQ`grN?S!x?%Ap)uESt~@n=37Tt5EqtrDJ^qjH z^X!yA%Kt=;_Y%L(JuLpGCe`@L_0@k0H~{!(NcLy~R059ih^kSjIGk;)D5>IJt^bD^ zerS0Za?TDSX{RB)og*kp(B}X`*xcj4RdMbsxQ9RB;5Pp~a2-~%3z+C5MB5_iN_8rt z9_`VaOYiBN!E0HbR1YoxlCpZ48isu;OiFn*;ruMhl3K?obSIi-wz1NKXImNI<10XK z|9#Jit-g!VSyH<~8uUWdp{`Qz=-yI$-P{oG6_q1za z4~a4##2PtI2vN4g#DuBWJ^#@WsHNyZHp?Cly|53H{0DabyX0lGSbzpkCFuRpzBTGl)$IW1aED>P07;cLDmL3x?VsaN$ zCVq76Aq;SbsJ)E2Ule^FNfI4>| zT4@X-N2mpuhnl>vQ6~+;BbXSP@7YT(ziU!BJfl=PZkh~k@u-Q|P+Usj)k+K7>YvW7 zBg;)*R@E;`fYlPqjvn$pSI4|Qf8nK=)(}y_i9O#p(bm?{SVs#*cY~-8z9|mE_3~c2 znPSw|q=(!&KB2Rn72GmxYPDI@mn9_VZ>C#1;T)JXNv%`>Iz0x4tLGcxb7!7@eos)g zHb%c5D+9f+#b1Tly|PVnN&^Sa1ge>>BoEzv#GQ2TC=wReKiV<_8>hpqE;=zXoU~}1 zqUn5ktG6-tJDGx%fyr0kn{dCkIn+q9u2+$(-}r0v-jM3HhZ59WU5q3UY|exS{~VY4 zMV7a6aPEy688d)yFYF8qpgmBC(wnO(-n38yU&foyc-G0zOBUY|C~)Wgkn^WDowaUJ zC)?X|uw!{;3b2UvPESoogb((!VM2o*ZMiuO)()8;o^LI%N60lmLZFn zx5rT(90>EYLI*>#S?@Q*PQUvtd@v(26!$X2E-D~a+DmH{;TQQAz3!782U)E?q=IwG zbX`r;7?$NG&y#g!T5gY)zrzT00%8N)l$Xpe+s|U>e>r`(x_nGDaV4of z*9>%tZ@cJGxpMq(JTBybgf$1lfRo8>uB4EbBk<%JmM_f?o^4al^Sev@!D+ZP)X7Yx^lLey|=E z8czJ37Axg_*XBB*-sjQW2Ue7dt_W{(9e5NM?d}OnhSY(d3UE) z-|rd;i@#3?dDnT|r@y^nG!^rj&b%ND(Pd}^D_=R&0bjp15368=pmA*SqsSFPn zy+uGaFs&o|w36~4Wa_$?T8$GNU$7eIMtBH4_7gc&vC+3`P^8NAWifHY)kd8YF}VFt zT1U4+vU;#wSVbmlxf8l6(m}4xymMTGgNYMN7MDBHk#UX zOdqu!wKE#%o5?J~XT)A244*AS&h>4{UA@CDJaFesA#x|<9b)I|x7Rzj)sYl<1TTYq zGFIT0MZw&}^$va9%7oJ_?l8Q~XEAfG0__74ojrUr*u3A-BxYGr6r+o1!2>$Xe`*nl_-|Uq30#(83*hzQzR@zWO2O3hZ zQF$W-Sspw6?%-t8d8AyaEN0?HX5L9A-fN%bDz>LgSElT3CuTV{IN8vCqIc=EZ~d0K zYSyXs%NUlRnEA=A0=rlbH4^m>6UC@h#Knu%JzUh75z=GiZtPL?{)xaG)8eNV5j!&P zL!0O%a=rDbe+D$2=u_g%62bxpXuZcI8}V}K`hqW5yR+poXw!RlANUJRO< zvwZ{315$K?3>y8<=TBx^w)g)rs5u#=MBXjr$$l1I)gYLj;&ViYHhWl#Eo0^jTy&G+$oEvPper-2$jucGJd0JOy%0@NPA6l*=^yNHICLr2DVqX zFA;;nXu~A&G@H$-ABZm7a}~4n<(csegc_0_Apup(Q~k$1%NeQu;2$yW?_6VptS{)& zj^+JN?T*v^$6yc~0#ARuF6lot*#EC%>1#M7M12XiJjAn1)c<Jb(FT z;H~dq0|vslhD*g&P(~W}#%yR5#}s#u7yUWCe5m-Xmth@wgbJ&p^3!Kxd&@>^ z3(Qq)yS6qHR&m&GH<#pxqr*CE*s}ha6`{V;A%M^4+7~%#S*1REQ`~*3e1oc@_+&24 z=OA)w`9oQov#!1y&v#e$O%4&$MTu^G(&6j`#Ty$U>Wk|1ug`;!t@ zg^r8a*gw?Z#7b#gu;IunZe)R!P|4r!t>1(gPrx{z$p2L*R%7#8Ht4#?TvoYE#)@Mn zuXDmw`+zb-XHMJN!ox1IFL>i(I1cg_IwrsponB2Nx2_+AzV%7obZ>KRSUP7eji-gMKQ~O1ccBUwhU>8@^MLgVZ_12UoD}BFgDUVtqR9&2eGD zFQ5e3b$D)fVF2vOtcDTfnWcl0*-^7*prD7rjEM1mw)3aTo-V%n5L|{OJgIAM;x)C4 zoz|c&6ZTSevKl87mPMA{e*|nuykPpf65V~182R|e_0wsz-}_J(pJr0sT5HmFv1$^? z6I;vFgs4_wAbN3(K>8pjkrJoBf;y5Ru6Y(=My#Y5zUAo6^FdF|Dta1i2?~7H{E$@c z?s_@8(Mgin0Td-_0P7=2o7zP%Gx!@AanoI6LSz-bFYyTRoTf#NyyAZxd=2rK$6Z`V z_RT32*yu<=2+uS{huLw=6Hc$`XmbsYMUR~>!r`6#PDeAOLltZ@gF*4%k4n1D4psLG z?iaHT$Z@+mR#YWaZHe1(w0&R|iaNQMBu+=0;6+Djdzr_1zjr!=AEAW_TM-RCIDW5@ zOwFACl`x@n>mN43;ExWD-0bJi!q7qv2NA-+X>5!z&}n@5O=HY9#A}d_;gSg)UqaEV zF0>;3yJRP0%9{kfEHA>w(fzzHCySM^lFt1-PiTLmuU8!plJVlFeE^2*&_)u! z+I_L69TyQ=%Wnz7`^w%++rf*-Y>v`N1J&RzxRdJd@HBH0B zwt-B7OW9+qi^2Il`_PW2Xjr=TyP`MhxrzJMF{a5-TMfFO0(k7TTQ}voGlFbF##r8e zefzfWsU)e()?-@bYav*OTQvW>`0>XPgM0QDgCV521xX^#(wO&O+h7`>OHlNPYoUfL zXLnInSKk$hysHek@}AOX3K3ppyv?m4e!)1WrtTITBKV34{#F2E9wj)hG=CRHy7C8k zfw%nKNfhLD8hY`QjdbrE%baT;!|KkDvE5-|Qr4|I`iW`S!-lu3JaAk5wx~o){$iYi zhsGEf%;9=3@R8H`H@0 zP`Fkox4-`9WRRi?eq`TL&6F=-;e+gr^B~To6<-qMvGi;4`L?43&cg9LRW&W#nDe1c zR7Z<58J9$8b7+*?)Om>GItr3AglqU&F!_=B8s__{?*dF{c=d2bpycF>?&{w%K`CJp3eL$7_Pytsf=^+3lNu_1@my5t`3hRVC8)fAa#Ig>u= zF9shvH8~50epj1LhdTqar0Q=PHQsyx63?Af;qx3@C*N`xH@1ke`X;v`@@Hu_j}=!0cjA`k|3oJqxwj?hXC8wyx#)<)w58%M4yd6vD-@sFhBpH zv?%mqP}^r%Ert>iWA_RmA_BIHE^;tc?wY1`rNvuaqVffYc<7T8I$@7D(HI zomnFuzy^09&}V|S4{32Pys6zOWb)(Elj-1=FQhH!uKRZ;R!=3vFYFN9%Zd6}r&vjG z)WyKWEJ@2N5VR#r71M|nG1?d4#?C6AM=$&p>7Gamp1_eQr$>64Ram>K}wV}}I zLwM(s{~Qrul9mb-5CcD=%Z{Q)b8nbi7-LVKiZB6eyOP9!Sp7~AuL)4qdkxWkb>pYy zN6tPMKe$HAia#x8mk&8g1cY`ldx$%-F27Avl<0m>qF_$5e&B0qr{||2W~{1?lh!Qdt5G9b;mjs6~jMvcWTqY01M z(pEe~G6dZKcd?A9j_cAN0N9e-W63nhICgjj90bzNmY(_53^Hriw}!eUip8$E7QQT(fA{3> z(c1?v%D-=!ZJk`Hlw?@Dir;z5Y6Utskw830<+@}16sm2#-=5oYV+*HNmy}^i4dt?u zU<69toz?2oNv;(AxIdZgzMTDz0r$~nRhoF=Tb8WGFwg4I+gWC4DMV7k9^%*2o;`&a z6H<&N-wO&(bO}M%zz>VEfa2pu7fu)i0IAiZSHl2y-CONWwrb0E2YJcWR~9WiY$63u zC^>JVld`kukWsk_C2LBYJQ0ss_aB;rvGpHz=7_LN;fA>^oPMQLxHpEmgj}V?jL{%1 zup_j*i5~r3~A8pb3V4+67YC9+JlVkuYM#i^BL6AW=my7 zP>UIHIJ1U;d;GPLAn*Yr%ixxlKi{McFlnnhvqklwQ#S8MRkhrhx?-D_vShc^((+lc zRpq^SOUIFQf6NFeX8q6nB0)YS)L(sd>jp4JaMq=5v*^`WdyTxOadxLwWU%VTXL2Ue zXWn6OMI5X|4AEJE=Teakx*so&;yQHmBDh=~Gr@*U&-U3AunhfYQ*JE?=WzF(PvRrR zScfcGpkTwO{=VV9MSS}0{bFOO(lPgaFy?2VCRYln&=Y-6d6D9T&!%9V zgO{_P$X?&ThP}`*7)sE|1(NeazG75W&DM_IY8VJ}IYCRkXga-V`K_W+_eO#@eE)BU zd@%0~Los6XGc1+Il>fHU1Ai5&l-v| znpsZAscS3)=O>bYau(wWX}8CHChDJUI0>J*IQUV133Eh0`)uvQ>tO6oh*mwpTLF>5 zN+Sr`nQ%!U<)6busL^z3!{!ptc+-!bSKqVHDK{ryI3X=xn8l307P;s3`TWaQ*LS8n zN#z*R{6|(l-x~WcS6*`hwO>eoVcc?|u>_)#A2*dKu}6aaK7=@$vA{I+NDJQ{dBner zaRJ$0C^0W^Zmm%h@Nfv{Ja}H_izy;Pm_or=jdn0L>C4F=o6`aAq$$P`r1oXCNA2|` zPZb+n-Nz+gxYSVcDu@#;B#n~1*s~tiXcHgGyH#Ck!S0*9z~bG=gG} zhAFUx&EIpF2_7GyMu+SrD^K?2#Rz}3ppgr4{~!~B-?910OXhxCNA!u`4rS> zAIq?LhGG|t>LGd%=Uu5_K_$0RZJcs9^A`vgrM}!0V2hFZj}~%-xfxo&cOz~nF=-@S z!{0o;lm=bBK9=}#p7b1GCrnXbWEe4FiXX&n{$gF$GGvs>J6z6t^!X`?GYE&lX3~b+ zwfP$43SC2>xDFOINEbB{2&>qfvErGOanQ2!o+_q$c{qxeEq9q+t8eUgRIgPdCad@N z!=|ViJbh#*ezyZA)dC^tBf-`A6pUp+A#UUWl|w zygc>jYf4bo3k{Y#eX9xqSm=HH_algli8 z)qjj=c8_P-XDt2Y#KGg^m^m)hC&V8^E89!Wg#t^C<`cb!nE*2=Ar5+^I2GiX<5|h& zf*CEe+?DmIXDt1*kdey$+zWe}DPTK#t=W_qFcilP zGYSNqw~`S7)gHDX%3t<3rv?%sxL{F2Dg(nHwOugQBVvg366yyyZkNjuH51mUx|J4M8obm2A$ zH=;mL|KPeowBO^rNeVi2_r4gP1R-FORe_MvVOChl`abay;XAVgtH9SPFeB(HUBpeA zOK0Q-pMnzFmIyO!BWIuO9z5@D&Wi_nEU*xi?rdWoB^Z-JaI=iC3qfO_i__`!A9)X_ z6R#jplExgjH3$s&Q5W71i`!R4F(uf_%Uj%wmH;E%jymWg|2f3l210q^fxKLja-06+ zf4r>B*ji|C4k3#{YA*jm1tU~*(_6VjL0SJdu!Pe&q><`>&#V7M@3A)F7l0l%c(n>r z3%eMe%Rx8LkT@`@hZ@miUKMGRnVFLZe5wG8Qh(amWTPy0ZBGy}SHg%f>11zu^;s}a zKR0+?Y+1@C;+ne1T>E`VZCv?NpTJp1M!aggA1jh?Ek zl@fMr^V}){wfH}%K^~`v2hCZE1fH}&)PKRMmCertK;Sek;7i~v_8DPgeYe$t4VZ{$@e-2S${QjwMcyJXXL4kSpZC@VT@?JS91v4I_JLyz|PV@FsazTbE^M2VrRyR>rLmjqaW0QO;J3M1B^;2IM1d$y; z2lPS!J@{P`;6urt3mDgsaf<+NohCvo5@S6s;kVA2u`M@J2wc*RI57!yn3ISOUKk-v zR|uXyOe8pEc9c-5!|)~(xz~u0LK`5QJp-B%%}CXLe13w%nOwO@Z}NJr%G&bh1N}n| zA{-|JIz&A%`X$Ox$|Jw^)Gr`-;m0|^9Z0))UA*Ux8G{Q#Wk3na)yKVEdA+5rrQ?bT z1T&$F3Z8r-H}{zjJr-vTT36X^p#F`i*sg2nEf>{F1xpEqKO>9hQaVm?Z4dJv8s7xpLPX@a$M^ks$L!Ru}Y(m z5vMJ@Xutp3?~0dyP+{v88G;%32iw%hUpH%&f_HA#3+WeEZ@S2Qab*gJv5Pd#j*D(# zZ?fvVq_>tVe;cN|^!h&Evi^pI`cP@=42KS-vm>Y<#Ym=!LQgrR%%1a^Reuc`=~auQ zc`TSP@8Q~7hWiyf=q-Ma*wt(SL!hC7gmPXzid|v6`8g*{&UV{A9Q1ATpBH zf_>=2$m;sgdz{$u@;)NCXfx#a#w0W`kZ~)#V&c>^H#{Zrjvq6$TQg5;dH)<30uorTRqJ)sQB8wK5@jN(^G}Rq))5)nKo}XtlZ6_1<0m{prOt$&ClBt;f=8 z?n=d+e$s6_S#%?i-mzG=jxQ%dHo!H;dHHO}_ANj$ZL#swuCvGQr!TRoPyBPScaEk9 z@Ml84=X70__G12^Evk--lEO`3FTu@*O%o0FsUE9Dv3(`{o? z!zby#_Y)g!i)(tOh+c1Fd=#GZ8l*Q9eoW=3i@Ct3-DEdjkg#=HhzY-Z|Y-0CxX z`k+W1WDwi3zlzYn45BlqZ8+rf1Rh3VJM&dcu|q$HyL$}*KY7Kr+BUh2z5XZNuA zugK>5%iI_5ieA)7HSNd|<4UM(k6syn*I|@l?YsZQajmTCFHe4855wX~-bDlOy~F!_PZA8ro=6&NH3+w8>-N)%o^;VXddfSh zzI@X}?z>N1gM*|vR-uLiL8q?Be(uEz{+JA=>Xc9+;zK7b$7yGTXO>0&1xL#iz^S zUY-Sazf_K`*;Tzt<=yD3TL3)m>))`t$}7mJyG-7Q_;33Bk)ZTnyoh*QT1}9MZ(i@i zNW+-A_~MMZ8p79q)WTtF=@@X}T=+=xr>?jL7Kg;7Z65tIICUoEn;EiXb>Aoh^JhQv zfARI-@o;_5A2_b}6442QAP5p7qPHMQh~9!EqD7Ffy0yAQCqeWjM0C-umW1f+iWWVh zt=?8!cK3T%UhmKE_xt~=u#gTO23?+-v;Em-$@_a?vj1wL?n z=x*uHH-E<%Z=xOqV;Nhp>O><^+kPWY_`N|@6uRL3I-!*|>P`ajkXWyyz0e7rkch zSN2%SDcL!fxj5#=Nck?^qup|Xnh2h!kj4;iv)7q8Rh;ugJMj?W3IqrKBmde z8RuK0dzOua2i_YzOLY-y@2E!L3|&82Ngd4ILcKKTQ%Q})t{Hi>&b>W2{+;oA)@^5J z3(#r`JQd-4d{ICpC_LZ)xc3fb~#2$_Rh z2uDctnLWXSYr%9gcr+(+xEj%;d*&k;$SBu#wVgJfnx3B_Uuy}Zi=Gi2GLFynd8hUO z*^sD+cwH3#;)EA=(ZpOY&+su6kW1*Q&K_nG*sKg$)qnzJl0=Qpc(E?M(!VC>t7 z&*7+O?rHO>B0^>Qp|Yg8fcZN@N&Y+3bKhc!qj$CQWqijc$9i`FyzVtmdn!NPLN%=f z*NxjJyEmoA`Tx_I=$U>Li4=hvrnDlB<3uLWRWCG^+SnOocHUhg}v2@>{St9al2sVK53l?R_^%xHZ0vl=3wAfpe3$tw_t4VOQ(*74Q1r`pZ>oV%yT$Bue$VN4@4KR(+1XZX^Jm?kT&hj1p@!w5dY zFfy`iY4OKG#toBP^h9Vmc0wx2epbg)elKk_!YG zAbDAvSD&XQ*j*&{d0jw@UxtacExWIeJe@~)JE5;vl^Mym>94KqTnFkpYi}aog+dpy z`4eQ6fNuhjfx!^yUtoq@5(>*Df9^aAub5$KypuL3>X+b7HiNmnt zxQoejJAp3{3Us_|Sgh9=o9$mTk#?yPgR~I$tjPr+eaivX(C@o z%eo2z`NDyM(d$Uag_+!cSN zaDae6SyXXva-ntu-%3ex9N-mF2FRn3v559hUL8eID4$s002`IO8cI5VExufo0)3CR zFw7PbK00yB^G9%=a-qDOcOuJb34Y#c4xq{wtW?U&jB@V7ToljJI=t+dsd(4h=<6q% zL3(sE5m+6aSLZI`87WYtNEJ|ewW<0HH49}OWRKlStK{jPkxzi#y(65>yzZj7DuDPBt>8j{RwMV6{M%|rNevN0aop6=j;~E&^379;j%yS zs;Y+})( zTeQ91`3}z)g_I|_(ebQ#?-i0dm{!w*Sv8^l)(xRQ8#7KVE)1S@VJW^3{ZWpCc2N5liAm*0ujMPR zK;cg_-%FkQ{T=%8Y)ve2t(Evz7+~xhw~@+kzj>Ci+mT`%MlhJc!hH`cN6XV7u45(a zv(v~uP73rnY;`BehH;tB=}pr@vVIwx@ngT@$;LtBOOZlVJNh(M`YeCM4;J>l)*5b+ zVh<+4U~IhdmJ~p>#@Eb$voGj;hSbr_UAvs{7P55}8|AJ~kDk4P@|7?cK2xn~B5dxo zyj5g+&HkgkeA{tCT0tTuP;ve61%jt&VKA6X)vf|Mkcd2puB|3xUs9dy9N^mE7Sl@X z$D<~x99oNs^vX&D_V)SqcIR;$dr}7C?E7(TxDs@}CSYf!=8~!xs+<|9qJ>zQvyEh! z%Qq7ErkXB+eu1!_Ur+UfTPqy&fQ|OK@%&n$@O^8PD+RirOfgI9Mf&WSurDR?&$?lX zfba6s7Ne=&9{c7Ko*WIqq$FHdIZ=6)pNr=D%bx=I_%w4mIAwr zRT<}l8;9{u@LWS>h9%js_S^X4^hnmZ6F3$-<4vmyF*ljZe{No3w={V*aSbnD2-FGtBS1cESi?h)KH$;D>ono%Vd(d3JvGx#IQd9f zpNQ?)ePA~#yaMvA*QbJ?9Od#O_4r3KSwJ(id#f z_tPrFCeiYX#aq-;&LjUR2I`1p3o-Tu^C3N!`YF2rfGvdAYOhjoMI=fww_ExRqBznK4iyZoKC{?Q845 z7Y41pWx5&Kcr=~qu!VdOGnIMKLNz~fSCJd;)M8+Xw=8oc)DGTDbgOxi1k#6Dw?Mcq zrksy@2!!o`=we6fGXZzK+)dG$K7et&Yp;+`D6 z&1$`Lu?m;u=Xq&u7g-Iv@Bl5FL_MwNQYX1uEfPgZQP4a}B~^s79|c;7-GV7?w#YZe zi{|~H>d5uBHxqMdYM+}KxB2*uY+j#M>bJQpogMe-3U^#b-u~%8f+3EA*2dlV7LiNU zWX-7RyB%^r+hN)hChfd^^V9x}q_zaj-)#?iE4g*emfwVU!$Y6x%PZAy zmb6F+Z1W_!KHK6NL)p{~dj``$EaFz8kL>M%y zmyA6?Op2!J^jGwe<&@+YHi#8T2NT79whk&*A5#t#Wp^;9nxiEL!A^jB&40f0Dd(>e z?nC%+zQ?^X^+)RhzDnBQ>rs1ADaH?GET5gerQ2AZKEyBc)|bOH5qEr~=Z&?}|zXBKbNR1VytZ`>n7EICIfxjfoxS zPfoudo@%Ze#I5zm3Z-zr90@yz2LoFN;=jX@lPDUDf3~o!I=BAOR`6RVsKm0O_yTyn z`gK{ns{F2|R#^f|pRe}spaZ^kWboip^9NQY@g|RSp3CThM;Y?yZlhyM(M#{}n|WOQ zWuW}&a$054@%B|HT$S-0#wxeiy(Pr7w~0x5zFWHQC%$yF+J76ca0ZV&1xE9L42R^~)hL2(x< z|Ktbowr!N;)nZa%vK??(Ti;_>SnA&9P9N(&!zznB9Q{F}eEpd*>YVh*PXRZ{=zsCx z+U{A*<&y?bU#Nm2_v-9!vvuiADs0=#)u;BanN0^0yJjsrkM_2my7`@hvb7zX1Z%6* zf}hz)%Vzn0B0UNG_YfiuQz~$$;Q^PK%vni0oIk2M6RtAf3b1*AeXhS$JiBH(Q9yFY z_-2cf5B;IXOS=krqkT;jsPDT(59-y93Tjs0bo*JKEYjWkeuJ{0SjX>_K&7;&SD#Zt z#8?(q+RS{G0^9j6pGtHT0jsv@QPM*x zZyv)u5>3es^{}b6)9Vf;zUz`O)9I|^W835PZW%0kv5H(TCoJ&C39%;^M5#e6rvuVZ zWv_*>SGMFVPL*}>`7LD!Va})K1F1%|H;G;ZeTeQNn=m)4#l`1RV}h6NAuWbPP!W~M zOI&Zp*+-|8Kx9}7u=(lwTf`a-1Nv{g45u3HO^J8X1a%Y9or~L9GS)QUP0yeGwQbjxVYme0iv%Nk97w^qIzJFJCG#!qr=XQTkIx{OZ`5h6SG{V z!RABojD*^nZ`X*iLz?gGEGIZC@DV~$?jF)#5S5AUSjG)0P}p3_hy1v1h@)ROuz1T1 z9-wA}DjZwne{^H2jg-y}#&QXDTR!fpHN2lN!N)`6|6DySc$x(L#p@iyj1JXB0$Vcf zC)|I5*nEhS@2sP+R}`}L-l+7B z%l!zc?kUEP6>$=)ks2IbF4fE|Zyd`>$=VPczUuYv9S4jp*}1STH$znF&fAs%Iqe0h z{K*NN04@|znjKO?eQQ|Mz%;C^}z2x#7ADKuEv|kqRQ|7JjFHm z#Mvs&YL~W4A8mZy4)sv0s>11F?P3t5yOCHyrVVxf7?&*`m2x)i!#$RSHkxVdmAn&V znEbQ9vJ9}>@_2NLzeQ~S(uUbLoW46YL`Rn#PmGC@+tqX?t-seG0#;crC0VKr35)_+Ca@!eU&8 zt%1(4eTdXsJ=VOljix{Ip${X(FH-Od_?|9_gLje#QomQ8>iO|WsH$_*V2^kv7GWwM zI39`VvE-v z@y5b;e>uUTTzvTJ+#dm4eVc*QW&E*er-mG3;-^_eBpzx)1{*If>;3`*&xRR(yZXba5Z1movb5 z4^)uY6Nty5GC5vd{;z8i>FxM%ymn{dnT777aYeH0Jt>CMirgK*~^d1tKo)Q4}Pu=F4A4@xHl9lhAKR`A<& zj-Bd47?b7`X~sH+JubiVoeeVi#5q*viSV$GOYS*F;Qjv)L6r0%i*C{-awNWj1w{vXl%RcRi3jt6>Fh`4!Xy&3n<>jz*@92cLr*G4Q2|&7D@^UN z#WcmzG`wEIyVZ6^s5n7`9?|%aior}u?VOb_bM7UzVcpq-S@4p-M_w1Q{S4&W_P$~r z?VQdHsjG$d{{Mx*2j&2-AHV3P|f?&23A2CJ9R3 zl=?9&xcO)5wiW@n<--*<+V^k#Zv5RJcgDKK&YXAMAvD#QT;D-!Dwu3XmH*V3e})Ov<wAZ=)I= zx7`(=rX_N!$gr!K*Kr1JZ(2#XbkrRsHK+Keu|WK}L^li2$4|(S)+{XH#l^l3N42NN z>n@1D`WAgUOIvI;_e|)0oFdyx%=4^hEJx)hVAOJia)f9Xbhg>O07e<4oVC-*c<$Hv zh~uOFfX)0zljNxF{#aiFp0$sqvBj}0xodo|h5Lj@p4((LABo&Lnp!la>8ofi? z`gooYjU_|Qn8#p8M+~xh{^m=VY#MY49E`X>MpAz1ln-2t7D370T@eagffxx-CP{-I zJc+&>y<6mVKCg-(RQ$-T*h=k$Rk~Kc*UFSWq&^Yzgom8-oz<_|sOW;U{@7mG7h=gG zNXg4Wm|~YrK^tNZ+@1Me#F>7eWS^eUJPlV;QE>=2Ru>6ojj+)F9eIpm(5N%g=> zJKD?jKAMKvvI2!Kt!&pc%>Pynd6cG>rm0VS_R-x4pYHd(uo7@^)3B9`F8|55d-rml zg*1tI!;)0xqupJ&Sf!9TYAH8 z;6xjNO`gOkm43;*bFUSj1H$dnn28Hp+pa3g7oWEkBs7jR)hGV)?cV>lo^|Se7&|n& z!ad^fdEFJiS56=7?3(R-TqK^%R~Z%A_gEGGb1+|YSo@0{8btWvJ!9jc_PfGc{W@OU zu~zw^N=)^9Ye~{=_Oyr=ZUFlbp!NomgA)3C_06%fw6(0y%5*ueJ5vgA|ih&m-ZNF{@ zKPvf&J*>?{y#z4e=u-~hNN!t6iAgJ@(!j+nJHC{orwVgrWp&>*f!EcD3}AtkW0;ws zKpZ_zv#6MhTH~9#A6(%XXXiy@85@h2VBO(O^`IR)A$k=hc4XS=*7PbjK0vVl=e@?) zj}L<$FY~>*#O!7H6y{JoAy{$F4M&6uH^>{~?lUm+Ra+l*d~RxYr`B*Kr%h7qfX|24 zQg`_-0SLb^%XuuV(>ftK-zlobL`}a!M;&9Kx2@yFsU)&RnwnnVNoz?R>ji|a;`Hv* zF$tich;tyF{5y$cV;09oQU5lTcU6hRrYQepQhol^SF%fEwQWNp>5w5i9Q~3k^ww`)Osx#D+g#U;DAiN&7=*PJ#h{EmlkGX|N;I}+{t;OK4i z8x`ff581>017|oh(tWE>M|@hNv&{fz@Bz)+F22mm6KubC!EhK-rQP1gktvp`ESe2I zPmHp$b3T#?Z#|<%&?~UpwW}qA{+kdq=etnok_&eEPTM|xgb)5Lw1`P!(EiLn0EH+u zTe`G2S4}CAAVb6*@XsI;q${|6KW`jT3#I@(U)&SWzmSRta;Ely^3X9aQ0vu`Abr^e zZ_ha$P|$tj0pEcWav>N?i2jkfOES(Ku7(88t9il?R~Lp2PXeZF0sgtHmrt*dotK^p zC)S4*9STb@$bpuo8xd;%=6r+mEJ?tvgMUVtJJNS7fFB&CkIbGV2(i~wA{d1mJmeaH z*LVLk2`W1g%FT(w5I3%g5u^4^whDr1Q4jtzA!M}hWUkEDlV+`g81-=LbD13SO)~Li zl&RIjy?*YZ)c3_ZMCoda@EvI5@Be@UQ^p~W<~w$euq219WXN~FAK6ob@D7Vt{(>M8 zYM|t|#(ohkOa#A7NDBqf+|zPJk!J?l!$%IFx$zwUE}QhYhFdx>$p;>&kpk`&{F?zv zEGP6+z>|Xji0PqP0ER;iJkqXS)P5f`WlRJoR1 zc&j8vQW50%Ae8C2fk_~aBtSWp?XqFbQUUS_6JW8rSgg+;z~WFB2r2J9EqmvRhr@BT zUOo5S?a8l2>$1KxF3gcaD(ve-Qb`+q@>#5X%jQR9Au)cJuq^w=0Zs;X(o(0C(#DyAr`d@VA#2VlH5E zKsLhvUGZ%GZ!nbDMg@>o{Qs`hNS+P_B@8;Ex;6xh8W>$-g&K%JmAAapulTSaPGDN} zDtUUqfA|3MlcIcQmL34yly{DDRe|*^xB!fH0p4@+=-)sC=3Ex96b@xT5@1gvYNUt? zkC)0xSS~>3dI8)gvPc31igDZ^LQJ0;8KAYEk#KQndYd2(Ad?4@C_vVIvWLucYN0^> z0z+ZUidux=qFbU}DkdDO!&1WKuFK z6RXd;lamz$c`^;)s)9uazuND@2f92bJ9wVS5o3ZEAe#^(K=|TaNSg6Wv6hSe-`Jz5 z+0;*~&v%BM&2;B2_e|7XEYau_6b6joQ**oFCB@8t{ zp6AtmEr&o|f&aT0P8m>ZlbqfAyey$Abx`*`&B535u@xs}+mxsZ6< zTcu+XSPVJrGYLAcZdTmUw7#@-=v7-z`bdL>dI>cwL}`8(cRHzzII>l-6E7XNto>Md z=Mo$NlTi5Nh(gg6{rgHtT?*C=#YhYbX{TLl6tc0i*3OF!wgPOqRy%%F>NZlLl8!Y6 z9Rh~(wT1Vbr~C)HJadSd^lLnn0d%3Cr)mmcLd!-aagCo902m%gRpu^r1put@0NLIM zoOzO~90oK~BQxcYhcA!jDh49WI;$>8T{oZ-9LIEJn=4U}v_@|yA9!G#L+e@`hyc%M z6BE|n=D4)@dOAn^MTfA*rru!0MJreD2(JNn?m+fzZz9r%T{~KVH~j!LA$InW_7c{N z8shJ&}84x8m8~d4Q7BFNS$rH12z> z)_ati=NPy~NfO{)coLBaq)4+eczdW4rVPeZE=y1Iv0z8<*@Xh%uR_`OAB90p|fNRMWb_sqO1 z7L`pnE7@08rk!nW#w>iCI}87Z1y(4$z|nbd%97m!I+v>7EGlUipto_H2otnxiMu4z%v0~eHV^x-dQLBP1APZ{hZLom<;2J z`MlBO0BL^209aoYXavvYw?1p_9}(D%1vEu30^k7dZ3^ye2{@7yGBuSJnlgP8Y)b+V zJ_KgUGc`rh{gx0C>aG2GcpC?N1+h`#c>+MG>|=0yQJgd@KKg+_0+hQv_>f00is!dP z{y*rIDHZVnE#XuEIvX|?$|-;=6p*#O41MxM4&3hM1wGNdM5h0EhugZzT!R?mTmTqB z6p&%sHiM6m=x=!XN_2xTJ}@d(15(4w+7XF0bRSL!9!G+lH3Bg zVC+VGFT1{7b}5Pdzq-~gyL#~qWiVe!0nb5w2Ntd<2HArwwYTDB)47zbuA;qa=t>1C z-on&K7ez<-KZgsjN}Te-3kyimz|~sI8NG(52UxhtEL_Z+7zTMwdj`#l`Qeq+3nIr-2k=bvf>+qE*nS1D-)ys$J}RgYPI~QXI(Zi z=A^?8R=R*YN$6!gI2A zNEkHpZs#i#3WCVAjl8^{ z#1KSRZv_mb5*ETfG)xhl*efPJJ0j2)4^2;_dMe1Co!pu;!xB*#xW?+ER^h}qay7Kj z*E;Q%af|gZD&_B63ke2Suf4&+5*t{CKP4!G@2^o5=w^ki*Od|mU3cI1$j|FcFz&JC zTuuC9Z|8H;3!-SXy9i$wm|Q=zfz>z1F{snhblqZl^PrNUg@zngv#-eB^|;tLD` zzg1yKR#es@IL6d9eu*P}IuvgUAy4G{X9C=rgF-5Tu>oUOIJ!qsAh`QOU_2<<`jH5T z$kE$IO6-hOMZ?DC+m|BmR_EB<$1@FGJIiilSMFC-)^*Xgr+?vVoZg`dK|SsD9R_#3hKB}@hIJL14jBaVT5;b3Xgc*p__Tke zdB%d{j*tRUrIZ-UQ%5x!ZVO~TeopQybiV#m+H0lPW7WN$m~RA^Pj@{V(8g#JgynwO z^7a+pdO;kMeg5%Ch)dv?aG=W@?=awNPYJh3+m2SyfPX-t# zi^Mm{%xc>jyBRPGBTNyh9D<-_gdHr{b&pbjJ0nYTw+rLPc4_H4X5@%=D1aKzQBgJX za8Z7bk1~WuKLuPE*mYbE2{4*^hlx*X9oFI0Xl>+GWl8XN62ngCK&r%#Lksk@d1s_ zVF~B1$_=*!=ZLZj)({zUmZLMp{6O{xH1Mbs>Kv-)4BZWxEx+h066~pIz{c2B+J8^;?hIF1?ocTb z7=F;JsT1#~Dfghp9Ha&&6Xf`{@Po*oUbTI3+iv>m+_2?np4y51AB4dqU%ZR`^-Kf~ zb8(8@-j4*L7Wo41Z2yeypgC#7Xzp{jzPgjDNOreoNK}RSV9MN>{toc380(ww%Ekr+ z!dJF&>Q$5If&AUqT1YqMY;IqXVJ#4A88MU##{}bK^i)>NI}#$u6GidlCF~n}yQ@K- z0#S`sMJb&?&-3wEE5nMyp3%3nI&1C*+doC>A9g7*EcNc<44duaF|NuyeBAGL_&`dj z*Sh7m3t6iuY(|}uXxt6sb467cL$qTHUTQK45XClZ6?1FLO`}Fn;WA62&o2Y#Xg;)g8<`4 z|HU~wT_kf(0hLJuGr{?WeTNRgs+IBNUKp7{Ciu@yZdUuEIK*poiaITWQtU&7aHa$2 z8^&UR10}PUqrj{5)}E(Rtg*MW5^mV!D55vu!WSjJP&opNxhZrb;xC%5(GN?P@)7R= z&(vhv(-3{dk^aXw;@#X^94ak?Zc?hyRNCkbu=w~-%>J-4c{xBTpY2HYqXL-LTTLqD z&*>+N@?E`J#2*%d_e_dHk6=`lMi7z~dAED+8y|13}1bT=3y+=dx7)2thD!w`@ zMtEBiMd8(KB-hLFEz)g<>u)>HkM5LvF)Dcy$SWWUITEfgI@M_CC*}YDJ9Pvv5=kdV zf^i295&TEsLHpE>pxw=mj{AfP=-5ZIIRgy|`$vD41duXn2)m^Y4PX>d)I@s)q{s?z zzNgwj7*@Fku`l=v!_}}eEciCAmehO_r+@=1;@QEDTgJ%xGR_g4ZIeb;g z?o>%uaCmV}^tx^if>mAsW`6ZG-`JbxOSGuV0cnur|49A!SHp* zVmv-*h<4ySJ*6=h0!Y<<4#%C)17c>(W3ShLsaY9xSuy7?Z-{SPw06yY(7X6! z^Y!RM>61R+%0c+oQyy6*h->yjrfdJ!lN>Mb+5&6Hr(NtJB=8)nj!^!!{PyTqG5s65 z!V<4nU++FI-z+s99s77Iu&CdwOdHI-vTFjP)5LJC2c*oTGM^F4!8;@fv=%UHxWnym zI@PKjlZ^-7unnJ~)DN48@wEb5NlhD59%X~wvlG%k$QIm1hc=)*=jT-_h>=KOLSDcc zz$e%kVE@@VF?ML)160~ZQ-_qo(4q<$BSNQ86UJjby zB=>B{Te#85hd}#QzA+1Q@f_|AJrabmkUXsz) zuz_K>2=l~ob|wPyWi^)tsZNal*eNN^&(A9d{w=UX#d0LH&AVpC`!SR8H5CXqEtH-X*au|I`=nsH4IFol^HcG79Mr`| z^^rD?bqf?eK)OQF`qkAI4!{3DzUDIK#>0;^E)D&F_=^c^pV;lCNQh zZ`tCr)YVXQF{U8_GA$u&ZlR7+f9GLSSMzI!njbY2T5uW!mkN0YlLXv1&3(1(lMT=B zDeR7IvzWdzLT1@OFc2~SS^K$Q))y)x{tElF4HBH${EfN1%;)LsPtkzUa#ek0$GLQh-Yz5@5W=2Gl zA?Q7|(b4nJlg9KsmlM-U5!R$Fb>BD6ynzAn<7xUF^MUs@^A3_8AFa9vd3gESE@j;a9rm+u7d~f64683$+5yGi{KMab={xE@a%ulB( zl9y}kBp%vzwhw~SZ@h`sD&1TaGh9@y}E>|Nj54-f0GI}1NQ}D#q4>>;Xjn6+CJrrg?vyA;Fv7Ek>ARy>U9cF zufH!SdKLa!sgKHVqte7)?5_3mG;vNSIaA+PQe-@55WchT!e66^qD1Bq??>TeBDQCnHe8Ir2egR=9Rl9-ML z7~La56)lX+A)t)zTAG=Yzr zOzQp=ltD%Xo*t@C+(lIM$S?tFckW5Hm+G zw?0T+eT7kW<%kF|xwCh}I1~hd(qkZZL$HG!DjgWi6Vw|&5?ZKCb<8X!?MG2a14}WH z8KN%wukk@%$j;LgBu&D5C9rzq?fuyBhyo*G=JyYGp_*GnVBLdsmIEzup7D=*wVB;`Sgz7z8t^Kl zJ!I{hX=}oyI{`)l2wcEND0lZ%8)xzy()P6r)SuPzTH zs>lG|e$OeP_Hk=tJ-OUnWwAdm7#@FsdVc+Z5UNGdmh>4h##91M9>r;TWiai}qfot2 zQQ9|*$eb-m592)Gq@Zl7 zq_nOrpBJn(oHujBH2nxDT1gwoVT55$BG*_M1PX!e2Vh!nHJHzBgm11(1G2s&G;>oX z4k-U-+Yl5e)HyHf2a$eXcE>C#7srF8!5D^)Ku7%03AL6s^s2Jnj1V z55KQ!kFPv3YZZ>Tsj4)Pm*)K)d##waVC#;y1K}4D?Lxh)=TTb==4adfv3;XHg_RkD zgQ{C@sbP*^tbVH`(S;7AeEyVr!}!M09=WgNy=Cl+jd(e)zOTOejBDbaX2 zi$pdw>ouYE;}AbI@AIV46~B4!d7{ws5go$T;q2`@f$!rV&8N5><6ONY-)BhCNC7Xy zcIG4631w4j2-e0TKMb6km=q~9j6Ua2qz9Vur{>`*()Ri|D5{X#JuGJ^ z+OrW}&_-s@T1Ygc+V9P7ERpulzQQC*R#CN5L z;lIe>2H;pKlLcjE=75ruvNe5MF17iHo;@@>^l| zEbp(jj&)6pi*FYfQcvt~Rzcp^eqFi1MIpY^MO0?VXwE$E3Ah9Ip+Vlhpipzl&F*mb zqm4PzZs`&46HXson2*XaaM31s7@2#Eh5t>a=}`^&I+f|w=9f{)$8!M~p)aP5*?_iu zuA}7FZPA7jYeKm1>zjw~{XMZfHx!l3_H_x^D8Y`+L-uAl?wpOU+OS$b#n$Na!?`FG z9pVuNt!&o`;v(893Dn5tnbGh7(u<-H1R-Ai1o?Rz%&mVOUf$-b1NMtXs0N23zB>^C z#|C1WA`!eG6*}OQL^(V)1sBi_jB6n0X~xr%a}wqk6Hw&z5Ky$qG8v@r{OXtOR`C~2 z%};U|${!GI{#j|1?|R2*8T)I4;*n1X;(HXHL2v`vbyQlA!Gp>IPK@OPx(5k2*`{T+ zWF+HNk>FdloXOoiI?P(Hm#>4Wi72>KknqzU$vmwUQ9O$PmPt1zd8>Uh_wVa=KHn-gS_NVOUWhP-}*K z=zfaa%(ikfZ%!YJw$1e%ibQ-Dxm2K1!ND`E?v50@m&GKZ+Cntquu)CuAUQ|}X-)|; zL@K4IBL6@L@al}7mLai!sN4B;=&Rhm5Q_IvU1hj~{#kHSBBnuB0Uhm27_k#z_&K7| zEjP34?-e-UWT!T&#`zNwK42l4B7IM{@|GXt zD72B z&aYzB$Gcv#|DAg7EJ1H6n%wZqJ#!`(XWv72cP{Gh{RCnvO~PLJ*|la`ra!I5vQeh@ zmWW=-9N4;eMbGKNAFxY~UKj+mmj-YU?C;AvL0z?hxAVF#%?@<;erw9dc1rh}RNK3H$H2*TgE%YP1rUt$}Cx^nKeE(8&tRuz@t!v4{0sdxAW`v4xNQ*AUNr!u9=MeO zDViTacDwNDg0J{&NjA!!Hv7MnpHX1BU(Wx2pX7G>!eii8iBJ0ggti;7<4={BOz`Ov zpyj_bH8=17$1Tw%)uT$Ke>B*sSo{j3H-HTDTr)6}d2JbxK%UEy2T~!5gl(%Kw*3HS zcjzQNECl-jNRQ;Kc>`HxMj(SD2X`g6QISxTwglAbPlpmAmiq;$uFQo_9_q>uO{YsV zaB`VA>FgY>Btcmb1@wZ_9Kll++5hB}!zX%(ZGdz-{t8yMozS%h0m?b0M5-*<*_|)0 z!Af?Ebxr=)WB_LRb@!r)j0DJ!0(pCs?){+*C8B@n`h-K;x(E>TUv|FdOZKKW4?Y(Z z3@|U|EqCIW2Qpp2dQ}#_DNHf}i9*F7pa-Inx3*nS`OF2KvXTy*i{9zFTNMi!eU0yWm$(H@gGUoEoMe5WjPgKsV@L5ciXlq(#e|gx^94Yz6haBWuV$Ur&>aWF@KhTmmYS$``sVz{5U8KNlh zJ_KQp8R_;5RJnWMT~I|O^{sozxpAN*nPAG-FAE$L$$scgm9UNls+h zrl=#*j!*fR+J^3!`wnJ2ljU zTkYar>lZ)?-2x)!tBTV*CGs4zb$OtqWwcF&KGSX)s4W6^_u@kM$DT8yi10--_+lb6 zkrfN#|J|4fJ6KZwcQedTE9Qc2W<@$v{BW5)nuRj#AulTL{G5lmH{_L{Q-T zDH8>z%aW()xp~E`yzq)ylhZ6V@VM;0cSc@8rX6B-m{aCHC=;DL#?|r;7m`P(^o5 z1$mA%brY_p$D14)ZI7>LQh{VE2~+F1hC$d~vBs|1+x3N}^qOP(F`ftWfFl0LTOF0c z9Df}WlnkqoX6CZ^I=Z2^XWhi<6^B43lUYr*JqwiovM+Z;_y3`mAGlZT|nhT z8iD+0`k*;OW=0B==?}X$IyfMKsp*k4_uLMott+8sB-^=NmDBNYE@lLOAb5L|v%dG; zDU9F7VhylR+vuC=0xe-0;ZXJK15TrxXpLu3DVGQlK1VM=J4sgE?|scw2HgvFY5%aY z%tvlseExxENt<*-BQu60MfsZJ2fYQ#)9~abEV8;%@M@@2u$0j4ytEjzUY9-fwc*j{ z_VO?2{YLVnu%``k9_;(NZp73naZGOQ2U{IOwf7Q>v2m)B_c&J~P=6FZ!tb`9&}a3L ztdfTPHy|@7y@FUXz+Sv`=Y`O=oSslaL*l4zdw#uUW<&OykrLDJ&poDQ59)*8i1Dl* z^eV0{AdV;0N69a>`&Y!KHyKASN6>6)KlmT(i%!~4j;pnsa;QEcazZDL?^%R0)F7%# z0LuAi#Qbmztj#+3A}f2Jqp4~5_E>~Gff>=_y35utMbP|Vg+-Vw_Qb{A$i%uA5ia(~ z+z9;beXoDSK8N>P&@0$$cV+4f#N6+Uz4-aViGv0iYfTXsC3+1WjP{4Vt?Uk-vQ2NX zvSVBMmb8BhK*W~fq8^ZDmN^LBu|Bo6Nuw7p5@Ggo-jbO7Or?(UE$#kAkA~wsx7=Ja zsrTE}MO9|?FSWV|6Xo(A%^Z}y&-=*r9ZadW3UkMBo!)qgUFqebqxtrFZGKcDK4)yN zGVK_)s3QPrdM2Nm17+!hoJm%;>mW#8R(+^I_aGJS=`*zLPu|X%8%SvK6$%?I9VY(O%MiBNF9SN zo_`5KnkcTVX>cOqH-aB9TC&YT0)Okn@}>z<{Cb!56*7uA?1TGT)VqlWoF#AVy0AcB zg55P)7)=4mJP|D+3~!iJ)a^JyqZlWjB9BZ9jr&C(UCV%AH}~p#ki-#WaRfMS4MDQ4 zN{m`&rxTh^2sId2K_SS}tx=A@W4^6-Du68quxLF?m)Led%#{m=kioe|EfDO=FGWSw z>;we{v+;di7cW|w_pYN~dn0`o3Ouzl15~--2a!)Ao{Doa zgn6ZA%1!uq6sQ4+h&_jU+A;OrUyO4}7?;N9o><%-F_*V2OUI6PWr@K9s$`~6rvMVX zLG3NO`?D~x&(!Nl z(@7vZdF$qGXbvMVfc7jebjW7mtegfl8rll@Hm?ypcdnH)~yL-+{L^<)s(WCgDJai6cHho z){SNQ-iQ3(d#|Y}B(}10oh^T^KJ7TICX+S|C0^E?OsS@tN!(TCjtZuSC^DmHU%J9B zB*pP+57V}kx@VlVpMLb%a|Je{Z~4Kg{p6`f`|BWW!Pz(|6wJ9nPAW9@|5FDEabs2Lt;A2IU$xW?X)9mJM6V|jQy zdv>cNz&T6>O=u!w<+F>~Bm^``zDs1C@2K0W4=pWG0 zQvjGF`yhS0ENAZ-rtU-q)Y`zJAfBd(9KbdgLf61%-VtbHiA5^5dB(VnQA>6}&_kpz zu*3>fz5Q)zg>ba3JzIMsOnP|;^Z`)r8krbQ@V9M%DQ<1YVugWFJl3-+ZUUikuTa7) z`on5HQKMldoig|7%cggG>C? z@lA0+0eQv^iDYR6zNy-1LK$q30d$OdMHmngOiH?D$egMm{KlgPhp@qJS>r~Fg6_Wy z2NiupkD{&3`UB737Si`TAjT6juY%n*k9*|)v99+DI&VgXC3#d2*jPu?) zga2fZjwCDA$T_|Z-Tf`7TXL6A!&%?g&!(StJL_4tAN*@soxUz@ArM>SNpb2GSW!VC z+&dY+nVdeHF_*+G8dfAVg zUXT@&1?D0%VH`&QcZ}?!5|{=~4`Qi|ej!(3x4Y8m|{3papGGLVdFmfIX%hyX#-2^Z=F@oB4H-nKNc)r*}cKv27q@mR{Bci09p&HFT*}1DiQ+hmT{oV6~MSy(OQ=;p_C;6ASY)w zC)$!EDTL;1kDm_Oy&WsU>&&;RWDv%FS(@VSi^I*4v z_Hkb8l~U_9!^@s@>4$#}k4ko^f8xq^jLkSeIp2n=wbzQ~Eol1#_^l}kEXz}a(lA~y zoajb+%)4)rk!u!{K%4bLBzTRRFf)F4^T_sOF)2>mHde2_0(t@u z#@6CJ-Aiv?0W&n^SmR~g(tuuguRzm)s3`!?_xUk2UbuVw6Muf3FUgbHgc(g+u}|_S zki{$r0$vOf-*A)nt-ML*d@UbN9l%v_-a{LHve_sBTZ`TWXT@BY<$tcSou5vbuj69B zI&&ogk;Ay2`E5p=-4^f@3(tD}m$AeEfXg#goNUE|pZp zBgSyUnYJG5;WgB$WakGqjGWSsAJQ_{t?!=fnbYD9`wG}_C|8*ky@wu&o4at7C-=Fm zi*N)Q-sYz_M|BK5<^=C>w1+QtCi4vU`JEVRLh_A2q32o2 zE&uE%!vkA&afR4{`JT~&0X9cX=*XlX0iGW!(a6Wr#C-PySEvI}ESnGx^1xU8&f)=R zjud95tc1#tfYoRK?-?>_S)4fa65=E*tW{U)vQ&v;T&d^|VKDMkTE1=$I|f`0RT+=2 zE8Cl@@RRGG#@%6KC0Vl_kOqBNFJe;$d9u3xU6+1Ake$79t6&*#(5?9B+xjs^DeU{Qwp98AX=4wBq?n z#1sRYC1}h3Fm1VIQRpOE7O?ZnOMjVq3?Zk|{wy34H7Q{ktGE8V3wP04=@1TFHW}gC zYtv{TTv@X*`5%hKg|NM2Q$0q)+GBsNkCpU@z<4oP#?!)ONN`o9cDNZq!Ba@0htAY@ z1J52I>XKab?`0doiUnbTw^^usQ2tAvbrcku0G$AmscegdkOpQ>1(Dxuk-};GMB$B4 zKr9sam6`PDZA?5A^?WDiyx>OgTZU^$&zDxz(>8LF$#^zRk)pY4T>lZuXw7c@yB2OO zi?-92UX|1y!C5Ra`xbAhJ*IAOX{OQw&XX}elH&Y-M&C0!r$KjEngE9|;%wV=>> zInve3Ne8Exnma4g8XO(xVKAZS(|*ogA~mEv$A8ng=|0Y53g9kK+SJ&4-SjW-dzw0@ zw>js#X?hdyrj?voy4A}2(BRaZ$FqCDZZsf+i`2$P3+u+H1~pR~)2<;Go;!YuBm7gm<`)+FOTY}Ffbez4E>t#x!rQ`S z0;#{lQSTz_qY^8B+bVgvf@X+cH66n|;W4}<4u7krd1aqnNSXaYhwcv%8ZLh4fdqLL zdlLcxNBw8u1utcwd(m6@mlcFs7rw87{zW|^djLa$7x=$>gz@{|Wd8#!>;Ga%hW|3_ z|6;>(7nVl(d65&U3W|$Bl=2xG8uq<<$C$;vOc%GgX&rn8!w_-JYU-oWa z1#3J?Ia{7JnPid{Y1vDV`&o5N=82`U1Tqcv-RH4O{cgy~%CUz^ubzqHZUcdCSz+f?8`k^@oT;JxITW2>gBaQPT>!Qgen|4uGgJb z{~g(S&qX=hwbQ9Qh9Te)>n;?$oLm{SSD)^WzST-y{z%76m^fCBtu*tl=`%KHsOPIv zHsbk}>oJYK<)K*3aY0y;<#9-%f42Rnf+=rIYV>tG%bOey#mmXLE4ftMvu z4@B*jLO`6DAprwgz(8T;BN;bsmB2T7f_1V1cZ;x-1I<1I?JkH`h-$L`QBJLc~Y}002nT)s*xB0DL$A zfHOjXjp-TgPCx(vcjlgHJy#~sm*BK{W*!TU%gZQjtose?otz%t-(KI|SlZfHJU>NU zUYyS(*T$!(tGhd&M|!CTIPPt&(Y;pk$%B6mlSdyB|@|1V8-tfr4 z#@fO*a=G+pRZ;oRnx<;R(){ni-@RQW3*&`jLq&~^Wm#q4ntMC8cejzN^MifGn=^UM z^`(t1&GU#!Ig6J7!3QppK{L~%ljB2(*)gH#8Ue|1L*paUlfzs6YpY#|>bk1&2^g!I zjJco3Y|9wDrmwH!S8Hc;XHWb1IIO*^xwNLN=x4D@RM5=S@XyL0jn!qtqrdwGIy-yY zf7Vw7B**KzSU0uT54L>U{FQgm1+8taCYKW0SV1hd7Bsfif3Nz!I`#FS_uCGveSczb zs-WvN+S#RNT|wvAK#^Uz+LrJ=oaUFJ+eqkoIJK9_k$bomeec&o{fY zuC)g~R#ek|b8~ZWco>vbU)(zKvG`X)c8ayXr;)p@nvIF1$+OPoA@$je(76gy2>~W0 z3AU*Z*kkWqXN!Xqqdg-7uwNE>Mg`O?_ar0g@Vw`79eVhlDNikqQA~suFLhM4_O$g5 zZq3cEtjsUXPc1J@e`_2l=|;@Yjl*EQ{R5r92fIf{2Vt=FtHd_v6f{+b+$kF|c z-u<=a{ng6-mEyh4`ER?h(bcj2<-GOfxr+LVt%dx~U!`N?L%)Z5Hf9Sp<_fmY4;Fgz zR{Nl1zY1o%^5>^J27kAV4YzG9cK7u*Z7<{xZ?5%huC*c8{`^^OURr3Jovs@ltr_V5 z+1_5!()zQrs~+~db#=LA4A#=sjjt~r3In8L+#Lnd*D=7T^#5<4H!t)*>jD7QX6j17 z=kKt#5xCxSYSa;WeW>3nkDri=`p}=YRznp-^-8ijdB>YG&W41(6=U@E|LrHJ<@Ocz zfBJ#QK9!b~jW%0C@duT!UxozKH#Nv!Ld(Ew>-(_eLhfKlAvF&GxS^Y>D*i%uZXTg3 zj3ZM3z{zT@fB$VwS|}u&*#HnSBu5f4&E@}Geh9bOHyb;|rNBs{q&)9_NOm{RH9jly z^~Aa_9!|3bW@u=!tbfdcS_z)9F2RbtZ}^H)MFL0W$?1J^P<-)SCRNNs)>k_m0!gD2 z-$Fw4LtIuQ$~|vEnF6O7ap5TsXE1!lpbW(EPk*Z#1o1So&TmQZAV9A?1psZ0LL2&f z)RPtAqxKkI6y<%fs#We*S5a)OdirMIc=jyU+fcTITX_iYYNq`Go7$h$yl{;l5<# zwRuD~p^c}?DlrkoW<+2>5qG&-id$)B0CP~{(-NHgRZ4f{MvB8utm4gz;whd-L}zF~ zc$8x8mFHYhp*D$s;joA>;IBM^SY;{I{lK=J51zz~RGZ0>KpIa`i z^=D(r(}bs>4!KlW_`Qp=1YLC1RhJWir zb!+)%xwgDCGu9&eAiJiT&M$OMy+IM_g4(bESkzbD`E$R)^C<{h&ivjzujT(9CH9tf}M?U1&AN9~s&CgfFKMKwx#4Tc};X|hY=>>$U& zjDMK}!{7L(50H385~*6_tzJ|A1Pi$uLz_oRfn1bO2i-weYY&#odn=&5xFDK_jqi2# znKa^fYNubxDqE%+G{Z^cQNY~$jkNG{Qc{5)$)SY6AGl>1wL3j zThX5wHEjR#u3WR93gfY%{9fD}wwxA@$D5x86T1iMW`B?d`bHjpdNNx_+`l^9n@2Vb$aqDo|j~?W2{P zh<^ta*fguol~K9-W$m1CmiT(`IZZHgwJpSFLjpy+_=jp;(}X!z^#lN+g7rQAAzJgD znW{2+un-HGy&->ES#OfUa(9CuXtJo9?PJhnqVedX$?iFpCeLivG-hdD~AnGxz%~TGSEVH zpAz2Z=)$!7Xp+UtPM%D94+m8u_N!b9%m1sFP8xNUo;nC$aym|@cD~s2ag4qk;7x@^ zwEWG5epc}H2_AgbvI_uuHP$T}psh^vqMgH0D=RV3IbFYITf3d@g;-QBZLzV(6Ga)G zl44$5-$ktlBH(FC>7=G_8DV(LI+~D@{#x66u9wyaWt>jH<*A_r(D(`ietZ zoyybAy|BQP-)7JHW*%BHe93=oERq7CEba}<2-EGwx3%mQ;QR0#Pd_sHEIh75ss8ut z3@*J3M;#$xOW3^w<)s*_`jh8v!(<|@3zL3dvth>lrhPg%YNb@9qs=+Alz#Bvri zjpXApZ&w8~Nwj?PGJBloX;Gm&k#B5fl`dM{y!H<4$@@Lf!VabQqoQ5cu)Vt`c`IFb z00hYTWKvzr zy9Z1#GHfN5&$ph6>Ga&T=*|N9AA=#O#*_CoN{T_XZxbnA+ZXa6hUj#QdV4=>zF}L7 zT}MtguN^9EMJPr)lb1<&C8nhmj#zzcDM_wP|HSdT^MR%I^@^2d!akumb@05?(A$Ug zIzOltR#HZyGV~v6ZACC}jLb!}xqQ#!+Y+zXg;aRx`sV)7G1$_g`%=o@twJ#Yy~{p= zeCI!W~5cR z0q%x~2mT0*J%@}G8N9(198i6pJF^vRR%4F6` zpfNz>sQ^La4--|m@xIaqA>()8n|;X&+vmiO>5wGPhTaQxJ$_je@+#{vJ-wTTH`$o| zY0lxdE1o;e*ux1?GdkGB2-(LD1oA96E~CHC-L{f1DqPpbbm~VbeG^VP@FDEv-{m@1 zIv94!8)JiHA3^Z51B>Kc?<`2Ev=LgkcHN1!@J+xwD^m{Y&n#o_Jsma{~MmcuKZO&6T5^wV)o&Jo32zK8(V-0s44?I8rGS%t}WpJN^`r9}6 zre%p^P9qdU_U`H&Y`*!GDcwx*iIBNkRT<*LD(OD$Aey^E|Gr1lK_+tJ)9iul1LA`( zf_DX<$#NOx%Gs;Ycz|QJ^OoGGzs{*Y_X?g`3-AGfW6HltaVtY=qcMBmq$>f)%2J>& zR(N6#8x8XPl_}NE@Jqlqz0B1^s8;(=A%xHv3!d}LN#up}+;j(58h>y$z<@};N43U+ zgLOYG_n1c4p7@=ITR+Y^)ifRuV(mP+GV@ zdvE_zj3KEy1yUf_XMy_kP&?RcWR549L4*yv3L6RHH<7Ch{Y%$#lhU_-((G=Rl6t z`1(Z}@n^QDfab8C&I~2PjZa8Db4ob&a4pz8n@lAy=161=ywZ+*JLTDR)SjtRIPv%- zRZnH^{iCPO&dxb3dAKGG(#%QsoD0}-S2*4eTa2}1$&fc6IXdo0oR*N5m-0YnH5#U8 zv>cO)#?#TS^J)~{roi6NX@2LtFKJrG27;3^Vt!poG$1MV6uq) z^%EOQ5;dfr`UK=Mae6~6V;cDDj<^pt)4Q)xvG;N|H-6Lv$0N1MNPzVkEd@mF0+7hS zkhL~i!W60qX9_(%KRhsN13KNE8nbgp%@ZQ!8cBH?@GnES6fb20lhh`Sn+rRzxsOQb`WX z9lVmoEyRYC1nrtjI<*RYO4X>ye0)mIUu21s2P>R-aMxQ(NA$>^G^#YNyg=PmG-MaG9VI;hw!-;m1ObWE>?&YP)}2 zR41U|PSb{nJWXbNysU?HWzNO2jfG@wc)|SnD_8t}@kDi}Q0lsMzp^^rSe}Fr zGg5FUi}X3C6K}!?c`~J&Dq=Vgl{#qzD=({n%wr*6QQSBcU zJ$;n|Xf)DyERkGu?0+5lgz#8>KNm(t%j29qQVx|m7_;!mlO#6l;k)1VlrCalU|X@ zM^YAao}gV`ArmcsHRo$QT;T94fRA3Npbp)BD3QI@ga=O7V-H1HJOXsQk$n^!d=+(~ zfz@N236!F*T63|lb9@GwI@z=JnvS(JSLYhKkOu(wKRm2=xXt-mkPVPPuB(EwKN?IR znLur#_E5purl%(4C^y>gj&B?F(edP4x4Gp-ZfY?ER@_kbejpSdf!fj7>mooWIC*qf zh@BtEh0K2lxdC%l3~=HTND?$iC+oWU^h5j{aS`5Iu5d!MjmPfQ!2nXH^1uTw_mJ8W zX8VWHes+3-^A0%(NdXc{gs_x8DSCr|(BT&fTS*bF&5QQ51kz;X+q>vT128BhfeG|L zp~}&HpP?Rqw&6Q}ySR`Z7nwVO(^hn8&zEt@F{yiq4&1tN;7Hedo4(PWm8io`B5v-h zO3i(1tYlH@=lnslgRHL5BP;mRHm@DCe?@fZ3UDAiS{qJ*5d@Pz|M!Loh>ydB!HpT$#SCDW17VVMCLrDa(y0E$bPIRqb{vBR z0|(@~g8{r{>~UOIN9RIE&DX*R~GmWwg zK?RI?y>C%9Wo{kYURTc*%i@zrVyqhqDWBqBd%blmeqr-twuT~XDNna~O*m?mA(q8I z>JQ}uc`_L{S3gGi8tZ&FxT%A4Qf|EmKacl(&wB1526x$1OE_nk0vEL;+v)->`tor5@N#+Jy{w$(WEdQ)Tp2H!_GP~w z0NA_W>F?>O31z{9!U>`9lygsCSvHUfbR-vt$fTnc3hz5EwojKLx^g23*CF6Sop-Hu zU239rF<82=RVbf0P?+@IBg&c+jiw3L;RY#?MpB{A-$GlJbpve;6vTsA7GinyO{;`$ z7>%hcGX|o(E|mCEw^U=F41LXw(+MxL{{8(U-P{vuNeuZfVG8EoC%2S+1cuBy!r%o8 z)pG81SQ?-FKx6f4dXCfahvlL?`nfcjQP>)7Y9$op-FV?2TlcH=m@$N*RELyA{>{@) z6B#?;%IhA5)1c{`x)QkOn@Kbg)bHhan%Q@5-;`Xy*o1d<`LiN86{l(rTNLgeF~gaU zO)nw3rEs2GEDBK91iA+18anw#I`G#}pL-$HY*s~g9_yib<_nv2-}>wY&Agnor64fv z_(t;mV=6Jq<~Z)Jo=2Y{quImCY%%}wxm8RK{m2>gY6q7wEHfWoY zvfxiR=iXOM@%z3UW4)7&xhIDJmSlG!Cy0+_MtU|fyJv^@! z#mS(fb4!o26$7Kv^!R)KAaSVCZyOPUHCx9gQG}+v4-bLJGXB!|9p(Ec%R0%BoCf^I z2hwpe$@4 zGS*&J)ZLfqPWV$|FLs&p;3Us3IG$;0XlScI9SsfTk^NbHA(A;%4IVxhA8;ix-h!I` zPX9rdt939yq6|@9IZenW_Zc21=||p>{495hF09Bz;YJ#|Bs@Vaql+9r_g=pDd7cGjm|;wo%1XIC63h z5joVvTXakW(|~;Ifj&pmpVvu!UgWDgDjHAy;z5#;<6fQjmbZdrZq~SPuV@V5bd{y1 z?~kQU6MKTHX*I8&okK;o3%Wl8yC2dhhR zId6&E_Wh4J1q_Z-mUDDXs2~yg@=c((+fw1he&u`sxFE8z_(kfyk#&gb=Rv)`k%H`m zga@0$X!*|^uZ%@~mlr*cETz~-d7RSINbN`>I$opN?dtvUDR50|8uD)57efY2TA?s^ z*)+@GbEg*Zr!e3GD$8$`-Wks1OF%7rqUSM(KqSde1gQu1lW zSI1BpCL`Cr63vZV4%Cr@wDv&)nUC3sz_#VJa#y3<0J`S+8`^uJ@r0Yw3?6X3#K>^v zp090O=d=mrm*BUHM(5py-ePRQE5}&ov{!^Obb9c$)rA`g{}^E4>8#sRemuMAkbt1d zn5#`tYi+Ig)2aKY58oX|yn#XwQoCv0I1|?-k(Qr(olFz0NgL|FJh3`uzjSx6Me1(V zM}-Kw3L`^@A2I9zsZlBRuHH2-QKVn-D>u8=AyOWc5-N0{DYs+=7m8P6gTs=@95pM% zLwtXk0ZqFH>VbvGgzZ}tL}+t4(n?w8WfSw9dzN}(<5B`<4{>F_(P|n)4{vIQz?o1$~$A&K+ z`U5BIf0BZY-_po_3^QSR_KC(tYPu*pCgl2{XD9T!d~}1A2)*dqF{={VgPTR=w5Y~x zlzo>7t%)%(EOq^;%7qk6^rQ!KwBPY?XW2oZ5wapxpX5Ft1^Ir*R{%Dg&KTj#zRw0x+uG zSn|Je#h3ZoRp=q*Xwi(lsAz%pRCi)R!Vw{fEF)l+QUSI$w6wG|wU`N-;F>A{b!D^V z=BGC+|7wx5s{>fOO2Ihtkyh4Y6W;!aFk93VUXo~acZ!DyT@WJpQyK1)&5aCXb~)(? z1(I3CMMPIWv{cs-!$Ews4j@35>A}kd%~j#b)F3_hX+%cWdp@K!L3^@Qa71i)&%5!h z2vq&DIMBxmS>Tt$=$Sb&em24w4g%uO#|- z(T{+kL*B)ue=;oGE+7}~Fk{6j)I6zB2=Z=HN-uaG&61&x25`TO@*0^6j*WdNp1>Hq(-z_d}?gu?~{y4i6?<7nNcHDWF z|JZAEQIaoz&L;>Hv9>zlTy2j-Wdgmv*N;eAAJ1sldL0=#mpS`0q0;p)S%d?NAbk-d!W6Mk|y55 zQwr52CI0a03S?gCSGFw29=5xGb%fDU@R+P&05t!#;Xv|KicjJ|jB~1-y}-_(qkpWk zdo$4MUP)XA>G8^I^kWhOc!r^o>gSm0XU-$#*tdxyfg}JUXS^*l2q3p-Mu~CYQ3&?g zQco(*E&AJh;eT&gk$;m;0?Aw62{8!%dkchP{d0g@-BV#4;lH<`5bRro3%LK!VK787 zy5+(^|MwQ7?|%x!!kjVBGcDGtPGM@Uw?tc|CkU9|5(EHsWddiE|JsKDopIE0 zbzb)e_twDVsMhP*g3IOQtMkYN7P(B@2VZcqFq~uS3L)uoR00E;{e5$?S@xuS{-Iq! z91RH7BQbMsWm~kzyE5?b^*xthyY0e&!B~+uKU^pNRL?!578EV=rgHej(~+;CZ(VwR zIjE)fM{wo2gCZ2rG$I%W|B(1#DFknA2ZKB@qg+JbE{>wLC|Aqe^)bB~IS{1`djSR3B6T}4m z!H^$5Y#C@KY!0)TR}61?hGZhjdWH=9*mEg?QVcT-((4?DDEJ5$Agk_^jwF}X;f!f| zYuS(Rc0PSQxjMP?1Lj=k1BP3k5vD6RBmU_l2AQI%wbeL|kqfk#Iw}CoHK?gg$Y-IlO z;B8iAuKTC3pH<>@wHv?i+oHcUcnRRz-+ODB*(IUf_TG_BgAUw2CfjX8x1r$Wdu6SJ zQ=Q?3z_KLY%aqo|=uCktOhIl@He8TU%{%YSfhTjG3N}QEYb319U!8|Weo}?v71s36 zv`p2k#_EB#EZ6VE9cev;-@K4n<8o|RX)1jGgCSfNXvvQ(QD`KAa$a3}(vCt8HJ^5K zEy0x}qvuvUb%VBD71|IQGpH=Z4#Q+Tr*wjsmkkk-cQHB8QpU1UC;c=50*g~^!p@mM z7JEen`PIQZq}s$jA!xj7`qdkf$8!%+a}5M)991TVM^0OtY~Ik*vN99C7!uRun5dplPZr;z~*Iq(!t)Lk4x!hPrqwBN8f8z6}$<73UpiF}O*lNF+HlTv!XNL1UiT16!aZBt|^vAVd(2bx#Gth&O~nE0y2{3f8jE++-%QaMY5~Yce}%x0Z?2*e)*4ogvhx;Au6GoHRf!a z59lfGOPmv&>KR6;d%?C5yY)goFPsAqSB&N4u2^mFiNT3qCAA7Zt`^;S{!|g3G*)hw>zUQ2#|ylV9p!oR zKp3fXS28OBxMww}yX(sCVW9Sjm2x@^!lmgROLS#M-EycG6;O9JXs$&CSo#zVp#M^4 z6+RV4XGtEa(+L@!rU#)m$~OGp{P58fn?5lkL^~iQOO>>kL(CdF%aw?51$5YUzDS#$ zISB>NnU-lGn+_6;0+Lxe0!fE3gegLG?ly?igMx78z!clhPBQd4nRp5S~9) z7)4C7q}mKbag8NT4Jt3^1&^i&y_Qt#gq&6$Pz9nScQzW0LdxEX0)13hPwfug!o>j^ z6j{<`k{_N^p`Lzt%X&CicuI+(a&_9Od%D*Zs&p6q{!)&|dr(Y*8B|UP)>o@gOMNg( z7b-X^O(_ihW6;cfIfG+rMO9LuMg*q2)kAZ6A^ug4=4li?>iA45)U0x@SppeFd=Lv3 zMuxxR`AC28p^ilIGbKI7VyJ+S>&<~^iJQaoWf=@r@5?MxF25bv+`wZ6#nb{kNdLXc z50tIRo1>#FGcx>^@6P+v3Jy7h@=hK!ohd#-A|hQx4h)s!e86V`FDZsW zsDzQId5i;~onO=f_jGK$){nBCkH5Ecfei~y&-`vS!4$&CP6$<~?2HKl$KTGjQgIx2 z8(K_OQ`0dsA@A|+=NI!b+FKWr6h?mAVf|f=z-G_!F*zPCz3ln2^Pti{kY(k6;K+U+ zS(XEQ%N3M`^^FfebrICzwlj5iG6v&HcvyhO*)dauCZN8jgdIEq3pUq6gi2 zSpH1DDVx5q2=I_2*I+NKoq zS3~@4z89Y>*ewOuKyZ>?jkr+x84)l+u@zL21WaM^@bGvz5J_PA?}0*+6@=;gpB?{3 z|KCm@G9{D|^C~5m#1Oo3zJ8iCGQ^~S4;7I?WifG^B3VHyk<6+izi1^1vihKMiJUbU z%D+jjWbE0)NEK`yknWt$_iyJvL1l?j-u@6pvD;e_GldPo;Ym8O;h0G_$WI`Tzk(F) zUHTxgUUQ(nhUtXDEwNGwn>nDPtf*8NMx3leXe%v7Hl1@g9i}nM#!SFyV2#lKUn6rn z7{K=z(O)H)rTm>34=({g*+F5X1kG5ORvxK9Cy8Ms(7%<23xw$)N5hWJ4+dK5w`<}! z{NGy?21lICr3nC}LLFxIR}U+{ynwycl0Pu8 zl5AO3=>F806p&UFSYb8#VN73T2=us!GXKC4;sB`8uVH`$h(u zDwF$N;f7cb>J#^o^JLINwD~8|l_v4k_-0Dmg3J`_ys2|0lZiP%Sr01SBn34ZSRWsM z;lqzYcyI`T=D(Q@c}vWB5xXcKDj{D;f{6Ql_)vi@U!rA*d+2X`-{Kbu%lO%^KfgXy zFoJH(9QhO+x1ZZYzht?qLDupnsf?Q!WeVeS^<^#rbD*SMbGo)C>}DWP*5{1O72$WY zpS%{`kGqIkY~nn8)OCN2R%y6=PQ6w*n^2qd;JZ{X2`L#>s^g8!Aev%E3yaE#>0At-ZD}=wn0t!BO(!~1Rq!-thGMu%htFW)&G9k?`T3 zl7gBvewEQ0mWAmgeia$<_QDl0jwElGd|67(ZJBA*BK;i2gw8duPl%mv3D9;~{9{56 zX4aQJ@z1&D@ZP@Hs){8;Aj7V_8^t$P_<}Qs&M#TJPyf-H2hNS*n5Leu`g7#+(ebf* zmPo;IjGQ>Sm*8}Q^Yp?nTW$G7Wn%R@-AjnB+4h8A`8*=*dP@iTtrcWH(N|^@lPkn`)JhF53ey zI~l41!rVJweL8rB+`(Zwu~^O^$ml9cAtA+n%A&A`=RS~T_Eydodf|bb(u69cUYx|V zch7EKioIi1$zcbH7%L$mPbkuic|mLUXwh4?2ELDPx=p7-VvSZEz0zLm$X$gs>l4ce zQA3T^oG#Dg66}ytD41e+_EsL-Yx^=bP&%!(78{5~_@fkd>d~`}Sb+9B%9s=`USiNp zJGr%hpOdHOQ0s_`MfH7N(80NGYjai~IGUwUBwV&rrr!0kULp0WapFmYBZdJJT6B`_ z=(+#yLOP*^MQV6_&(PHBg#^@9UE|B!j0+NqhD>#4W|JqyhJ4&Hxe7jzmo~_!iuh)q z%_KrPk1UDE2Bf3hP6ZWU{WzH_N$-AoXr=7rCq2Mc=wandy#zZ%asNnF2*3WrzXdy* z+@Z_Z+RC7-j~o1r?5Pabxo_2C<|pS7VF@K;Dh$*{wx#v{T()qs$cM|~(MsCl^Mk&4 zp19GMFO6SJM&~Nrcs0OshHltV>!iU(tK+{r_{fiRyuS?1*eGPSFE&0Aq8_*O^)ghM z|1+d<65eF;j8-W;n;uhK@9iVik~KjpchYPE-2)m^bqJWmH$H8y@p4H!u6zvf+u`N{ z&7|Zg{|I%UuDQ#LO47YJ8jNI-4Y=HQ6IsY}{oMkhgPxxL#*#5^VcR|NyIckij@7G7n%$uRdoKlKpZabtsjlB>)(^Jhc8@YB z$VKJmx?ajUX_qA5%_N*ouyZR5W7dNR+XqI#FJv9sH#5zeQax(0%!}YOG?HgV-3u>Q zf1dg4#QXV5FoP||o!{Y*NRik5K2Of@B5w#!DWReO3+n!|`^q0nB%>3jUhgC3&S_Cd zjIB!p1`j}o*-|b_JL11kOX7UgDUt5ft7XoYfa1RejrDywGaZ@+w_4c9*9yzu&*oE3>k+h2Vs^&}xYYLEDY+zW{{6*DILq*h z>uKYH>DPlcYc`eoZW%s|CN4EbWltRVN(R3w9pW*WN%0H_AEsyajCKdqL-^x~nC7i7 zcS@8Qb?GGK?_Ej9U=l6+{=($vcJ5Z`<(+Rug+@GZ23KkRo)9V5_&+af*7mQqT>z=Kj-7)(IS$PWVTE6~T zPEpeVKw>bHLJdnP}U^kuVE$cB){OY^8Fl5Au;uiBS3PC8zU#|71G! z*>sK|$f1an*X8cQJNZC&4DCo~@7qfx5*Ui5AVb0OWRdab+!3um9&U>qq` zvzFYIL-e(~J>0ory+u81zB=5^u43rOKYhY)$sRtqvmaP<9{S$?Is+xr-B{mUiBSY84Vz`-?%BW-EF$K}E;3uDSUB zgM?ux11EhKEzau+LOSzOqGG%Y?>}BUOPv+$!5OY(b9^eacgmXNrcaK)GiS7p?HP4a z0SM5oXM`fH>~V7UFxO4M{OWmp)#>_ubxpv2cVJPsQz+A1e6l3Vds`|d%+cZ1O6|gR z^y6y~0p>Cb%kKu0#v;8s+lGzGy6p6HkW&yE7ZB32jyayhxhf?DD;6;^ zovKUgjLL*Z z{m+zbXd$qbk41lYpf)MVvnyi0eRbjE8yjMUDlq1jpqHfyt+XK#9eK5K2q5oRrXdlO=}r^T3=w)LKc15XM_ z#XTMy8HlDacO^04njU0iIz3nAEn44b&0!?Q$Ui|ILV8yHq~%-oQ6fK+)yeCk;fSS8 zsTI4EuOI6+Lze}+`~#w&I3LiIj8Uk#k5ZN#VPgeBI&QFl_?OHLuY4P=i|WTe6+7Fp z7f|j!56smN3Y||>t)EmZ-H8!;Y@tq{@`${oq!tHr=6%C{yLbjryT?*Sj1sP*-(5a> zK0(cuS^2o21yE!$Augkig$QGMRPIp^-aoC8gWOZ*X;b@spC8tacc6kFcehdBc&NkA zS<|fjNzw1eex9fF6vaLonMUpW`Dczu+m&%LJQ>qmpH>p|YwdsnAIn{Lb4FB@$+naQ zl3D_pp+{e1w<;b^Fe7>rZs9!AsO=~_OR@ZYO%*DTE**z)8B#uZI3Z&cBycP5Vd3-* zBdUq-RnsFRu#!Ous(VbDWdnDh<3U<kJB6ny;J#;lOo zgH|k@`}2non&VwC4`o1>j$MQ@kcaNbpj7PkmezM1U@dw3hSKFbcvp&5bLQoCS#hl1 ziYAO$JkeB$6x>s@Ln>6$@qtxm>qy%@o;_-us5ZUk(H)Jm*X!Hcdlr!)%M83jG)qY!RpkUwC3F&gDs%qZBMS;3(n&}`3`aLgE`4nwfVm)K39@=7yj{hr+0^B?g2@AVI z2ZspCs_lIJn0muA>9%&5odLPWLFz>>0kzbFGs#fK+VCBV(aoDesIx5cLUmQ*~M8}N>O>IR2A{9u3`Q=XYV@5nzocyINulv7#Klhk6CV)?I*gmUsa zfNX9Zm3TbAty5tta-~C)TLS}_zhm^Diortfsr5QY9FKPznIG?+s;C7NyzeG#mZv%Vny;|#mfC({E+0sik1F5Ha)%i z1#tQEF9U(H0QUb`RSN+8D_3C{yOpg6L+=)A(y6$p@xPHAX6NxeQJsGefj*#(Q6Fj< zF>TXL<+r7i7`AQ;si&%^*LT6$%3Dq+iTSF2Y)u}2Yiy|PQ9~va3z2I9B3I~QWseBV zzRmWxG>2|J1?yYZDt~+X(-v*Gm*3O5TLGG^{;}|{{1%8*?<1Z0mT0;r(VbTKG4q$( zt9XLE1(0Rey7BC@5Vddv~Zo`gs;p zBt+;feaQS&_+j0%Jwu`s{WG3_dUcc`k9fqzRrFKWGFGI~aBJ;{-Z!su3kicynt~)i zxta|6JQ-MD&Ny(&5f0-N2kZx8j1XsPN1kp|T&(8ZKnC6~RVj4IKu^W@bEHO;(RnwK zfcHxCKQ_6K5cm;+%X|qJzsv_PU7YJO5vSiG|)`jrjIb@Da}#Xr$UI=mz5q=tSI=tS%5$%Sm!(QdAy z({L&LQ0iyF0xaCPB^Sv3kZeY!IO13v9@C`nwcM^?;3`ls6sjNvgh64l8r802znDV; z4Da0fAK44-oDp?B60be|KMIp;4FxxcS-~I zHajwj@|MUA(4cnwkuT?cQKT(LP%YDKk)KIsx6-W4?cPIy?Ql2v?V56x!jM2;A%%11 zH`j7^l8vQscc}gqe7SONu4{#;nj;!cRd{)qegK8AVkI%NBk2?hWH1!|+sb+QzpNZg zRXHS{;+A)#k|U?;242jg@PogPF#}stsdO8bF@MX@K)B~1OT)_)0I=uX?b;bfqxH5b z9{yc+7+|_d-G;hb;J>&^0;g(o#=1Xo$FDTX3qCW+l7A$ucA{c6KD*7Iz&yxb^(ZX2 zSLbuN6

)ElK$IijZ;lnJ+O~R~6RNR7JJ~*v)7D5X9P{forQ;fmvVPAe?VNPoFc` zyf5#?5P%$Xw#J6J2)&^_NagxceWg!mU`fTe_1!S$#diC8cLlc)Qkt9X56By9p4_<8 zV_t7L5t!X0JLk{NK;XV=M-H`VXN24NamyQL>6*?k(+_>)qZW>EYZ(mcTLgQ%^>n>t zxMqRFmXaT`O0LqX-6aWM%?f;ikLEt9v%VqxyC1S5VfwSN@28h|rcqgWJ=D%AzpU!y zkl0V?RFoJTJ$poOt~@cSzm0{CHF(F7$~DigRkE9>a?2;Sjrrz?bAQMod$3Y_t&PZ0 zFsy$OYO22Gj10@XZsF!P2b$*Onc5s2|1wAZ2Okt%PE9d4hJoQP? z0B<-`7S}S5Sp7?N&1B58&w`&DB5aXKQE!thGFGFttFG$(Bg*y~Iub&M2EShM<>#rv z8opEg$1sjX&#eu19$uN%Zhy6Zr9RNIc|FGf@-zt`MW-6TS;|9P1ka3sERhk}SwD3C zMbQwUj3amJm4Zg}jNtK1EplKNLi}JOqPN=F{M%KpuorDA(B}czigyeQeh zda88B+6505WY9t)<7zGw%C3FPCw1^G4an2Y#aIv>N*Z(<=@6ied3A$D@6a5g1Qu8V z^1p8Eom&LHn=0xh+u2KbKEZ->waRDdQt5O;Y~wD;^VBk=>KkDL>jmpIy0uifN$x+% z0Yon2*~Lhm5k=s|!7KtX8&Lc?ucHHj=&;D2$*#(--!KIxbTAljh8vSTu9PDkH{KtB zs2)rVFA!CORydKBA#b}~ewPtS&eEd)Fl_yoxb@E|?a%B@7xwn5n!jyrUi&n^4_p}c z;{89`yAp7y*S2pB*^l+umn0=yCuyvcq_G|eQA{NyyGD~?##WR<2w9t~l`WxREK@0x zn28QaC`&=b)Gz-ezyBr?%(}9)uh); z@)&|+b1zrVBwvKlMqchz3!jmlf%XQSGc2<2cL?ln$_^4e$V^4jy zYz$}qYN2(IgLEB)h-|Em)2t~)9!-W{bVQ`P@z!&D0fsiY;SeNYK|!v2kI=`ZujXjr z(w&=NvVEM95rW-X`QQe7C;Vez%xz&W%)|&_%jwrWFt#5oi!uC~K>#vyh z&}V9Rk^=owL(jW}_1djUZ{-X)jOCOr9b9#OtZfd9@2Yt*e0&FQ++q=YtUJBWZ`Br* z#M=c?kFrKXJkVn?pFbtl%C#biqmQe+J~j=^Sc>zlp2I?7{yvC;o3t%&o$3n-@m#~t zdWL@OesTIM`-I{1rsF~u3^9)$tow}j`JBGOseM7hV$xjaZH3+p*);p_1r z_0~}}YG9W$EW0nWwf)WMgcU3$Z+$=WtQ`W~=@m`Tr4q<`)jRjbwP`)$exuYs_+6@oO(ke-o>^{HIK5tg3K zEcw?r5b1ilQex93>$iGr3UO4^>W)TUeRH;UB^o)Dp4!-4x}k{i39)jrQV`% zC`aK^S6-?~QPj6Jl!RYD|1{<20@h=#pG&s+n*G9#RWN^gG+t|4t`=+7O<{%f-G}`+ zH?{qss`m4>0qRO}`?|o|KzlU9--skd;|cF%x(O717J6&8(?*yfA1sENWWvYdMV`OD z&Ju`V9x3Plp8D$-uT}~nsgANRTsRs09{Y3@FUEAsy0kLa2Em+pMlE7IQtd|;x1skb zeEy>0f=ovT1-=)YWQc0#!}Opld;1s9F1wORNFGd0{K!uAtn*Kawa1UX1TrQfUEDcK z_RbHf0`xd^EvTY~6wsha_fc)H!4C$o((Y-+i9Pjlz@_b{6(lsYs+r6<jLz-5TWvVF1yDO2$NIrR`(Lo)h-1MRp{^-nUQedNa8FtvZei!gSB;d$T zOg*R0mxxL!id2Z4KK!D_mXdcO|CAP(w5v`NAjP)~XF0CWdB^i-V156oKXWGCkLE*4q7+nG%(iv6G**vtR1ha4Q~$1$uKF!GKYnq4U1`v zl2Z|7wryA3aZyLyHN32`Fsv6{0Zh+~HsOCb7fv^+2=D~*W1e36}Nkl?e&J8el8+ZmefoE*A>|XiU+!CUyvF~P9 zdXsq#v14wx3ebkwp!-9p-Ap`w(9Ur6Ci@Ou9zDll_7Zv45fKjkos7PKc9MQ$dKO44 z7mqw{*rzW<`}&TcyHG-^XLjkh?F=| z0%e_`ES?T`F{R|i#2E6)44iti34*q#Q(!`9ZVZ?aQK4Tcl^Q4RLwa&yQsx8I(?j}_J!mC>58u27tgwn{9GsCwM(~K#V z(fQ;qFYKqqpEP%{=V@3G0y1KL(()E}lWU?`8EBl_@iQi=F}_Yx=9DB3@eKvt_k^KS z=iMkF>Jvf|rp-o7mEJ42rG)IXRIN8c5kCM5!BeSOH%7VAH z{ER3B5(jbXbM)Zr@VT`hl-6l$m!#B4-;si*7~zk17PJlo@{XKKc%c{Nm{Vx-1`Xp6 z38cSb>;f9qhARqETWn!3Xu5B%L!jv!k+_f4p|9rN4maRrgw>gjS1c-IzI+Z!qO*;# z3i-MR)8jKAEV^_a$V?!@*@TXs76!tsjl|J9=O= zy`GtJQH!7MuD#b+PjcfNB|=9w<<(^hAVJ*9v2ivGnQZNGv@od7i4Ugd!-FAT*3nDs zG488`rE@eW8{X{7^42~8GSnKBEq)p+H!eN09ErQ61|rY1{3(ik{dP9NOAKB*!&eMs zn;_*%(S2;L>8r7=K}NxOU4Z6^%ql~$YFFYze3^%rK{E*1I+va@owcWn=lE>4fbe&< z*46}6=ldCC@fJ+6w+mseJwWLa&hY0P5R zx1#6LXuzvCD!|500NG&znyV!9(;t@NXyYKTX1|c1Eyczn*^%P0+MH+JdG<>yk5Zny zC~CpNOozO~2)7T=5d#tMR{H>M{jiy(2l6!W6nSAL?_tTiwKOM={8nwtBa@e3hawva zsILX2PCr}y;eHfy3d@&fJ0EG;c`W6H0s`^y9B2knG*k#g$_+e8I>)C%c5eo!(TtpndPRr zV?3T4vU>dWqqu2edWC}lvuCc`2AO{*IBlEy5whweZ9mEnWrk#b!2tzuU)yxb{=E~V z+qijp1jdQ@3TDlT0oNmVDzp%T3_>9**KBCd2{G`EKOxAovf7||P4kE!Is%`31ail% z^t`&TRl-4^^&$cbT=^C#x2};CeyfP*2eDkSr*rw59w{Anjc8FOky$EiLkxG1fv_W zQ3Q>`a6gnI?0Tw?4N+FCI|W$)PP}5c!JQm)AMZSUK<$aPeJ+(N8O26<+sgq(7tYeE zxL{H&>3Tt;`AMpPG&2gd#0&<5hyHmdNw@6L&SIt!^B=k+b$8RL`auLsgI`|e@T(`4 zUlA}eWdfrw)yEB|rfNw9-L6y^(VnjY?3zaxMubKkktv}DAX^XzNG>zq+nm1;hj0GC zNTrY1p;@v|hDo3%JmzhRRSkIrwz$M~UUVz?0wi*uP;@U9?Dl3$aW`PCm@$drR>&y6{uumw#@6wGu0K2S~;q)`v6kg!Le<4Lnk8M?!yYp?Uxd{@X8D z4t6~YZdJ~k4z>TFgmh~vX6A(IQP7BVmRRt}iPMxo_9;EL$LT@VxL(Sb+0c&Py|bDBUHU|sMv7b6rf<~{#YMbyDARK9@e!hLzNGO>%)201(xD?2iJhDi6T*NJBCE{aBuTYWFFYrj z9J%DkQ65I0Q?M8q)E6e)e_oG0V5OFJC1@r=Ji#-hV_{K23i$(IcN+U^ZD_#L>@B^= zv?cAVOWyjvvr!c5+ojcMajM6{)p8@PSc|?)}B5VwM zq=jX(roq_gn63n6vvP6S_y#!FA+J5= za9Lfwt6f(AF=Lo|Nypf*>syh0G1ZLDfsfY8JVKJ&3V4CJtn1|LS5EMV`3l=%&03QWAy8=swWP8>Wg~pd{@(vZkTK% zra4Dks(TkZIEuq)o`j`)3?}6xOr}; z+u`8d3O%-R4OP}v-NKIHi#ya=Uu=7vu5xGJ)Mwvt{T*yP+d?=J4(80}9%Vz0H_ogg zD&o@3S9_RieACAI3;^Bau@x5&lzK`LilmUh1*(26IbolcDRt{@CN$rgMgv2`js1f{ z%WM8|R_*@03HJ!Yua#(7SN8%28T=^2c3me>!vP%xHR3w@6M(l}ixNQ#&0REd<>l-$ zexI=@ne4UjfwEJF$k@P?3U5GdVpjS4$q}p}Mi9GF?lyv*XgM=Awp{U}@`{u1^#ud_ zNUQvk8>7xPk3wVZERZ|gdsna#PWI8RKHrqr0<@N9$+g-3Wy0N)naz_$G4@*Obz~o% z;YLx^?%F#@smlF_e@sj5Z9bgRzwaVqan1Ccj$QOC9rteG^DS9Z*R&)XN3k!XkA+ zua4e1Wnw@baXeXMs&@4JfLQZ~6SevG`A-Q|SfU2Y>k3Qk35}3E*~qdJwu$7sygj!AX6mxP;@ce9J>wA-_Z;7-AFgOU`l^%o zuuE^zROR})D#O$WvB{;pk$iIwa}sUM{H`E`d7cI0O{761%ue=Vz{1#BB|s^YGIJsknW|nias760>i~)iqlRuDF4O_llHA~e zig?-UFxppX-1uFq-at33L&JlH-H|yeDV}2|asHYwTg@o0W2)CPmT?P4xk+KI zy6q~7E|F?Cj-jm~y%cl^z9AAj5RVPn6O58+(S#v-tX&BM7ZWS*hh8>+l5&z8Cav`D zvjk?eM(&!`Wf`&K~~;GwLyF~|kd9aX!xjH*Vs zgYKfB+cD%Yq#HDvHb5gYu%MxS2GvCacl{G`ZTnOQ){V%+H+dOtjnXGH1lVlEeNW44 z$YBc$X^F?a1p2&ClRL$Uz8s1aB6;v|!g|2_LK>EE8yXXAlh(X!*a9RA@Jtx0=f6BLu zPF9k^q=Z=T;#crn{(9Ty*S3HaS{2jQu5XdH9sTCcg;wpH7~I>7f6^wmHxK3JFF4|K#+AXNU} zczg@}v(Z`pa*BVD-+ylgWMd%HlRs;1@)y~%u>C&+|8)%*u-uE1fHfQKRJ1PH8k-oRdKQZP1TQh*;`$yusrx7cqoxsX0esxHv4O(!0kW_d;e}Cp8s22CmLeOLfA|#0{{qiX4jZu?O!X}D+)tWj SANRLGEFny+4woG|9r+)69{2VD literal 0 HcmV?d00001 diff --git a/docs/assets/images/commfeed/commfeed-03-updated.png b/docs/assets/images/commfeed/commfeed-03-updated.png new file mode 100644 index 0000000000000000000000000000000000000000..172ae62da80c803b5ae2d0a8cc8e2489c626e977 GIT binary patch literal 52367 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 literal 0 HcmV?d00001 diff --git a/docs/assets/images/commfeed/commfeed-04-updated.png b/docs/assets/images/commfeed/commfeed-04-updated.png new file mode 100644 index 0000000000000000000000000000000000000000..6eb5daeed399fc838fc90ee2b09b78d3a0113298 GIT binary patch literal 44452 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%0D-hl2bfxiB$8CPzjdS#wYcKXW$_xM(85&+(V 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 AJOBUy literal 0 HcmV?d00001 diff --git a/docs/assets/images/commfeed/commfeed-07-updated.png b/docs/assets/images/commfeed/commfeed-07-updated.png new file mode 100644 index 0000000000000000000000000000000000000000..63aaae0b1620a6313739a7a672b61b427411a072 GIT binary patch literal 37736 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 Ghx8!E=Ovv0eGTi6Xc6QY9 z65A7YT(0mFx*=O!yKmcd87*=7jG?AW4rs@rRMfn0w$Lrg zdHTP(ocGNTT=f5WSI!LLL!JPiwF%k8Pgc!c_2Dyn#I$28CLL0jec1!Pw=ZeC|NKOu z`t!UsZyQtMPu!^rjc}*ooRVn6)a3H=#>s|4I}|`YerX4 z;vHho*K*X!R>NHLMpO_D~gcsOfDeN z^ixzTEnL>*=uYRpzlIArK0rk99=w~E9;*W6sscc1y17BT=f4d6wt~>hPe_RV$E9XP=6S!yTL_e)$7L?9?3F}JX)WyeDoxNt8y@nW@YB!rm zuPUw4qqIp`eOg>3Y)%XENm1wAex2Z+9-vaq;-^pJATjaGh)2)W>js%pvUgKeH`Fiw zu(YRII)H8Ucx-|Yi8>dO^I&)K5W5TA_fB&fcMetNk+Mjx&~Mu{B=|pQ1DkWTabBB zqZIKDfdmk2z0_r5;c?v5s71rjw=%a-ME2+O{)wz5dCQk6Hrz!9my($>cT?a;5)qe4 zl<08QtV`o=K8$-(MpFh#qa3%-2vx>G*VeWju-6vn#0jFQZPcfz$6Jvf|87p`*h`Pf;1ARYqR6Gol?+(oEc@ z3eVT{!zIxT9WKuJ2gowT0DGhP4^`#OIpg7Yx#UI@C4B0U2oHS@IlM37M&~^@YG6i_ zQ{dX3iQXOT)c+MZZ!zFeH?N0Oe_1zlrz++*yA1oqHFIP~d08tILwnas?$_c0_bJ-> zs>9wI25Ijwc5zn`t+DtqYdP*uM!$b8OS56-D-TKH)_to)Lzb4=c0#R9+2bNU;0%~ zxFUd<#JoON6GImgRCtLHhXiI1d~&O#YR7EvpTAoZW>nk&Y(X!ChE2< zshBqT!jjI1Ajk=C9lPdan+*IpN)a@R`#rcYw{pBq5`Cuo=&_><`If7<2|5s%{RuJL zo@X#%=R!{MC$zIE3CT2s-!A>TCs6vobspzG=)V6{;SvT2(1B+I^{<|Pjsyh(kgO6%lv}bnA?_|LRFk06hJpN_o-${s{;+fii-A1|cu}yCpLvZRxrYdoGHazrNC^RVk1mntvh%(0s4=;RL&-4FPCE-q#EP~JBdL3 z1U{a;kn~$x@o#LfShdV39uhJwWW>S+3OVp>r8SHcxK%=#pj)?O;BuGI-sK;wLJ>w2 z@SF=?@R9zz6<85MFN2{oI#p$ntJSMJ5=jF9tZ4(+X%!$dgdqkW1mowdeD;?~&ag58 zar|9)4Bx8=x{L-J^p^cJU`-WCb5S6Hf7af+tYz*XQEF^)ZRfj%UK~gJBS)&Z$U+V# zpF%yw%Rtu%N=2o6H^^7neQO?+;JqC8vRsjSS0s@zrhmLK``8odk?)aU?=4Lmuq)Xw?&9Jz-~G+w|k(O z^4h-!5}txbKxJQ|lCiTOJi;s?AB!%x$j)Y-els%fXmM>_ij#Z$gL1CEKMxkq4(zp4 z;7(8X(ty)u6pi8tKMj?D&%=`6esU~=CCTuTzibQQ0wx?jynM3A?X%}CtFok05$SKVMkj_Mx6xSVkdTA{*zIr3Hs`4w|q`+F` zF{{rr4T?y*^_whBU-wZg-y~0dO`Bh-iw`@;>O9YLy^GevJI`uziJ~>I%It;=_T4>4 zpJA<=2SbWl4@POaDm^(>X+S)I5O++!8;{?;-JP$ihd0r?zpvjg7u33pHc+J@!B6bO zw17_r7A+wsM~82r_I9E-ro10MM$(=Cz=sa4)|KGi8Q`skO6xxG;Yw!2P7$q8;ASWK zCAj5Joi@i22ZbZUt`$ix8#2P^?sG%**cS7k6G)$m3D?u4qo#V6wx`QzkzkXMlX|z7 zgAe-ymc_(UGCIAZ@z$Sengi)o%|U`Tt0jkaPM}*Lm9d0T;DlCY^^g#4#RY#egRKlA zP;utV>ok;B-g?Is89sjL9#r4rFosAH8+aD?x=9Ji9-? z-qNsc`DHVP^JSaQInDHm=B&_U7^GkaE5oL8cEQ(~XJ%^ZfnDk8>gTe~DWM4fk=k{b literal 0 HcmV?d00001 From e935679cf73d6cc720aaa27d72fdfdf0e3ead482 Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Fri, 31 Jan 2025 20:26:24 +0100 Subject: [PATCH 085/145] fix: ts --- src/components/Search/SearchAutocompleteInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index ae18442745de..2fea54f5d0ce 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -138,7 +138,7 @@ function SearchAutocompleteInput( isLoading={!!isSearchingForReports} ref={ref} onKeyPress={handleKeyPress(onSubmit)} - isMarkdownEnabled + type="markdown" multiline={false} parser={(input: string) => { 'worklet'; From 30e4bde8256c420c870fab8113292800c47d7a81 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Sat, 1 Feb 2025 08:22:07 +0700 Subject: [PATCH 086/145] fix: moving expense to selfDM message not correct --- src/libs/ModifiedExpenseMessage.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 1d5946dc25b8..a593b2f097c7 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,6 +1,7 @@ import isEmpty from 'lodash/isEmpty'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {isSelfDM} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTagLists, Report, ReportAction} from '@src/types/onyx'; @@ -139,11 +140,20 @@ function getForDistanceRequest(newMerchant: string, oldMerchant: string, newAmou function getForExpenseMovedFromSelfDM(destinationReportID: string) { const destinationReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${destinationReportID}`]; const rootParentReport = getRootParentReport(destinationReport); - - // The "Move report" flow only supports moving expenses to a policy expense chat or a 1:1 DM. + // The "Move report" flow only supports moving expenses from selfDM to: + // 1. A policy expense chat + // 2. A 1:1 DM + // However, in the olddot, expenses could be moved back to a self-DM. + // To maintain consistency and handle this case, we provide a fallback message. + if (isSelfDM(rootParentReport)) { + return translateLocal('iou.changedTheExpense'); + } const reportName = isPolicyExpenseChat(rootParentReport) ? getPolicyExpenseChatName(rootParentReport) : buildReportNameFromParticipantNames({report: rootParentReport}); const policyName = getPolicyName(rootParentReport, true); - + // If we can't determine either the report name or policy name, return the default message + if (isEmpty(policyName) && !reportName) { + return translateLocal('iou.changedTheExpense'); + } return translateLocal('iou.movedFromSelfDM', { reportName, workspaceName: !isEmpty(policyName) ? policyName : undefined, From 5f1d3ff8b40fbe563dbe41740d47170277fb2f74 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Sat, 1 Feb 2025 10:28:41 +0700 Subject: [PATCH 087/145] test: implement test for move expense to selfDM --- tests/unit/ModifiedExpenseMessageTest.ts | 45 ++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index fbf9b121069b..6a3daa5a10f6 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -396,5 +396,50 @@ describe('ModifiedExpenseMessage', () => { expect(result).toEqual(expectedResult); }); }); + + describe('when move report', () => { + it('return the message "changed the expense" when moving an expense from an expense chat or 1:1 DM to selfDM', () => { + // Given the selfDM report and report action + const report = { + ...createRandomReport(1), + chatType: CONST.REPORT.CHAT_TYPE.SELF_DM, + }; + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + movedToReportID: 1, + }, + }; + + const expectedResult = 'changed the expense'; + // When the expense move from an expense chat or 1:1 DM to selfDM + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); + // Then it should return the 'changed the expense' message + expect(result).toEqual(expectedResult); + }); + + it('return the message "changed the expense" when reportName and workspace name are empty', () => { + // Given the report with empty name and report action + const report = { + ...createRandomReport(1), + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + reportName: '', + }; + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + movedToReportID: 1, + }, + }; + + const expectedResult = 'changed the expense'; + // When the expense move from expense chat with reportName empty + const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); + // Then it should return the 'changed the expense' message + expect(result).toEqual(expectedResult); + }); + }); }); }); From 54cd42446808a0c4ad5e6b7dbe4dfae96badb252 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Sat, 1 Feb 2025 11:56:28 +0700 Subject: [PATCH 088/145] fix: eslint fail --- src/libs/ModifiedExpenseMessage.ts | 3 +-- tests/unit/ModifiedExpenseMessageTest.ts | 29 ++++++++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index a593b2f097c7..f88c03eab9ac 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -1,7 +1,6 @@ import isEmpty from 'lodash/isEmpty'; import Onyx from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {isSelfDM} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyTagLists, Report, ReportAction} from '@src/types/onyx'; @@ -13,7 +12,7 @@ import Log from './Log'; import {getCleanedTagName, getSortedTagKeys} from './PolicyUtils'; import {getOriginalMessage, isModifiedExpenseAction} from './ReportActionsUtils'; // eslint-disable-next-line import/no-cycle -import {buildReportNameFromParticipantNames, getPolicyExpenseChatName, getPolicyName, getRootParentReport, isPolicyExpenseChat} from './ReportUtils'; +import {buildReportNameFromParticipantNames, getPolicyExpenseChatName, getPolicyName, getRootParentReport, isPolicyExpenseChat, isSelfDM} from './ReportUtils'; import {getTagArrayFromName} from './TransactionUtils'; let allPolicyTags: OnyxCollection = {}; diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 6a3daa5a10f6..d310c3a9b226 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -1,7 +1,10 @@ +import Onyx from 'react-native-onyx'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import createRandomReportAction from '../utils/collections/reportActions'; import createRandomReport from '../utils/collections/reports'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; describe('ModifiedExpenseMessage', () => { describe('getForAction', () => { @@ -398,45 +401,51 @@ describe('ModifiedExpenseMessage', () => { }); describe('when move report', () => { - it('return the message "changed the expense" when moving an expense from an expense chat or 1:1 DM to selfDM', () => { + beforeEach(() => Onyx.clear()); + it('return the message "changed the expense" when moving an expense from an expense chat or 1:1 DM to selfDM', async () => { // Given the selfDM report and report action - const report = { - ...createRandomReport(1), + const selfDMReport = { + ...report, chatType: CONST.REPORT.CHAT_TYPE.SELF_DM, }; const reportAction = { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, originalMessage: { - movedToReportID: 1, + movedToReportID: selfDMReport.reportID, }, }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${selfDMReport.reportID}`]: selfDMReport}); + await waitForBatchedUpdates(); const expectedResult = 'changed the expense'; // When the expense move from an expense chat or 1:1 DM to selfDM - const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); + const result = ModifiedExpenseMessage.getForReportAction(selfDMReport.reportID, reportAction); // Then it should return the 'changed the expense' message expect(result).toEqual(expectedResult); }); - it('return the message "changed the expense" when reportName and workspace name are empty', () => { + it('return the message "changed the expense" when reportName and workspace name are empty', async () => { // Given the report with empty name and report action - const report = { - ...createRandomReport(1), + const emptyReportNameReport = { + ...report, chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, reportName: '', + isOwnPolicyExpenseChat: false, }; const reportAction = { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, originalMessage: { - movedToReportID: 1, + movedToReportID: emptyReportNameReport.reportID, }, }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${emptyReportNameReport.reportID}`]: emptyReportNameReport}); + await waitForBatchedUpdates(); const expectedResult = 'changed the expense'; // When the expense move from expense chat with reportName empty - const result = ModifiedExpenseMessage.getForReportAction(report.reportID, reportAction); + const result = ModifiedExpenseMessage.getForReportAction(emptyReportNameReport.reportID, reportAction); // Then it should return the 'changed the expense' message expect(result).toEqual(expectedResult); }); From cba621d4dbe1970c6e119873fd394b55532e30c2 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Sun, 2 Feb 2025 14:33:42 +0700 Subject: [PATCH 089/145] fix: correct grammar in comment --- tests/unit/ModifiedExpenseMessageTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index d310c3a9b226..642fbe5ead21 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -400,7 +400,7 @@ describe('ModifiedExpenseMessage', () => { }); }); - describe('when move report', () => { + describe('when moving an expense', () => { beforeEach(() => Onyx.clear()); it('return the message "changed the expense" when moving an expense from an expense chat or 1:1 DM to selfDM', async () => { // Given the selfDM report and report action @@ -419,7 +419,7 @@ describe('ModifiedExpenseMessage', () => { await waitForBatchedUpdates(); const expectedResult = 'changed the expense'; - // When the expense move from an expense chat or 1:1 DM to selfDM + // When the expense is moved from an expense chat or 1:1 DM to selfDM const result = ModifiedExpenseMessage.getForReportAction(selfDMReport.reportID, reportAction); // Then it should return the 'changed the expense' message expect(result).toEqual(expectedResult); @@ -444,7 +444,7 @@ describe('ModifiedExpenseMessage', () => { await waitForBatchedUpdates(); const expectedResult = 'changed the expense'; - // When the expense move from expense chat with reportName empty + // When the expense is moved from an expense chat with reportName empty const result = ModifiedExpenseMessage.getForReportAction(emptyReportNameReport.reportID, reportAction); // Then it should return the 'changed the expense' message expect(result).toEqual(expectedResult); From 7699f8960f2018633307d588ab549b7b04369db9 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Sun, 2 Feb 2025 15:37:47 +0700 Subject: [PATCH 090/145] test: implement test when the expense is moved from an expense chat --- tests/unit/ModifiedExpenseMessageTest.ts | 74 +++++++++++++++++++++--- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 642fbe5ead21..58bc6858740a 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import CONST from '@src/CONST'; +import {translate} from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; import createRandomReportAction from '../utils/collections/reportActions'; import createRandomReport from '../utils/collections/reports'; @@ -418,7 +419,9 @@ describe('ModifiedExpenseMessage', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${selfDMReport.reportID}`]: selfDMReport}); await waitForBatchedUpdates(); - const expectedResult = 'changed the expense'; + // Given the correct text message + const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.changedTheExpense'); + // When the expense is moved from an expense chat or 1:1 DM to selfDM const result = ModifiedExpenseMessage.getForReportAction(selfDMReport.reportID, reportAction); // Then it should return the 'changed the expense' message @@ -426,8 +429,8 @@ describe('ModifiedExpenseMessage', () => { }); it('return the message "changed the expense" when reportName and workspace name are empty', async () => { - // Given the report with empty name and report action - const emptyReportNameReport = { + // Given the policyExpenseChat with reportName is empty and report action + const policyExpenseChat = { ...report, chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, reportName: '', @@ -437,18 +440,73 @@ describe('ModifiedExpenseMessage', () => { ...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, originalMessage: { - movedToReportID: emptyReportNameReport.reportID, + movedToReportID: policyExpenseChat.reportID, }, }; - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${emptyReportNameReport.reportID}`]: emptyReportNameReport}); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`]: policyExpenseChat}); await waitForBatchedUpdates(); - const expectedResult = 'changed the expense'; - // When the expense is moved from an expense chat with reportName empty - const result = ModifiedExpenseMessage.getForReportAction(emptyReportNameReport.reportID, reportAction); + // Given the correct text message + const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.changedTheExpense'); + + // When the expense is moved to an expense chat with reportName empty + const result = ModifiedExpenseMessage.getForReportAction(policyExpenseChat.reportID, reportAction); // Then it should return the 'changed the expense' message expect(result).toEqual(expectedResult); }); + + it('return the message "moved expense from self DM to ${policyName}" when both reportName and policyName are present', async () => { + // Given the policyExpenseChat with both reportName and policyName are present and report action + const policyExpenseChat = { + ...report, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + isOwnPolicyExpenseChat: false, + policyName: 'fake policyName', + }; + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + movedToReportID: policyExpenseChat.reportID, + }, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`]: policyExpenseChat}); + await waitForBatchedUpdates(); + + // Given the correct text message + const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.movedFromSelfDM', {reportName: policyExpenseChat.reportName, workspaceName: policyExpenseChat.policyName}); + + // When the expense is moved to an expense chat with both reportName and policyName are present + const result = ModifiedExpenseMessage.getForReportAction(policyExpenseChat.reportID, reportAction); + // Then it should return the correct text message + expect(result).toEqual(expectedResult); + }); + + it('return the message "moved expense from self DM to chat with ${reportName}" when only reportName is present', async () => { + // Given the policyExpenseChat with only reportName is present and report action + const policyExpenseChat = { + ...report, + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + isOwnPolicyExpenseChat: false, + }; + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE, + originalMessage: { + movedToReportID: policyExpenseChat.reportID, + }, + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`]: policyExpenseChat}); + await waitForBatchedUpdates(); + + // Given the correct text message + const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.movedFromSelfDM', {reportName: policyExpenseChat.reportName}); + + // When the expense is moved to an expense chat with only reportName is present + const result = ModifiedExpenseMessage.getForReportAction(policyExpenseChat.reportID, reportAction); + // Then it should return the correct text message + expect(result).toEqual(expectedResult); + }); }); }); }); From 637541355fb86431b3ea32138f73c6b60bd8181a Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Sun, 2 Feb 2025 15:44:09 +0700 Subject: [PATCH 091/145] fix: eslint fail --- tests/unit/ModifiedExpenseMessageTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 58bc6858740a..f7ce67486ec1 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -455,7 +455,7 @@ describe('ModifiedExpenseMessage', () => { expect(result).toEqual(expectedResult); }); - it('return the message "moved expense from self DM to ${policyName}" when both reportName and policyName are present', async () => { + it('return the message "moved expense from self DM to policyName" when both reportName and policyName are present', async () => { // Given the policyExpenseChat with both reportName and policyName are present and report action const policyExpenseChat = { ...report, @@ -482,7 +482,7 @@ describe('ModifiedExpenseMessage', () => { expect(result).toEqual(expectedResult); }); - it('return the message "moved expense from self DM to chat with ${reportName}" when only reportName is present', async () => { + it('return the message "moved expense from self DM to chat with reportName" when only reportName is present', async () => { // Given the policyExpenseChat with only reportName is present and report action const policyExpenseChat = { ...report, From d05f309adcc36b2cc343927a8ce27ac513097e57 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Mon, 3 Feb 2025 04:32:12 +0700 Subject: [PATCH 092/145] resolve request chagne --- tests/unit/ModifiedExpenseMessageTest.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index f7ce67486ec1..1b3de26795b0 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -419,7 +419,6 @@ describe('ModifiedExpenseMessage', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${selfDMReport.reportID}`]: selfDMReport}); await waitForBatchedUpdates(); - // Given the correct text message const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.changedTheExpense'); // When the expense is moved from an expense chat or 1:1 DM to selfDM @@ -446,7 +445,6 @@ describe('ModifiedExpenseMessage', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`]: policyExpenseChat}); await waitForBatchedUpdates(); - // Given the correct text message const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.changedTheExpense'); // When the expense is moved to an expense chat with reportName empty @@ -473,7 +471,6 @@ describe('ModifiedExpenseMessage', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`]: policyExpenseChat}); await waitForBatchedUpdates(); - // Given the correct text message const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.movedFromSelfDM', {reportName: policyExpenseChat.reportName, workspaceName: policyExpenseChat.policyName}); // When the expense is moved to an expense chat with both reportName and policyName are present @@ -499,7 +496,6 @@ describe('ModifiedExpenseMessage', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat.reportID}`]: policyExpenseChat}); await waitForBatchedUpdates(); - // Given the correct text message const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.movedFromSelfDM', {reportName: policyExpenseChat.reportName}); // When the expense is moved to an expense chat with only reportName is present From 5bfc60871e734497fb6b39377ad284541d6b4030 Mon Sep 17 00:00:00 2001 From: Github Date: Mon, 3 Feb 2025 10:13:29 +0100 Subject: [PATCH 093/145] fix unit tests for CalendarPicker --- tests/unit/CalendarPickerTest.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/unit/CalendarPickerTest.tsx b/tests/unit/CalendarPickerTest.tsx index 5e6dbaf26ce0..f7bee25e366f 100644 --- a/tests/unit/CalendarPickerTest.tsx +++ b/tests/unit/CalendarPickerTest.tsx @@ -1,5 +1,5 @@ import type ReactNavigationNative from '@react-navigation/native'; -import {fireEvent, render, screen, within} from '@testing-library/react-native'; +import {fireEvent, render, screen, userEvent, within} from '@testing-library/react-native'; import {addMonths, addYears, subMonths, subYears} from 'date-fns'; import type {ComponentType} from 'react'; import CalendarPicker from '@components/DatePicker/CalendarPicker'; @@ -121,7 +121,7 @@ describe('CalendarPicker', () => { expect(onSelectedMock).toHaveBeenCalledWith('2022-02-15'); }); - test('should block the back arrow when there is no available dates in the previous month', () => { + test('should block the back arrow when there is no available dates in the previous month', async () => { const minDate = new Date('2003-02-01'); const value = new Date('2003-02-17'); @@ -134,16 +134,17 @@ describe('CalendarPicker', () => { ); // When the previous month arrow is pressed - fireEvent.press(screen.getByTestId('prev-month-arrow')); + const user = userEvent.setup(); + await user.press(screen.getByTestId('prev-month-arrow')); // Then the previous month should not be called as the previous month button is disabled - const prevMonth = subMonths(new Date(), 1).getMonth(); + const prevMonth = subMonths(value, 1).getMonth(); expect(screen.queryByText(monthNames.at(prevMonth) ?? '')).not.toBeOnTheScreen(); }); - test('should block the next arrow when there is no available dates in the next month', () => { + test('should block the next arrow when there is no available dates in the next month', async () => { const maxDate = new Date('2003-02-24'); - const value = '2003-02-17'; + const value = new Date('2003-02-17'); render( { ); // When the next month arrow is pressed - fireEvent.press(screen.getByTestId('next-month-arrow')); + const user = userEvent.setup(); + await user.press(screen.getByTestId('next-month-arrow')); // Then the next month should not be called as the next month button is disabled - const nextMonth = addMonths(new Date(), 1).getMonth(); + const nextMonth = addMonths(value, 1).getMonth(); expect(screen.queryByText(monthNames.at(nextMonth) ?? '')).not.toBeOnTheScreen(); }); From 147f52a6dbe4c9d94878526955bff7d6b7bfeb8c Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 3 Feb 2025 11:30:16 +0100 Subject: [PATCH 094/145] Update adhoc flow to use mobile-expensify PR link --- .github/PULL_REQUEST_TEMPLATE.md | 4 ++-- .github/workflows/testBuildHybrid.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2dfd9348d961..b8de634a489f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,9 +23,9 @@ PROPOSAL: diff --git a/.github/workflows/testBuildHybrid.yml b/.github/workflows/testBuildHybrid.yml index d958e0958083..6a8a0d5884bf 100644 --- a/.github/workflows/testBuildHybrid.yml +++ b/.github/workflows/testBuildHybrid.yml @@ -81,7 +81,7 @@ jobs: }); const body = pullRequest.data.body; - const regex = /MOBILE-EXPENSIFY:(?\d+)/; + const regex = /MOBILE-EXPENSIFY:\s*https:\/\/github.com\/Expensify\/Mobile-Expensify\/pull\/(?\d+)/; const found = body.match(regex)?.groups?.prNumber || ""; return found.trim(); From e0b024859972a6ecaa0bb0dec74e4dfe53547035 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Mon, 3 Feb 2025 20:18:47 +0700 Subject: [PATCH 095/145] fix: update the text to moved to self DM --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/ModifiedExpenseMessage.ts | 2 +- tests/unit/ModifiedExpenseMessageTest.ts | 6 +++--- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f2e4428bb6ae..359eee0a491c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -996,6 +996,7 @@ const translations = { threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`, movedFromSelfDM: ({workspaceName, reportName}: MovedFromSelfDMParams) => `moved expense from self DM to ${workspaceName ?? `chat with ${reportName}`}`, + movedToSelfDM: 'moved expense to self DM', tagSelection: 'Select a tag to better organize your spend.', categorySelection: 'Select a category to better organize your spend.', error: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 9e44e060487d..88cbe93db485 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -994,6 +994,7 @@ const translations = { threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Seguimiento ${formattedAmount} ${comment ? `para ${comment}` : ''}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, movedFromSelfDM: ({workspaceName, reportName}: MovedFromSelfDMParams) => `movió el gasto desde su propio mensaje directo a ${workspaceName ?? `un chat con ${reportName}`}`, + movedToSelfDM: 'movió el gasto a su propio mensaje', tagSelection: 'Selecciona una etiqueta para organizar mejor tus gastos.', categorySelection: 'Selecciona una categoría para organizar mejor tus gastos.', error: { diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index f88c03eab9ac..c8ee0f3b22f0 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -145,7 +145,7 @@ function getForExpenseMovedFromSelfDM(destinationReportID: string) { // However, in the olddot, expenses could be moved back to a self-DM. // To maintain consistency and handle this case, we provide a fallback message. if (isSelfDM(rootParentReport)) { - return translateLocal('iou.changedTheExpense'); + return translateLocal('iou.movedToSelfDM'); } const reportName = isPolicyExpenseChat(rootParentReport) ? getPolicyExpenseChatName(rootParentReport) : buildReportNameFromParticipantNames({report: rootParentReport}); const policyName = getPolicyName(rootParentReport, true); diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts index 1b3de26795b0..b9e1869c72c1 100644 --- a/tests/unit/ModifiedExpenseMessageTest.ts +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -403,7 +403,7 @@ describe('ModifiedExpenseMessage', () => { describe('when moving an expense', () => { beforeEach(() => Onyx.clear()); - it('return the message "changed the expense" when moving an expense from an expense chat or 1:1 DM to selfDM', async () => { + it('return the message "moved expense to self DM" when moving an expense from an expense chat or 1:1 DM to selfDM', async () => { // Given the selfDM report and report action const selfDMReport = { ...report, @@ -419,11 +419,11 @@ describe('ModifiedExpenseMessage', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}`, {[`${ONYXKEYS.COLLECTION.REPORT}${selfDMReport.reportID}`]: selfDMReport}); await waitForBatchedUpdates(); - const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.changedTheExpense'); + const expectedResult = translate(CONST.LOCALES.EN as 'en', 'iou.movedToSelfDM'); // When the expense is moved from an expense chat or 1:1 DM to selfDM const result = ModifiedExpenseMessage.getForReportAction(selfDMReport.reportID, reportAction); - // Then it should return the 'changed the expense' message + // Then it should return the 'moved expense to self DM' message expect(result).toEqual(expectedResult); }); From 32bf0c8686909284f0645d616447fcf0c5130fe9 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Mon, 3 Feb 2025 18:53:43 +0530 Subject: [PATCH 096/145] minor update. Signed-off-by: krishna2323 --- src/libs/ReportActionsUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 2e4c376b8ad5..b959479d8e63 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1616,7 +1616,7 @@ function wasActionTakenByCurrentUser(reportAction: OnyxInputOrEntry { if (!reportID || !transactionID) { - return; + return undefined; } const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const reportActions = getAllReportActions(report?.reportID); From cb33aa2c678bdd2dad504092492732df72a1ed76 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Mon, 3 Feb 2025 20:48:58 +0700 Subject: [PATCH 097/145] fix: change comment to be more suitable for the scenarios --- src/libs/ModifiedExpenseMessage.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index c8ee0f3b22f0..9acf9f964403 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -139,14 +139,13 @@ function getForDistanceRequest(newMerchant: string, oldMerchant: string, newAmou function getForExpenseMovedFromSelfDM(destinationReportID: string) { const destinationReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${destinationReportID}`]; const rootParentReport = getRootParentReport(destinationReport); - // The "Move report" flow only supports moving expenses from selfDM to: - // 1. A policy expense chat - // 2. A 1:1 DM - // However, in the olddot, expenses could be moved back to a self-DM. - // To maintain consistency and handle this case, we provide a fallback message. + // In OldDot, expenses could be moved to a self-DM. Return the corresponding message for this case. if (isSelfDM(rootParentReport)) { return translateLocal('iou.movedToSelfDM'); } + // In NewDot, the "Move report" flow only supports moving expenses from self-DM to: + // - A policy expense chat + // - A 1:1 DM const reportName = isPolicyExpenseChat(rootParentReport) ? getPolicyExpenseChatName(rootParentReport) : buildReportNameFromParticipantNames({report: rootParentReport}); const policyName = getPolicyName(rootParentReport, true); // If we can't determine either the report name or policy name, return the default message From 53e148af69f6acf392aa1362e02d9ff7bf135834 Mon Sep 17 00:00:00 2001 From: Linh Vo Date: Mon, 3 Feb 2025 21:06:45 +0700 Subject: [PATCH 098/145] fix: incorrect message --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 88cbe93db485..4e82ce7e16d8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -994,7 +994,7 @@ const translations = { threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Seguimiento ${formattedAmount} ${comment ? `para ${comment}` : ''}`, threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`, movedFromSelfDM: ({workspaceName, reportName}: MovedFromSelfDMParams) => `movió el gasto desde su propio mensaje directo a ${workspaceName ?? `un chat con ${reportName}`}`, - movedToSelfDM: 'movió el gasto a su propio mensaje', + movedToSelfDM: 'movió el gasto a su propio mensaje directo', tagSelection: 'Selecciona una etiqueta para organizar mejor tus gastos.', categorySelection: 'Selecciona una categoría para organizar mejor tus gastos.', error: { From 18af258fd217466c4c5b330fc2ac29be89cb301e Mon Sep 17 00:00:00 2001 From: April Bekkala Date: Mon, 3 Feb 2025 11:26:18 -0600 Subject: [PATCH 099/145] Update Create-an-expense.md adding an info bubble for SmartScan detecting only text written in the latin alphabet --- .../new-expensify/expenses-&-payments/Create-an-expense.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md index 2157e05aa377..9b5720579e23 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md @@ -60,6 +60,8 @@ When an expense is submitted to a workspace, your approver will receive an email You can also forward receipts to receipts@expensify.com using your primary or secondary email address. SmartScan will automatically extract all the details from the receipt and add them to your expenses. {% include end-info.html %} +{% include info.html %} SmartScan can only detect and process text written in the Latin alphabet. {% include end-info.html %} + ## Manually add an expense {% include selector.html values="desktop, mobile" %} From 596f476258cf78e2e7c527ba1cef18aa99fbe368 Mon Sep 17 00:00:00 2001 From: April Bekkala Date: Mon, 3 Feb 2025 11:37:54 -0600 Subject: [PATCH 100/145] Update Create-an-expense.md --- .../new-expensify/expenses-&-payments/Create-an-expense.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md index 9b5720579e23..e87b2ebf703c 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md @@ -56,12 +56,12 @@ When an expense is submitted to a workspace, your approver will receive an email ![Click Scan]({{site.url}}/assets/images/ExpensifyHelp-CreateExpenseUpdate-3.png){:width="100%"} ![Enter workspace or individual's name]({{site.url}}/assets/images/ExpensifyHelp-CreateExpenseUpdate-4.png){:width="100%"} +{% include info.html %}SmartScan can only detect and process text written in the Latin alphabet.{% include end-info.html %} + {% include info.html %} You can also forward receipts to receipts@expensify.com using your primary or secondary email address. SmartScan will automatically extract all the details from the receipt and add them to your expenses. {% include end-info.html %} -{% include info.html %} SmartScan can only detect and process text written in the Latin alphabet. {% include end-info.html %} - ## Manually add an expense {% include selector.html values="desktop, mobile" %} From 1f08e3bdcda8260928f7d578ae75954562ce88ae Mon Sep 17 00:00:00 2001 From: April Bekkala Date: Mon, 3 Feb 2025 11:47:27 -0600 Subject: [PATCH 101/145] Update Create-an-expense.md --- .../new-expensify/expenses-&-payments/Create-an-expense.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md index e87b2ebf703c..23c1bc58e5fc 100644 --- a/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md +++ b/docs/articles/new-expensify/expenses-&-payments/Create-an-expense.md @@ -56,7 +56,9 @@ When an expense is submitted to a workspace, your approver will receive an email ![Click Scan]({{site.url}}/assets/images/ExpensifyHelp-CreateExpenseUpdate-3.png){:width="100%"} ![Enter workspace or individual's name]({{site.url}}/assets/images/ExpensifyHelp-CreateExpenseUpdate-4.png){:width="100%"} -{% include info.html %}SmartScan can only detect and process text written in the Latin alphabet.{% include end-info.html %} +{% include info.html %} +SmartScan can only detect and process text written in the Latin alphabet. +{% include end-info.html %} {% include info.html %} You can also forward receipts to receipts@expensify.com using your primary or secondary email address. SmartScan will automatically extract all the details from the receipt and add them to your expenses. From 94e80472e3e753c948f5350c6434e6f6760cb515 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:32:17 -0800 Subject: [PATCH 102/145] Delete docs/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards.md https://github.com/Expensify/Expensify/issues/460782 --- .../Assign-Company-Cards.md | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 docs/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards.md diff --git a/docs/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards.md b/docs/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards.md deleted file mode 100644 index 54bd12ce5c49..000000000000 --- a/docs/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Assign Company Cards -description: How to assign company cards to employees in Expensify once they have been connected or imported ---- - -After connecting or importing your company cards to Expensify, you can assign each card to its respective cardholder. - -# Assign new cards - -If you're assigning cards via CSV upload for the first time, - -1. Hover over **Settings** and click **Domains**. -2. Select the desired domain. -3. Click the card dropdown menu and select the desired feed from the list. -![Click the dropdown located right below the Imported Cards title near the top of the page. Then select a card from the list.](https://help.expensify.com/assets/images/csv-03.png){:width="100%"} - -{:start="4"} -4. Click **Assign New Cards**. - -![Under the Company Cards tab on the left, you'll use the dropdown menu to select a card and beneath that, you'll click Assign New Cards]({{site.url}}/assets/images/CompanyCards_Assign.png){:width="100%"} - -{:start="5"} -5. Enter the employee's email address and/or select it from the dropdown list. *Note: Employees must have an email address under this domain in order to assign a card to them.* -![Below the Assign a Card header, enter or select the employee's email address]({{site.url}}/assets/images/CompanyCards_EmailAssign.png){:width="100%"} - -{:start="6"} -6. Enter the last four digits of the card number and/or select it from the dropdown list. - - If no transactions have been posted on the card, the card number will not appear in the list and you'll need to enter the full card number into the field. Then press ENTER on your keyboard. The field may clear itself after you press ENTER, but you can disregard this and continue to the next step. -7. (Optional) Set the transaction start date. Any transactions that were posted before this date will not be imported into Expensify. If you do not make a selection, it will default to the earliest available transactions from the card. *Note: Expensify can only import data for the time period released by the bank. Most banks only provide a certain amount of historical data, averaging 30-90 days into the past. It's not possible to override the start date the bank has provided via this tool.* -8. Click **Assign**. - -Once assigned, you will see each cardholder associated with their card and the start date listed. The transactions will now be imported to the cardholder's account, where they can add receipts, code the expenses, and submit them for review and approval. - -![Expensify domain assigned cards](https://help.expensify.com/assets/images/ExpensifyHelp_AssignedCard.png){:width="100%"} - -# Upload new expenses for existing assigned cards - -To add new expenses to an existing uploaded and assigned card, - -1. Hover over **Settings** and click **Domains**. -2. Select the desired domain name. -3. Click **Manage/Import CSV**. -![Click Manage/Import CSV located in the top right between the Issue Virtual Card button and the Import Card button.](https://help.expensify.com/assets/images/csv-02.png){:width="100%"} - -{:start="4"} -4. Select the saved layout from the drop-down list. -5. Click **Upload CSV**. -6. Click **Update All Cards** to retrieve the new expenses for the assigned cards. - -# Unassign company cards - -{% include info.html %} -Unassigning a company card will delete any unsubmitted (Open or Unreported) expenses in the cardholder's account. -{% include end-info.html %} - -To unassign a specific card, click the Actions button to the right of the card and click **Unassign**. - -![Click the Actions button to the right of the card and select Unassign.]({{site.url}}/assets/images/CompanyCards_Unassign.png){:width="100%"} - -To completely remove the card connection, unassign every card from the list and then refresh the page. - -*Note: If expenses are Processing and then rejected, they will also be deleted when they're returned to an Open state, as the card they're linked to no longer exists.* - -{% include faq-begin.md %} - -**My Commercial Card Feed is set up. Why is a specific card not coming up when I try to assign it to an employee?** - -Cards will appear in the dropdown when they are activated and have at least one posted transaction. If the card is activated and has been used for a while and you're still not seeing it, reach out to your Account Manager or message concierge@expensify.com for further assistance. - -{% include faq-end.md %} From 157cc80c4294e7eea7b452c3dc9dc70760b21d43 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:33:48 -0800 Subject: [PATCH 103/145] Delete docs/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings.md https://github.com/Expensify/Expensify/issues/460782 --- .../Configure-Company-Card-Settings.md | 122 ------------------ 1 file changed, 122 deletions(-) delete mode 100644 docs/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings.md diff --git a/docs/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings.md b/docs/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings.md deleted file mode 100644 index 75580b94f1ad..000000000000 --- a/docs/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings.md +++ /dev/null @@ -1,122 +0,0 @@ ---- -title: Configure Company Card Settings -description: How to customize your company card settings ---- - -Once you’ve imported your company cards via [commercial card feed](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Commercial-Card-Feeds), [direct bank feed](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections), or [CSV import](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import), the next step is to configure the card settings. - -{% include info.html %} -You must be a Domain Admin to complete this process. -{% include end-info.html %} - -# Configure company card settings - -1. Hover over **Settings** and click **Domains**. -2. Select the desired domain. -3. Click the **Settings** tab located at the top of the Company Cards tab. -![Near the top right, click the Settings tab that is located between the Card List and Reconciliation tabs.](https://help.expensify.com/assets/images/compcard-01.png){:width="100%"} -5. Set the following preferences, then click **Save**. - -## Preferred Workspace - -Setting a preferred Workspace for a company card feed ensures that the imported transactions are added to a report for that Workspace. This is useful when members are on multiple Workspaces and need to ensure their company card expenses are reported to a particular Workspace. - -## Reimbursable preference - -You can control how your employees' company card expenses are flagged for reimbursement: - -- **Force Yes**: All expenses will be marked as reimbursable. Employees cannot change this setting. -- **Force No**: All expenses will be marked as non-reimbursable. Employees cannot change this setting. -- **Do Not Force**: Expenses will default to either reimbursable or non-reimbursable (your choice), but employees can adjust if necessary. - -## Liability type - -Choose the liability type that suits your needs: - -- **Corporate Liability**: Users cannot delete company card expenses. -- **Personal Liability**: Users are allowed to delete company card expenses. - -If you update the settings on an existing company card feed, the changes will apply to expenses imported after the date that the setting is saved. The update will not affect previously imported expenses. - -# Use Scheduled Submit with company cards - -With Scheduled Submit, employees no longer have to create their expenses, add them to a report, and submit them manually. All they need to do is SmartScan their receipts and Concierge will take care of the rest using a variety of schedules that you can set according to your preferences. - -{% include info.html %} -Concierge won't automatically submit expenses on reports that have expense violations. These expenses will be moved to a new report for the current reporting period. -{% include end-info.html %} - -To enable Scheduled Submit, - -1. Hover over **Settings** and click **Workspaces**. -2. Select the desired Workspace. -3. Click the **Reports** tab on the left. -4. Enable the Scheduled Submit toggle. -5. Select the report submission frequency. -6. Select the date that reports will be submitted. - -# Connect company cards to an accounting integration - -If you're using a connected accounting system such as NetSuite, Xero, Intacct, Quickbooks Desktop, or QuickBooks Online, you can also connect the card to export to a specific credit card GL account. First, connect the card itself, and once completed, follow the steps below: - -1. Hover over **Settings** and click **Domains** -2. Select the desired domain. -3. Click **Edit Exports** near the top right and select the general ledger (GL) account you want to export expenses to. -![Find the desired card in the table. In that same row, click Edit Exports.](https://help.expensify.com/assets/images/cardfeeds-02.png){:width="100%"} - -Once the account is set, exported expenses will be mapped to the selected account when exported by a Domain Admin. - -# Export company card expenses to a connected accounting integration - -## Pooled GL account - -To export credit card expenses to a pooled GL account, - -1. Hover over **Settings** and click **Workspaces**. -2. Select the desired Workspace. -3. Click the **Connections** tab on the left. -4. Under Accounting Integrations, click **Configure** next to the desired accounting integration. -5. For Non-reimbursable export, select **Credit Card / Charge Card / Bank Transaction**. -6. Review the Export Settings page for exporting Expense Reports to NetSuite. -7. Select the Vendor/liability account you want to export all non-reimbursable expenses to. - -## Individual GL account - -1. Hover over **Settings** and click **Domains**. -2. Select the desired Domain. -3. Click the **Edit Exports** to the right of the desired card. Then select the general ledger (GL) account you want to export expenses to. -![Find the desired card in the table. In that same row, click Edit Exports.](https://help.expensify.com/assets/images/cardfeeds-02.png){:width="100%"} - -Once the account is set, exported expenses will be mapped to the selected account. - -# Identify company card transactions - -When you link your credit cards to Expensify, the transactions will appear in each user's account on the Expenses page as soon as they're posted. Transactions from centrally managed cards have a locked card icon next to them to indicate that they’re company card expenses. - -# Import historical transactions - -Once a card is connected via direct connection or via Approved! banks, Expensify will import 30-90 days of historical transactions to your account (based on your bank's discretion). Any historical expenses beyond that date range can be imported using the [CSV import](https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/CSV-Import). - -# Use eReceipts - -Expensify eReceipts are digital substitutes for paper receipts, eliminating the need to keep physical receipts or use SmartScan for receipts. For Expensify Card transactions, eReceipts are automatically generated for all amounts in these categories: Airlines, Commuter expenses, Gas, Groceries, Mail, Meals, Car rental, Taxis, and Utilities. For other card programs, eReceipts are generated for USD purchases of $75 or less. - -{% include info.html %} -To ensure seamless automatic importation, it is key that you maintain your transactions in US Dollars. eReceipts can also be directly imported from your bank account. CSV/OFX imported files of bank transactions do not support eReceipts. eReceipts are not generated for lodging expenses. Due to incomplete or inaccurate category information from certain banks, there may be instances of invalid eReceipts being generated for hotel purchases. If you choose to re-categorize expenses, a similar situation may arise. It's crucial to remember that our Expensify eReceipt Guarantee excludes coverage for hotel and motel expenses. -{% include end-info.html %} - -{% include faq-begin.md %} - -**What plan/subscription is required in order to manage corporate cards?** - -A Group Workspace is required. - -**When do my company card transactions import to Expensify?** - -Credit card transactions are imported to Expensify once they’re posted to the bank account. This usually takes 1-3 business days between the point of purchase and when the transactions populate in your account. - -**Scheduled Submit is disabled. Why are reports still being submitted automatically?** - -If Scheduled Submit is disabled at the Group Workspace level or set to a manual frequency but expense reports are still being automatically submitted, Scheduled Submit is most likely enabled on the user’s Individual Workspace settings. - -{% include faq-end.md %} From b2adf91f076ecfd28df76138ee925e42effe96a2 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:34:36 -0800 Subject: [PATCH 104/145] Update redirects.csv https://github.com/Expensify/Expensify/issues/460782 --- docs/redirects.csv | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index 9ccef010ec96..ab7abe782458 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -624,3 +624,5 @@ https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the https://help.expensify.com/articles/expensify-classic/settings/Change-or-add-email-address,https://help.expensify.com/articles/expensify-classic/settings/Managing-Primary-and-Secondary-Logins-in-Expensify https://help.expensify.com/articles/expensify-classic/domains/SAML-SSO,https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards +https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards +https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards From b125307567978063e5f7b8372092d68cfedc8769 Mon Sep 17 00:00:00 2001 From: staszekscp Date: Mon, 3 Feb 2025 19:35:09 +0100 Subject: [PATCH 105/145] Update README.md and add building grunt to --- README.md | 1 + scripts/run-build.sh | 3 +++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 3b55f54bead2..de5c746a964d 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ * [Running The Tests](#running-the-tests) * [Debugging](#debugging) * [App Structure and Conventions](#app-structure-and-conventions) +* [HybridApp](#HybridApp) * [Philosophy](#Philosophy) * [Security](#Security) * [Internationalization](#Internationalization) diff --git a/scripts/run-build.sh b/scripts/run-build.sh index 70e0dcf7c586..0abbd4530adf 100755 --- a/scripts/run-build.sh +++ b/scripts/run-build.sh @@ -38,6 +38,9 @@ NEW_DOT_FLAG="${STANDALONE_NEW_DOT:-false}" SCHEME="Expensify Dev" APP_ID="org.me.mobiexpensifyg.dev" + # Build Yapl JS + cd Mobile-Expensify && npm run grunt:build:shared && cd .. + echo -e "\n${GREEN}Starting a HybridApp build!${NC}" PROJECT_ROOT_PATH="Mobile-Expensify/" export CUSTOM_APK_NAME="Expensify-debug.apk" From c2ce2b0cf0dbb17fa5ccf72665e4533db3727d1c Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:42:43 -0800 Subject: [PATCH 106/145] Delete docs/articles/new-expensify/settings/Add-personal-information.md https://github.com/Expensify/Expensify/issues/463571 --- .../settings/Add-personal-information.md | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Add-personal-information.md diff --git a/docs/articles/new-expensify/settings/Add-personal-information.md b/docs/articles/new-expensify/settings/Add-personal-information.md deleted file mode 100644 index 492d349357ec..000000000000 --- a/docs/articles/new-expensify/settings/Add-personal-information.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -title: Add personal information -description: Add your legal name, DOB, and/or address for travel and payments ---- -

From ba486c660323c6fd377b692730d5d4e45654650b Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:43:56 -0800 Subject: [PATCH 107/145] Delete docs/articles/new-expensify/settings/Add-profile-photo.md https://github.com/Expensify/Expensify/issues/463571 --- .../settings/Add-profile-photo.md | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Add-profile-photo.md diff --git a/docs/articles/new-expensify/settings/Add-profile-photo.md b/docs/articles/new-expensify/settings/Add-profile-photo.md deleted file mode 100644 index 60e56deaafbc..000000000000 --- a/docs/articles/new-expensify/settings/Add-profile-photo.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Add profile photo -description: Add an image to your profile ---- -
- -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click the Edit pencil icon next to your profile image or icon and select **Upload Image** to choose a new image from your saved files. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap the Edit pencil icon next to your profile image or icon and select **Upload Image** to choose a new image from your saved files. -{% include end-option.html %} - -{% include end-selector.html %} - -
From c9517f43698d79ec428406835c04eb6f6006b463 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:44:34 -0800 Subject: [PATCH 108/145] Delete docs/articles/new-expensify/settings/Update-your-pronouns.md https://github.com/Expensify/Expensify/issues/463571 --- .../settings/Update-your-pronouns.md | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Update-your-pronouns.md diff --git a/docs/articles/new-expensify/settings/Update-your-pronouns.md b/docs/articles/new-expensify/settings/Update-your-pronouns.md deleted file mode 100644 index bf0e902092ff..000000000000 --- a/docs/articles/new-expensify/settings/Update-your-pronouns.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Update your pronouns -description: Display your pronouns on your account ---- -
- -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Profile** in the left menu. -3. Click **Pronouns** to select your pronouns. Type any letter into the field to see a list of available options. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap **Profile** in the left menu. -3. Tap **Pronouns** to select your pronouns. Type any letter into the field to see a list of available options. -{% include end-option.html %} - -{% include end-selector.html %} - -
From 3e6d91f35a92c561ecb0474ceb749bdbcfa65482 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:45:11 -0800 Subject: [PATCH 109/145] Delete docs/articles/new-expensify/settings/Update-your-profile-status.md https://github.com/Expensify/Expensify/issues/463571 --- .../settings/Update-your-profile-status.md | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Update-your-profile-status.md diff --git a/docs/articles/new-expensify/settings/Update-your-profile-status.md b/docs/articles/new-expensify/settings/Update-your-profile-status.md deleted file mode 100644 index 5e5130f69cd5..000000000000 --- a/docs/articles/new-expensify/settings/Update-your-profile-status.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Update your profile status -description: Share your status with your team ---- -
- -You can update your status in Expensify to let your coworkers know if you are out of the office, in a meeting, or even list your work hours or a different message. This message will appear when someone clicks on your profile or in a chat conversation. - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Profile** in the left menu. -3. Click **Status**. -4. (Optional) Click the emoji icon to add an emoji. -5. Click the message field and enter a status. For example, out of office, in a meeting, at lunch, etc. -6. Click **Clear After** to select an expiration for the status. For example, if you select 30 minutes, the status will be automatically cleared after 30 minutes. -7. Click **Save**. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap **Profile** in the left menu. -3. Tap **Status**. -4. (Optional) Tap the emoji icon to add an emoji. -5. Tap the message field and enter a status. For example, out of office, in a meeting, at lunch, Office Hours: M-F 8-5 PT, etc. -6. Tap **Clear After** to select an expiration for the status. For example, if you select 30 minutes, the status will be automatically cleared after 30 minutes. -7. Tap **Save**. -{% include end-option.html %} - -{% include end-selector.html %} - -
- From 77683e33ca625b1082438b70bf2487aabb7b0dfa Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:45:27 -0800 Subject: [PATCH 110/145] Delete docs/articles/new-expensify/settings/Update-your-name.md https://github.com/Expensify/Expensify/issues/463571 --- .../settings/Update-your-name.md | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Update-your-name.md diff --git a/docs/articles/new-expensify/settings/Update-your-name.md b/docs/articles/new-expensify/settings/Update-your-name.md deleted file mode 100644 index d6b65def12ac..000000000000 --- a/docs/articles/new-expensify/settings/Update-your-name.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Update your name -description: Update your display or legal name ---- -
- -Your Expensify account includes two names: -- Your display name that everyone can see (which can include a nickname) -- Your legal name that only you can see (for booking travel and for payment purposes) - -To update your display or legal name, - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Profile** in the left menu. -3. Edit your name. - - **Display name**: Click **Display Name** and enter your first name (or nickname) and last name into the fields and click **Save**. This name will be visible to anyone in your company workspace. - - **Legal name**: Scroll down to the Private Details section and click **Legal name**. Then enter your legal first and last name and click **Save**. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap Profile in the left menu. -3. Edit your name. - - **Display name**: Tap **Display Name** and enter your first name (or nickname) and last name into the fields and tap **Save**. This name will be visible to anyone in your company workspace. - - **Legal name**: Scroll down to the Private Details section and tap **Legal name**. Then enter your legal first and last name and tap **Save**. -{% include end-option.html %} - -{% include end-selector.html %} - -
From b939db632042be9fa5114c0186ebd765f01a0e89 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:45:44 -0800 Subject: [PATCH 111/145] Delete docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md https://github.com/Expensify/Expensify/issues/463571 --- .../settings/Switch-to-light-or-dark-mode.md | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md diff --git a/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md b/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md deleted file mode 100644 index 34f96f9f5f7d..000000000000 --- a/docs/articles/new-expensify/settings/Switch-to-light-or-dark-mode.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Switch to light or dark mode -description: Change the appearance of Expensify ---- -
- -Expensify has three theme options that determine how the app looks: -- **Dark mode**: The app appears with a dark background -- **Light mode**: The app appears with a light background -- **Use Device settings**: Expensify will automatically use your device’s default theme - -To change your Expensify theme, - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Preferences** in the left menu. -3. Click the **Theme** option and select the desired theme. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon in the bottom menu. -2. Tap **Preferences**. -3. Tap the **Theme** option and select the desired theme. -{% include end-option.html %} - -{% include end-selector.html %} - -
From 8ce8c2eedf924f00a9a0e4339f2bb879b6da3d11 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:46:10 -0800 Subject: [PATCH 112/145] Delete docs/articles/new-expensify/settings/Set-timezone.md https://github.com/Expensify/Expensify/issues/463571 --- .../new-expensify/settings/Set-timezone.md | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Set-timezone.md diff --git a/docs/articles/new-expensify/settings/Set-timezone.md b/docs/articles/new-expensify/settings/Set-timezone.md deleted file mode 100644 index 11ce1340c7bb..000000000000 --- a/docs/articles/new-expensify/settings/Set-timezone.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Set timezone -description: Set your timezone ---- -
- -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Profile** in the left menu. -3. Click **Timezone** to select your timezone. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon at the bottom of the screen. -2. Tap **Profile** in the left menu -3. Tap **Timezone** to select your timezone. -{% include end-option.html %} - -{% include end-selector.html %} - -
From 6794526ce959ddb866b816b3a72297cfa2a47965 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:46:26 -0800 Subject: [PATCH 113/145] Delete docs/articles/new-expensify/settings/Preferences.md https://github.com/Expensify/Expensify/issues/463571 --- .../new-expensify/settings/Preferences.md | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Preferences.md diff --git a/docs/articles/new-expensify/settings/Preferences.md b/docs/articles/new-expensify/settings/Preferences.md deleted file mode 100644 index b94c9d35c1a1..000000000000 --- a/docs/articles/new-expensify/settings/Preferences.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -title: Preferences -description: How to manage your Expensify Preferences ---- -# Overview -Your Preferences in Expensify allow you to customize how you use New Expensify. - -- Set your theme preference - -# How to set your theme preference in New Expensify - -To set or update your theme preference in New Expensify: -1. Go to **Settings > Preferences** -2. Tap on **Theme** -3. You can choose between the _Dark_ theme, the _Light_ theme, or _Use Device Settings_ - -_Use Device Settings_ is the default setting. - -Selecting _Use Device Settings_ will use your device's theme settings. For example, if your device is set to adjust the appearance from light to dark during the day, we'll match that. - -Your theme preference will sync across all your New Expensify apps (mobile, web, or OSX desktop apps). From d0abfb0aa7f8f473a33ab08c8fc5274925426ed0 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:46:44 -0800 Subject: [PATCH 114/145] Delete docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md https://github.com/Expensify/Expensify/issues/463571 --- .../Switch-account-language-to-Spanish.md | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md diff --git a/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md b/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md deleted file mode 100644 index a431d34fbc0f..000000000000 --- a/docs/articles/new-expensify/settings/Switch-account-language-to-Spanish.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Switch account language to Spanish -description: Change your account language ---- -
- -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Preferences** in the left menu. -3. Click the Language option and select **Spanish**. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon in the bottom menu. -2. Tap **Preferences**. -3. Tap the Language option and select **Spanish**. -{% include end-option.html %} - -{% include end-selector.html %} - -
From d0f035214e4ad7e0c1a858e8c2f6a030062d5ef9 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:47:11 -0800 Subject: [PATCH 115/145] Delete docs/articles/new-expensify/settings/Update-Notification-Preferences.md https://github.com/Expensify/Expensify/issues/463571 --- .../Update-Notification-Preferences.md | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 docs/articles/new-expensify/settings/Update-Notification-Preferences.md diff --git a/docs/articles/new-expensify/settings/Update-Notification-Preferences.md b/docs/articles/new-expensify/settings/Update-Notification-Preferences.md deleted file mode 100644 index e4111b3d02d3..000000000000 --- a/docs/articles/new-expensify/settings/Update-Notification-Preferences.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Update notification preferences -description: Determine how you want to receive Expensify notifications ---- -
- -To customize the email and in-app notifications you receive from Expensify, - -{% include selector.html values="desktop, mobile" %} - -{% include option.html value="desktop" %} -1. Click your profile image or icon in the bottom left menu. -2. Click **Preferences** in the left menu. -3. Enable or disable the toggles under Notifications: - - **Receive relevant feature updates and Expensify news**: If enabled, you will receive emails and in-app notifications from Expensify about new product and company updates. - - **Mute all sounds from Expensify**: If enabled, all in-app notification sounds will be silenced. -{% include end-option.html %} - -{% include option.html value="mobile" %} -1. Tap your profile image or icon in the bottom menu. -2. Tap **Preferences**. -3. Enable or disable the toggles under Notifications: - - **Receive relevant feature updates and Expensify news**: If enabled, you will receive emails and in-app notifications from Expensify about new product and company updates. - - **Mute all sounds from Expensify**: If enabled, all in-app notification sounds will be silenced. -{% include end-option.html %} - -{% include end-selector.html %} - -
From 834044da1eb6b01aa5f3c04de45ebb59c4795292 Mon Sep 17 00:00:00 2001 From: maddylewis <38016013+maddylewis@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:51:55 -0800 Subject: [PATCH 116/145] Update redirects.csv https://github.com/Expensify/Expensify/issues/463571 --- docs/redirects.csv | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/redirects.csv b/docs/redirects.csv index 9ccef010ec96..7db0bf3dbb86 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -624,3 +624,13 @@ https://help.expensify.com/articles/expensify-classic/expensify-card/Request-the https://help.expensify.com/articles/expensify-classic/settings/Change-or-add-email-address,https://help.expensify.com/articles/expensify-classic/settings/Managing-Primary-and-Secondary-Logins-in-Expensify https://help.expensify.com/articles/expensify-classic/domains/SAML-SSO,https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify https://help.expensify.com/articles/expensify-classic/connect-credit-cards/company-cards/Direct-Bank-Connections,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Connect-Company-Cards +https://help.expensify.com/articles/new-expensify/settings/Add-profile-photo,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Set-timezone,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Switch-account-language-to-Spanish,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Update-Notification-Preferences,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Update-your-profile-status,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Update-your-pronouns,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Preferences,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Switch-to-light-or-dark-mode,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Add-personal-information,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences +https://help.expensify.com/articles/new-expensify/settings/Update-your-name,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences From 50ff162c1e74b865ee778473e643f1227a66037e Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 3 Feb 2025 11:00:18 -0800 Subject: [PATCH 117/145] add test --- tests/unit/OptionsListUtilsTest.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/unit/OptionsListUtilsTest.ts b/tests/unit/OptionsListUtilsTest.ts index 19ebf261bd7a..bff34a5bad2c 100644 --- a/tests/unit/OptionsListUtilsTest.ts +++ b/tests/unit/OptionsListUtilsTest.ts @@ -149,6 +149,22 @@ describe('OptionsListUtils', () => { isOwnPolicyExpenseChat: true, type: CONST.REPORT.TYPE.CHAT, }, + + // Thread report with notification preference = hidden + '11': { + lastReadTime: '2021-01-14 11:25:39.200', + lastVisibleActionCreated: '2022-11-22 03:26:02.001', + reportID: '11', + isPinned: false, + participants: { + 10: {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN}, + }, + reportName: '', + oldPolicyName: "SHIELD's workspace", + chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT, + isOwnPolicyExpenseChat: true, + type: CONST.REPORT.TYPE.CHAT, + }, }; const activePolicyID = 'DEF456'; @@ -474,8 +490,8 @@ describe('OptionsListUtils', () => { // Filtering of personalDetails that have reports is done in filterOptions expect(results.personalDetails.length).toBe(9); - // Then all of the reports should be shown including the archived rooms. - expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length); + // Then all of the reports should be shown including the archived rooms, except for the report with notificationPreferences hidden. + expect(results.recentReports.length).toBe(Object.values(OPTIONS.reports).length - 1); }); it('orderOptions()', () => { From 253746eafb7618eca185405eb2b5b8815c131191 Mon Sep 17 00:00:00 2001 From: Georgia Monahan Date: Mon, 3 Feb 2025 11:08:02 -0800 Subject: [PATCH 118/145] Add share extension provisioning profile --- ...pp_AdHoc_Share_Extension.mobileprovision.gpg | Bin 0 -> 11124 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 ios/NewApp_AdHoc_Share_Extension.mobileprovision.gpg diff --git a/ios/NewApp_AdHoc_Share_Extension.mobileprovision.gpg b/ios/NewApp_AdHoc_Share_Extension.mobileprovision.gpg new file mode 100644 index 0000000000000000000000000000000000000000..c9b3eb213f797c03d25f69b8f97a700b5d0ed71f GIT binary patch literal 11124 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 GduK@g=(H^W literal 0 HcmV?d00001 From e7a0759478c5f7deb8b3fb57e97de5b1564520da Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 11:59:52 -0800 Subject: [PATCH 119/145] Add env for forked PRs --- .github/workflows/verifyHybridApp.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 2bfff47ccf80..f535eba19986 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -20,6 +20,9 @@ on: # TODO: Remove, just here for debugging - '**.yml' +env: + IS_PR_FROM_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} + concurrency: group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-verify-main cancel-in-progress: true @@ -28,6 +31,7 @@ jobs: verify_android: name: Verify Android HybridApp builds on main runs-on: ubuntu-latest-xl + if: env.IS_PR_FROM_FORK == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -64,6 +68,7 @@ jobs: verify_ios: name: Verify iOS HybridApp builds on main runs-on: macos-15-xlarge + if: env.IS_PR_FROM_FORK == 'true' steps: - name: Checkout uses: actions/checkout@v4 From 13bbac4ce094e9a9111778f52872f3e68fc6eb2c Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:07:21 -0800 Subject: [PATCH 120/145] Try no env to fix env --- .github/workflows/verifyHybridApp.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index f535eba19986..ca66ee2f84c0 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -31,7 +31,7 @@ jobs: verify_android: name: Verify Android HybridApp builds on main runs-on: ubuntu-latest-xl - if: env.IS_PR_FROM_FORK == 'true' + if: IS_PR_FROM_FORK == 'true' steps: - name: Checkout uses: actions/checkout@v4 @@ -68,7 +68,7 @@ jobs: verify_ios: name: Verify iOS HybridApp builds on main runs-on: macos-15-xlarge - if: env.IS_PR_FROM_FORK == 'true' + if: IS_PR_FROM_FORK == 'true' steps: - name: Checkout uses: actions/checkout@v4 From d7bef9d402fd11c62f11c73201d29c198b3eda55 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:08:56 -0800 Subject: [PATCH 121/145] Remove env and use if directly --- .github/workflows/verifyHybridApp.yml | 7 ++----- Mobile-Expensify | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index ca66ee2f84c0..35b58baba104 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -20,9 +20,6 @@ on: # TODO: Remove, just here for debugging - '**.yml' -env: - IS_PR_FROM_FORK: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} - concurrency: group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-verify-main cancel-in-progress: true @@ -31,7 +28,7 @@ jobs: verify_android: name: Verify Android HybridApp builds on main runs-on: ubuntu-latest-xl - if: IS_PR_FROM_FORK == 'true' + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} steps: - name: Checkout uses: actions/checkout@v4 @@ -68,7 +65,7 @@ jobs: verify_ios: name: Verify iOS HybridApp builds on main runs-on: macos-15-xlarge - if: IS_PR_FROM_FORK == 'true' + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} steps: - name: Checkout uses: actions/checkout@v4 diff --git a/Mobile-Expensify b/Mobile-Expensify index cda178a7a269..10beb38304d3 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit cda178a7a269e241e0f5104bb9aaaba2b1052908 +Subproject commit 10beb38304d35ee86b948f64afcdf49d51ab3d4a From 6ad6a6d3e8e9d941c5b6049b773797364566cf98 Mon Sep 17 00:00:00 2001 From: Vit Horacek <36083550+mountiny@users.noreply.github.com> Date: Mon, 3 Feb 2025 12:16:04 -0800 Subject: [PATCH 122/145] Revert "[No QA] Install desktop node-modules only when building desktop app" --- .github/actions/composite/setupNode/action.yml | 7 +------ .github/workflows/deploy.yml | 2 -- .github/workflows/testBuild.yml | 2 -- 3 files changed, 1 insertion(+), 10 deletions(-) diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml index 51fed0a6a26d..cfa3f9fc191e 100644 --- a/.github/actions/composite/setupNode/action.yml +++ b/.github/actions/composite/setupNode/action.yml @@ -6,10 +6,6 @@ inputs: description: "Indicates if node is set up for hybrid app" required: false default: 'false' - IS_DESKTOP_BUILD: - description: "Indicates if node is set up for desktop app" - required: false - default: 'false' outputs: cache-hit: @@ -45,7 +41,6 @@ runs: key: ${{ runner.os }}-node-modules-${{ hashFiles('Mobile-Expensify/package-lock.json', 'Mobile-Expensify/patches/**') }} - id: cache-desktop-node-modules - if: inputs.IS_DESKTOP_BUILD == 'true' uses: actions/cache@v4 with: path: desktop/node_modules @@ -65,7 +60,7 @@ runs: command: npm ci - name: Install node packages for desktop submodule - if: inputs.IS_DESKTOP_BUILD == 'true' && steps.cache-desktop-node-modules.outputs.cache-hit != 'true' + if: steps.cache-desktop-node-modules.outputs.cache-hit != 'true' uses: nick-fields/retry@3f757583fb1b1f940bc8ef4bf4734c8dc02a5847 with: timeout_minutes: 30 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e7ee3ac4d973..0c6ffd4306f7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -301,8 +301,6 @@ jobs: - name: Setup Node uses: ./.github/actions/composite/setupNode - with: - IS_DESKTOP_BUILD: true - name: Decrypt Developer ID Certificate run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml index 1bd4282b2830..869db3d04be7 100644 --- a/.github/workflows/testBuild.yml +++ b/.github/workflows/testBuild.yml @@ -247,8 +247,6 @@ jobs: - name: Setup Node uses: ./.github/actions/composite/setupNode - with: - IS_DESKTOP_BUILD: true - name: Decrypt Developer ID Certificate run: cd desktop && gpg --quiet --batch --yes --decrypt --passphrase="$DEVELOPER_ID_SECRET_PASSPHRASE" --output developer_id.p12 developer_id.p12.gpg From fed9f59447cfd4cf3e7273aa28f65c31170f62ef Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:16:32 -0800 Subject: [PATCH 123/145] Fix logic --- .github/workflows/verifyHybridApp.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 35b58baba104..d5d6935af38b 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -28,7 +28,8 @@ jobs: verify_android: name: Verify Android HybridApp builds on main runs-on: ubuntu-latest-xl - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} + # Only run on pull requests that are not on a fork + if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} steps: - name: Checkout uses: actions/checkout@v4 @@ -65,7 +66,8 @@ jobs: verify_ios: name: Verify iOS HybridApp builds on main runs-on: macos-15-xlarge - if: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork }} + # Only run on pull requests that are not on a fork + if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} steps: - name: Checkout uses: actions/checkout@v4 From 88b52852161d0831ffd52f133b407fd4c79b2979 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:26:46 -0800 Subject: [PATCH 124/145] Add HybridApp fork comment --- .github/workflows/verifyHybridApp.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index d5d6935af38b..7395fc450679 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -25,6 +25,18 @@ concurrency: cancel-in-progress: true jobs: + comment_on_fork: + name: Verify Android HybridApp builds on main + # Only run on pull requests that are not on a fork + if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} + runs-on: ubuntu-latest + steps: + - name: Comment on forks + run: | + gh pr comment ${{ github.event.number }} --body \ + ":warning: This PR is possibly changing native code, it may cause problems with HybridApp. Please run an AdHoc build to verify that HybridApp will not break. :warning:" + env: + GITHUB_TOKEN: ${{ github.token }} verify_android: name: Verify Android HybridApp builds on main runs-on: ubuntu-latest-xl From 7805eae7e03214922bfee73b1318937caccd4478 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Mon, 3 Feb 2025 12:30:54 -0800 Subject: [PATCH 125/145] feat: add the version property to the grafana timings call --- src/libs/API/parameters/SendPerformanceTimingParams.ts | 1 + src/libs/actions/Timing.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/libs/API/parameters/SendPerformanceTimingParams.ts b/src/libs/API/parameters/SendPerformanceTimingParams.ts index aebdaa40c8bb..cc245c7fc404 100644 --- a/src/libs/API/parameters/SendPerformanceTimingParams.ts +++ b/src/libs/API/parameters/SendPerformanceTimingParams.ts @@ -2,6 +2,7 @@ type SendPerformanceTimingParams = { name: string; value: number; platform: string; + version: string; }; export default SendPerformanceTimingParams; diff --git a/src/libs/actions/Timing.ts b/src/libs/actions/Timing.ts index edb751b33a4b..5c4fd61276fc 100644 --- a/src/libs/actions/Timing.ts +++ b/src/libs/actions/Timing.ts @@ -5,6 +5,7 @@ import * as Environment from '@libs/Environment/Environment'; import Firebase from '@libs/Firebase'; import getPlatform from '@libs/getPlatform'; import Log from '@libs/Log'; +import pkg from '../../../package.json'; type TimestampData = { startTime: number; @@ -67,6 +68,7 @@ function end(eventName: string, secondaryName = '', maxExecutionTime = 0) { name: grafanaEventName, value: eventTime, platform: `${getPlatform()}`, + version: `${pkg.version}`, }; API.read(READ_COMMANDS.SEND_PERFORMANCE_TIMING, parameters, {}); From 2a0726f9c8adeeddd16500e833be78cf53127140 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:33:51 -0800 Subject: [PATCH 126/145] Update url --- .github/workflows/verifyHybridApp.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 7395fc450679..cb5357d3ad0e 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -27,21 +27,21 @@ concurrency: jobs: comment_on_fork: name: Verify Android HybridApp builds on main - # Only run on pull requests that are not on a fork - if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} + # Only run on pull requests that *are* a fork +# if: ${{ github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest steps: - name: Comment on forks run: | - gh pr comment ${{ github.event.number }} --body \ + gh pr comment ${{github.event.pull_request.html_url }} --body \ ":warning: This PR is possibly changing native code, it may cause problems with HybridApp. Please run an AdHoc build to verify that HybridApp will not break. :warning:" env: GITHUB_TOKEN: ${{ github.token }} verify_android: name: Verify Android HybridApp builds on main runs-on: ubuntu-latest-xl - # Only run on pull requests that are not on a fork - if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} + # Only run on pull requests that are *not* on a fork + if: ${{ !github.event.pull_request.head.repo.fork }} steps: - name: Checkout uses: actions/checkout@v4 @@ -78,8 +78,8 @@ jobs: verify_ios: name: Verify iOS HybridApp builds on main runs-on: macos-15-xlarge - # Only run on pull requests that are not on a fork - if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} + # Only run on pull requests that are *not* on a fork + if: ${{ !github.event.pull_request.head.repo.fork }} steps: - name: Checkout uses: actions/checkout@v4 From b136219d5f2ec1182feab808abd7f719d352ab9e Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:35:34 -0800 Subject: [PATCH 127/145] Remove debug --- .github/workflows/verifyHybridApp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index cb5357d3ad0e..ae1f002659dd 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -28,7 +28,7 @@ jobs: comment_on_fork: name: Verify Android HybridApp builds on main # Only run on pull requests that *are* a fork -# if: ${{ github.event.pull_request.head.repo.fork }} + if: ${{ github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest steps: - name: Comment on forks From 08101e860353bb5bdabe5006c6d33bee1886692f Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:36:13 -0800 Subject: [PATCH 128/145] Remove more debug --- .github/workflows/verifyHybridApp.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index ae1f002659dd..ee9e9f5df881 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -17,8 +17,6 @@ on: - 'android/AndroidManifest.xml' - 'ios/Podfile.lock' - 'ios/project.pbxproj' - # TODO: Remove, just here for debugging - - '**.yml' concurrency: group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-verify-main From ae953f647ff7657eee6641981b071b2a8a4e1396 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:38:06 -0800 Subject: [PATCH 129/145] Update job name --- .github/workflows/verifyHybridApp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index ee9e9f5df881..7f2e4ac43bb9 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -24,7 +24,7 @@ concurrency: jobs: comment_on_fork: - name: Verify Android HybridApp builds on main + name: Comment on all PRs that are forks # Only run on pull requests that *are* a fork if: ${{ github.event.pull_request.head.repo.fork }} runs-on: ubuntu-latest From 835194a5b75b60c4c8b3fdee6b06d4c8317961e1 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:58:30 -0800 Subject: [PATCH 130/145] Fix android build --- .github/workflows/verifyHybridApp.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 7f2e4ac43bb9..ee68362265dd 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -65,7 +65,6 @@ jobs: IS_HYBRID_BUILD: 'true' - name: Build Android Debug - working-directory: Mobile-Expensify/Android run: | if ! npm run android-hybrid-build then From 93175a240de34cc57648ce6aaaaeeb90f9e4e31a Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 12:59:07 -0800 Subject: [PATCH 131/145] Add Debuggin --- .github/workflows/verifyHybridApp.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index ee68362265dd..90e1a0fe25ea 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -17,6 +17,8 @@ on: - 'android/AndroidManifest.xml' - 'ios/Podfile.lock' - 'ios/project.pbxproj' + # TODO: Remove this line, just for debugging + - '**.yml' concurrency: group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-verify-main From 4c0763de0c079dec8d45c6d9b400755f44876d73 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 13:02:43 -0800 Subject: [PATCH 132/145] Set up ruby on Android runners --- .github/workflows/verifyHybridApp.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index 90e1a0fe25ea..c40d62f713e4 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -66,6 +66,14 @@ jobs: with: IS_HYBRID_BUILD: 'true' + - name: Setup Ruby + uses: ruby/setup-ruby@v1.204.0 + with: + bundler-cache: true + + - name: Install New Expensify Gems + run: bundle install + - name: Build Android Debug run: | if ! npm run android-hybrid-build From 47aad8d77a3d5edd25dbd7eb54ba0d8c5be0ba8b Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 13:11:19 -0800 Subject: [PATCH 133/145] Tweak submodule update command --- .github/workflows/verifyHybridApp.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index c40d62f713e4..b3c9a9e1f041 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -52,9 +52,8 @@ jobs: - name: Update submodule to match main run: | - git submodule update --init --remote + git submodule update --init --remote --depth 1 cd Mobile-Expensify - git fetch git checkout main - name: Configure MapBox SDK @@ -97,9 +96,8 @@ jobs: - name: Update submodule to match main run: | - git submodule update --init --remote + git submodule update --init --remote --depth 1 cd Mobile-Expensify - git fetch git checkout main - name: Configure MapBox SDK From 0aee2cfd1d1cd2021028988d6b84ab30f8f5ca34 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 13:51:03 -0800 Subject: [PATCH 134/145] Use different export method --- fastlane/Fastfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 462062383e3f..8435fe765b83 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -474,7 +474,7 @@ platform :ios do configuration: "Debug", sdk: "iphonesimulator", skip_codesigning: true, - export_method: "development" + export_method: "debugging" ) setIOSBuildOutputsInEnv() end From 1b8a1e510d3f052091e82d27d6c2979072de8c6c Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 14:04:11 -0800 Subject: [PATCH 135/145] Remove Firebase distribution as QA does not use it anymore --- .github/workflows/deploy.yml | 8 -------- fastlane/Fastfile | 21 --------------------- 2 files changed, 29 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0c6ffd4306f7..6ca2f0f8a698 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -252,10 +252,6 @@ jobs: name: android-hybrid-apk-artifact path: Expensify.apk - - name: Upload Android build to Firebase distribution - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane android upload_firebase_distribution - - name: Upload Android build artifact if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} uses: actions/upload-artifact@v4 @@ -577,10 +573,6 @@ jobs: env: BROWSERSTACK: ${{ secrets.BROWSERSTACK }} - - name: Upload iOS build to Firebase distribution - if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} - run: bundle exec fastlane ios upload_firebase_distribution - - name: Upload iOS build artifact if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }} uses: actions/upload-artifact@v4 diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 7efd6d5ebe1b..8ef8a8c10b4a 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -197,17 +197,6 @@ platform :android do ) end - desc "Upload app to Firebase distribution" - lane :upload_firebase_distribution do - firebase_app_distribution( - app: "1:1008697809946:android:2e48f9ffe8d0b6a2", - service_credentials_file: "./firebase.json", - groups: "applause", - android_artifact_path: ENV[KEY_GRADLE_AAB_PATH], - android_artifact_type: "AAB" - ) - end - desc "Upload HybridApp to Google Play for internal testing" lane :upload_google_play_internal_hybrid do # Google is very unreliable, so we retry a few times @@ -518,16 +507,6 @@ platform :ios do sh("echo '{\"ipa_path\": \"#{lane_context[SharedValues::S3_IPA_OUTPUT_PATH]}\",\"html_path\": \"#{lane_context[SharedValues::S3_HTML_OUTPUT_PATH]}\"}' > ../ios_paths.json") end - desc "Upload app to Firebase distribution" - lane :upload_firebase_distribution do - firebase_app_distribution( - app: "1:1008697809946:ios:3ffad71f664f2886", - service_credentials_file: "./firebase.json", - groups: "applause", - ipa_path: ENV[KEY_IPA_PATH], - ) - end - desc "Upload app to TestFlight" lane :upload_testflight do upload_to_testflight( From 2c91a49b74a29f02f92a7661c748b9e10d0448d5 Mon Sep 17 00:00:00 2001 From: Lauren Schurr <33293730+lschurr@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:21:17 -0800 Subject: [PATCH 136/145] Update Plan-types-and-pricing.md --- .../Plan-types-and-pricing.md | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md b/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md index bc39e33bab4a..ee181706d70d 100644 --- a/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md +++ b/docs/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing.md @@ -1,48 +1,46 @@ --- -title: Expensify plan types and pricing +title: Expensify Plan Types and Pricing +description: An overview of Expensify's plan types and pricing description: An overview of plan types and pricing --- -
-Expensify offers plans and flexible pricing to cater to different business sizes and needs, whether you’re self-employed, part of a large organization, or anything in between. +Expensify offers flexible pricing plans designed to suit different business sizes and needs, whether you’re self-employed, part of a large organization, or somewhere in between. # Choosing the Right Plan -Expensify offers two pricing plans: +Expensify provides two main pricing plans: +| Feature | **Collect Plan** | **Control Plan** | +|----------------------|--------------------------------------------------|--------------------------------------------------| +| **Ideal for** | Small teams or businesses with 1-10 employees | Larger companies with 10-1000 employees | +| **Pricing*** | $5 USD per user/month | $9 USD per user/month | +| **SmartScans** | ✔ Unlimited | ✔ Unlimited | +| **Expensify Card** | ✔ Smart Limits & 1-2% cash back | ✔ Smart Limits & 1-2% cash back | +| **Expense Approvals** | ✔ Yes | ✔ Multiple approvers | +| **ACH Reimbursement** | ✔ Unlimited | ✔ Unlimited | +| **Bank Feed Support** | ❌ Not available | ✔ Third-party card feeds & reconciliation | +| **Accounting Sync** | ✔ QuickBooks Online & Xero | ✔ NetSuite, Sage Intacct, QuickBooks Desktop | +| **HR & Payroll Sync** | ❌ Not available | ✔ Gusto, Zenefits, Certinia, Workday | +| **Security & Control**| ❌ Not available | ✔ SAML/SSO & admin-enforced controls | -| | Collect Plan | Control Plan | -|--------------------|---------------------------|---------------------------------------------------------| -| **Ideal for:** | Sole proprietors and small teams or businesses with 1-10 employees | Larger companies with 10-1000 employees and more complex expense management needs | -| **Pricing starts at:** | $5 USD per user/month on an annual subscription (Non-USD prices available in FAQ) | $9 USD per user/month on an annual subscription (Non-USD prices available in FAQ) | -| | ✔ Unlimited SmartScans and distance tracking | ✔ All Collect Plan features | -| | ✔ Expensify Cards with Smart Limits and cash back | ✔ Third-party card feeds and reconciliation | -| | ✔ Expense approvals | ✔ Integration with NetSuite, Sage Intacct, and QuickBooks Desktop | -| | ✔ Unlimited ACH reimbursement | ✔ Gusto, Zenefits, Certinia, and Workday sync | -| | ✔ Integration with QuickBooks Online and Xero | ✔ Multiple expense approvers | -| | | ✔ SAML/SSO for added security | -| | | ✔ Admin-enforced controls | +***Note**: This price is available if you have an **Annual Subscription** and your team adopts the **Expensify Card**. Expensify Card usage on both plans generates 1% cash back with every swipe on US purchases—no minimums necessary—and 2% back if you spend $250k+/month across cards. -Expensify Card usage on both plans generates 1% cash back with every swipe on US purchases --- no minimums necessary --- and 2% back if you spend $250k+/month across cards. +--- # FAQ ## How much does Expensify cost? - -The cost depends on your plan and subscription type. Expensify offers a 50% discount for annual subscriptions and up to another 50% discount for using Expensify Cards. Try out our [savings calculator](https://use.expensify.com/savings-calculator) for an easy estimate based on your numbers. +The cost depends on your plan and subscription type. Expensify offers a 50% discount for annual subscriptions and up to another 50% discount for using Expensify Cards. Try out our [savings calculator](https://use.expensify.com/savings-calculator) to estimate your cost. ## Does Expensify bill in non-USD currencies? +Yes! Customers can pay in AUD, GBP, or NZD in addition to USD. -Yes! Customers can pay for Expensify in AUD, GBP, or NZD in addition to USD. -- The Collect plan begins at A$14, £8, or NZ$16 per user/month on an annual subscription -- The Control plan begins at A$30, £14, or NZ$32 per user/month on an annual subscription +- **Collect Plan:** A$14, £8, or NZ$16 per user/month (Annual subscription + Expensify Cards) +- **Control Plan:** A$30, £14, or NZ$32 per user/month (Annual subscription + Expensify Cards) ## Is Expensify free for individuals? - Yes! Individuals can use Expensify for free to track expenses. ## How do I get more info about pricing? +For customized information or help choosing the right plan, reach out to Expensify Concierge or email **concierge@expensify.com**. -For customized information or help choosing the right plan, reach out to Expensify Concierge or email concierge@expensify.com. - -
From 5c4901135908e6f5836e1b9db36a5b06ab969055 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 14:37:25 -0800 Subject: [PATCH 137/145] Try to skip archive as well --- fastlane/Fastfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8435fe765b83..1698b9c5a3d6 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -474,7 +474,8 @@ platform :ios do configuration: "Debug", sdk: "iphonesimulator", skip_codesigning: true, - export_method: "debugging" + skip_archive: true, + export_method: "development" ) setIOSBuildOutputsInEnv() end From 70dac048d41a0f026b17e6216e3972a192e5eb4f Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 14:51:29 -0800 Subject: [PATCH 138/145] Sync ios standalone and HybridApp settings --- fastlane/Fastfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1698b9c5a3d6..7858af37733e 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -460,7 +460,9 @@ platform :ios do scheme: "New Expensify", configuration: "Debug", sdk: "iphonesimulator", - skip_codesigning: true + skip_codesigning: true, + skip_archive: true, + export_method: "development" ) setIOSBuildOutputsInEnv() end From 367250302e7a3c83e49a589525948b22ead7225f Mon Sep 17 00:00:00 2001 From: OSBotify Date: Mon, 3 Feb 2025 22:53:49 +0000 Subject: [PATCH 139/145] Update version to 9.0.94-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 25c5cf9cfc7d..d184323caeee 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 1009009303 - versionName "9.0.93-3" + versionCode 1009009400 + versionName "9.0.94-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 0e72054a1a83..ab1e4d711b54 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -23,7 +23,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 9.0.93 + 9.0.94 CFBundleSignature ???? CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 9.0.93.3 + 9.0.94.0 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 58532a22ad85..431d54e8732d 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 9.0.93 + 9.0.94 CFBundleSignature ???? CFBundleVersion - 9.0.93.3 + 9.0.94.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 2ac74ffa3390..571c00e8b18e 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 9.0.93 + 9.0.94 CFBundleVersion - 9.0.93.3 + 9.0.94.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 9befce9c5196..d2f986d5a5e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.93-3", + "version": "9.0.94-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.93-3", + "version": "9.0.94-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 297b440b988e..d1796d0e5fe5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.93-3", + "version": "9.0.94-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 a038781e23f7c483c8658272a4efe7a08d7272cf Mon Sep 17 00:00:00 2001 From: OSBotify Date: Mon, 3 Feb 2025 22:53:49 +0000 Subject: [PATCH 140/145] Update Mobile-Expensify submodule version to 9.0.94-0 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 6a1bd2e5c803..ec7e3cb97309 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 6a1bd2e5c80336edaf5ea929ce30102cfa3ef65b +Subproject commit ec7e3cb973093ffc57bfaf084d7f56eafee1a9e7 From 4b771fb7b1704c2484746c2aa614bf427f92b558 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 15:10:44 -0800 Subject: [PATCH 141/145] Revert M/E --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index 10beb38304d3..6a1bd2e5c803 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit 10beb38304d35ee86b948f64afcdf49d51ab3d4a +Subproject commit 6a1bd2e5c80336edaf5ea929ce30102cfa3ef65b From 236fc8f4c62b4919d0a5d8d71fc943ad11c59a37 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 15:11:33 -0800 Subject: [PATCH 142/145] Remove debugging --- .github/workflows/verifyHybridApp.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index b3c9a9e1f041..c559aa0bead5 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -17,8 +17,6 @@ on: - 'android/AndroidManifest.xml' - 'ios/Podfile.lock' - 'ios/project.pbxproj' - # TODO: Remove this line, just for debugging - - '**.yml' concurrency: group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-verify-main From 45fa83475575ce676dd93dc4436c78ae7e6b752c Mon Sep 17 00:00:00 2001 From: OSBotify Date: Mon, 3 Feb 2025 23:18:11 +0000 Subject: [PATCH 143/145] Update version to 9.0.94-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 d184323caeee..98a560cbbed6 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 1009009400 - versionName "9.0.94-0" + versionCode 1009009401 + versionName "9.0.94-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 ab1e4d711b54..25deb5ad9f83 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.0.94.0 + 9.0.94.1 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 431d54e8732d..acfce5c2e675 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.94.0 + 9.0.94.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 571c00e8b18e..2e661cccef7f 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.94 CFBundleVersion - 9.0.94.0 + 9.0.94.1 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index d2f986d5a5e8..ef23310e7f26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.94-0", + "version": "9.0.94-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.94-0", + "version": "9.0.94-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d1796d0e5fe5..7d68d8c6e436 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.94-0", + "version": "9.0.94-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 7f27b5af789fb2a294871527fcc64e644bf13caf Mon Sep 17 00:00:00 2001 From: OSBotify Date: Mon, 3 Feb 2025 23:18:12 +0000 Subject: [PATCH 144/145] Update Mobile-Expensify submodule version to 9.0.94-1 --- Mobile-Expensify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mobile-Expensify b/Mobile-Expensify index ec7e3cb97309..7d33c85d7554 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit ec7e3cb973093ffc57bfaf084d7f56eafee1a9e7 +Subproject commit 7d33c85d75549f3bc95621538ce0ca47b06074a9 From 2336b9a3f112df2d070772fc8502f5f1e08579d6 Mon Sep 17 00:00:00 2001 From: Andrew Gable Date: Mon, 3 Feb 2025 15:41:44 -0800 Subject: [PATCH 145/145] Remove bundle install --- .github/workflows/verifyHybridApp.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/verifyHybridApp.yml b/.github/workflows/verifyHybridApp.yml index c559aa0bead5..c69487a1b4e4 100644 --- a/.github/workflows/verifyHybridApp.yml +++ b/.github/workflows/verifyHybridApp.yml @@ -68,9 +68,6 @@ jobs: with: bundler-cache: true - - name: Install New Expensify Gems - run: bundle install - - name: Build Android Debug run: | if ! npm run android-hybrid-build @@ -112,9 +109,6 @@ jobs: with: bundler-cache: true - - name: Install New Expensify Gems - run: bundle install - - name: Cache Pod dependencies uses: actions/cache@v4 id: pods-cache