diff --git a/assets/images/product-illustrations/emptystate__holdexpense.svg b/assets/images/product-illustrations/emptystate__holdexpense.svg new file mode 100644 index 000000000000..d00738964047 --- /dev/null +++ b/assets/images/product-illustrations/emptystate__holdexpense.svg @@ -0,0 +1,1207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__realtimereports.svg b/assets/images/simple-illustrations/simple-illustration__realtimereports.svg new file mode 100644 index 000000000000..40fc3082a028 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__realtimereports.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__stopwatch.svg b/assets/images/simple-illustrations/simple-illustration__stopwatch.svg new file mode 100644 index 000000000000..c348fd73337b --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__stopwatch.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 1ae76f72ccef..5c9263e911b0 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -1,18 +1,23 @@ import type {VideoReadyForDisplayEvent} from 'expo-av'; +import type {ImageContentFit} from 'expo-image'; import React, {useCallback, useEffect, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; +import type {MergeExclusive} from 'type-fest'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; -import * as User from '@userActions/User'; +import {dismissTrackTrainingModal} from '@userActions/User'; import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import CheckboxWithLabel from './CheckboxWithLabel'; +import ImageSVG from './ImageSVG'; import Lottie from './Lottie'; import LottieAnimations from './LottieAnimations'; import type DotLottieAnimation from './LottieAnimations/types'; @@ -36,26 +41,18 @@ type VideoLoadedEventType = { type VideoStatus = 'video' | 'animation'; -type FeatureTrainingModalProps = { - /** Animation to show when video is unavailable. Useful when app is offline */ - animation?: DotLottieAnimation; +type BaseFeatureTrainingModalProps = { + /** The aspect ratio to preserve for the icon, video or animation */ + illustrationAspectRatio?: number; /** Style for the inner container of the animation */ - animationInnerContainerStyle?: StyleProp; + illustrationInnerContainerStyle?: StyleProp; /** Style for the outer container of the animation */ - animationOuterContainerStyle?: StyleProp; - - /** Additional styles for the animation */ - animationStyle?: StyleProp; - - /** URL for the video */ - videoURL: string; - - videoAspectRatio?: number; + illustrationOuterContainerStyle?: StyleProp; /** Title for the modal */ - title?: string; + title?: string | React.ReactNode; /** Describe what is showing */ description?: string; @@ -81,9 +78,6 @@ type FeatureTrainingModalProps = { /** Link to navigate to when user wants to learn more */ onHelp?: () => void; - /** Children to render */ - children?: React.ReactNode; - /** Styles for the content container */ contentInnerContainerStyles?: StyleProp; @@ -92,15 +86,46 @@ type FeatureTrainingModalProps = { /** Styles for the modal inner container */ modalInnerContainerStyle?: ViewStyle; + + /** Children to show below title and description and above buttons */ + children?: React.ReactNode; + + /** Modal width */ + width?: number; +}; + +type FeatureTrainingModalVideoProps = { + /** Animation to show when video is unavailable. Useful when app is offline */ + animation?: DotLottieAnimation; + + /** Additional styles for the animation */ + animationStyle?: StyleProp; + + /** URL for the video */ + videoURL?: string; +}; + +type FeatureTrainingModalSVGProps = { + /** Expensicon for the page */ + image: IconAsset; + + /** Determines how the image should be resized to fit its container */ + contentFitImage?: ImageContentFit; }; +// This page requires either an icon or a video/animation, but not both +type FeatureTrainingModalProps = BaseFeatureTrainingModalProps & MergeExclusive; + function FeatureTrainingModal({ animation, animationStyle, - animationInnerContainerStyle, - animationOuterContainerStyle, + illustrationInnerContainerStyle, + illustrationOuterContainerStyle, videoURL, - videoAspectRatio: videoAspectRatioProp, + illustrationAspectRatio: illustrationAspectRatioProp, + image, + contentFitImage, + width = variables.onboardingModalWidth, title = '', description = '', secondaryDescription = '', @@ -116,13 +141,14 @@ function FeatureTrainingModal({ modalInnerContainerStyle, }: FeatureTrainingModalProps) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout(); const [isModalVisible, setIsModalVisible] = useState(false); const [willShowAgain, setWillShowAgain] = useState(true); const [videoStatus, setVideoStatus] = useState('video'); const [isVideoStatusLocked, setIsVideoStatusLocked] = useState(false); - const [videoAspectRatio, setVideoAspectRatio] = useState(videoAspectRatioProp ?? VIDEO_ASPECT_RATIO); + const [illustrationAspectRatio, setIllustrationAspectRatio] = useState(illustrationAspectRatioProp ?? VIDEO_ASPECT_RATIO); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isOffline} = useNetwork(); @@ -149,14 +175,14 @@ function FeatureTrainingModal({ } if ('naturalSize' in event) { - setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height); + setIllustrationAspectRatio(event.naturalSize.width / event.naturalSize.height); } else { - setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight); + setIllustrationAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight); } }; const renderIllustration = useCallback(() => { - const aspectRatio = videoAspectRatio || VIDEO_ASPECT_RATIO; + const aspectRatio = illustrationAspectRatio || VIDEO_ASPECT_RATIO; return ( - {!!videoURL && videoStatus === 'video' ? ( + {!!image && ( + + )} + {!!videoURL && videoStatus === 'video' && ( - ) : ( + )} + {((!videoURL && !image) || (!!videoURL && videoStatus === 'animation')) && ( ); }, [ - videoAspectRatio, + image, + contentFitImage, + illustrationAspectRatio, styles.w100, styles.onboardingVideoPlayer, styles.flex1, @@ -208,14 +243,14 @@ function FeatureTrainingModal({ animationStyle, animation, shouldUseNarrowLayout, - animationInnerContainerStyle, + illustrationInnerContainerStyle, ]); const toggleWillShowAgain = useCallback(() => setWillShowAgain((prevWillShowAgain) => !prevWillShowAgain), []); const closeModal = useCallback(() => { if (!willShowAgain) { - User.dismissTrackTrainingModal(); + dismissTrackTrainingModal(); } setIsModalVisible(false); InteractionManager.runAfterInteractions(() => { @@ -238,7 +273,6 @@ function FeatureTrainingModal({ onClose={closeModal} innerContainerStyle={{ boxShadow: 'none', - borderRadius: 16, paddingBottom: 20, paddingTop: onboardingIsMediumOrLargerScreenWidth ? undefined : MODAL_PADDING, ...(onboardingIsMediumOrLargerScreenWidth @@ -252,14 +286,14 @@ function FeatureTrainingModal({ ...modalInnerContainerStyle, }} > - - + + {renderIllustration()} {!!title && !!description && ( - {title} + {typeof title === 'string' ? {title} : title} {description} {secondaryDescription.length > 0 && {secondaryDescription}} {children} diff --git a/src/components/HoldMenuSectionList.tsx b/src/components/HoldMenuSectionList.tsx index 4ffdfa1bd60e..180b801c3f3c 100644 --- a/src/components/HoldMenuSectionList.tsx +++ b/src/components/HoldMenuSectionList.tsx @@ -16,9 +16,6 @@ type HoldMenuSection = { /** Translation key for the title */ titleTranslationKey: TranslationPaths; - - /** Translation key for the description */ - descriptionTranslationKey: TranslationPaths; }; function HoldMenuSectionList() { @@ -27,19 +24,12 @@ function HoldMenuSectionList() { const holdMenuSections: HoldMenuSection[] = [ { - icon: Illustrations.Hourglass, - titleTranslationKey: 'iou.whatIsHoldTitle', - descriptionTranslationKey: 'iou.whatIsHoldExplain', - }, - { - icon: Illustrations.CommentBubbles, - titleTranslationKey: 'iou.holdIsTemporaryTitle', - descriptionTranslationKey: 'iou.holdIsTemporaryExplain', + icon: Illustrations.Stopwatch, + titleTranslationKey: 'iou.holdIsLeftBehind', }, { - icon: Illustrations.TrashCan, - titleTranslationKey: 'iou.deleteHoldTitle', - descriptionTranslationKey: 'iou.deleteHoldExplain', + icon: Illustrations.RealtimeReport, + titleTranslationKey: 'iou.unholdWhenReady', }, ]; @@ -49,17 +39,16 @@ function HoldMenuSectionList() { {translate(section.titleTranslationKey)} - {translate(section.descriptionTranslationKey)} ))} diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 0debd4585e7b..fd5eb9f6fd58 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -38,6 +38,7 @@ import ConciergeBlue from '@assets/images/product-illustrations/concierge--blue. import ConciergeExclamation from '@assets/images/product-illustrations/concierge--exclamation.svg'; import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg'; import EmptyStateExpenses from '@assets/images/product-illustrations/emptystate__expenses.svg'; +import HoldExpense from '@assets/images/product-illustrations/emptystate__holdexpense.svg'; import EmptyStateTravel from '@assets/images/product-illustrations/emptystate__travel.svg'; import FolderWithPapers from '@assets/images/product-illustrations/folder-with-papers.svg'; import GpsTrackOrange from '@assets/images/product-illustrations/gps-track--orange.svg'; @@ -121,6 +122,7 @@ import PiggyBank from '@assets/images/simple-illustrations/simple-illustration__ import Pillow from '@assets/images/simple-illustrations/simple-illustration__pillow.svg'; import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg'; import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg'; +import RealtimeReport from '@assets/images/simple-illustrations/simple-illustration__realtimereports.svg'; import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg'; import ReceiptLocationMarker from '@assets/images/simple-illustrations/simple-illustration__receipt-location-marker.svg'; import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg'; @@ -131,6 +133,7 @@ import SendMoney from '@assets/images/simple-illustrations/simple-illustration__ import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg'; import SmallRocket from '@assets/images/simple-illustrations/simple-illustration__smallrocket.svg'; import SplitBill from '@assets/images/simple-illustrations/simple-illustration__splitbill.svg'; +import Stopwatch from '@assets/images/simple-illustrations/simple-illustration__stopwatch.svg'; import SubscriptionAnnual from '@assets/images/simple-illustrations/simple-illustration__subscription-annual.svg'; import SubscriptionPPU from '@assets/images/simple-illustrations/simple-illustration__subscription-ppu.svg'; import Tag from '@assets/images/simple-illustrations/simple-illustration__tag.svg'; @@ -223,6 +226,8 @@ export { LockClosed, Gears, QRCode, + RealtimeReport, + HoldExpense, ReceiptEnvelope, Approval, WalletAlt, @@ -251,6 +256,7 @@ export { ReceiptLocationMarker, Lightbulb, EmptyStateTravel, + Stopwatch, SubscriptionAnnual, SubscriptionPPU, ExpensifyApprovedLogo, diff --git a/src/components/MigratedUserWelcomeModal.tsx b/src/components/MigratedUserWelcomeModal.tsx index d097e3095298..312f6e0f77d0 100644 --- a/src/components/MigratedUserWelcomeModal.tsx +++ b/src/components/MigratedUserWelcomeModal.tsx @@ -4,7 +4,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Welcome from '@libs/actions/Welcome'; +import {dismissProductTraining} from '@libs/actions/Welcome'; import convertToLTR from '@libs/convertToLTR'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -45,11 +45,11 @@ function OnboardingWelcomeVideo() { confirmText={translate('migratedUserWelcomeModal.confirmText')} animation={LottieAnimations.WorkspacePlanet} onClose={() => { - Welcome.dismissProductTraining(CONST.MIGRATED_USER_WELCOME_MODAL); + dismissProductTraining(CONST.MIGRATED_USER_WELCOME_MODAL); }} animationStyle={[styles.emptyWorkspaceIllustrationStyle]} - animationInnerContainerStyle={[StyleUtils.getBackgroundColorStyle(LottieAnimations.WorkspacePlanet.backgroundColor), styles.cardSectionIllustration]} - animationOuterContainerStyle={styles.p0} + illustrationInnerContainerStyle={[StyleUtils.getBackgroundColorStyle(LottieAnimations.WorkspacePlanet.backgroundColor), styles.cardSectionIllustration]} + illustrationOuterContainerStyle={styles.p0} contentInnerContainerStyles={[styles.mb5, styles.gap2]} contentOuterContainerStyles={!shouldUseNarrowLayout && [styles.mt8, styles.mh8]} modalInnerContainerStyle={{...styles.pt0, ...(shouldUseNarrowLayout ? {} : styles.pb8)}} diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 6c7a43758374..1654314e576d 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -9,15 +9,44 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCurrentUserAccountID} from '@libs/actions/Report'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; 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 {getConnectedIntegration, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getOriginalMessage, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils'; +import { + canBeExported, + canDeleteTransaction, + getArchiveReason, + getBankAccountRoute, + getMoneyRequestSpendBreakdown, + getNonHeldAndFullAmount, + getTransactionsWithReceipts, + hasHeldExpenses as hasHeldExpensesReportUtils, + hasOnlyHeldExpenses as hasOnlyHeldExpensesReportUtils, + hasUpdatedTotal, + isAllowedToApproveExpenseReport, + isAllowedToSubmitDraftExpenseReport, + isArchivedReport as isArchivedReportUtils, + isClosedExpenseReportWithNoExpenses, + isCurrentUserSubmitter, + isInvoiceReport, + isOpenExpenseReport, + navigateBackOnDeleteTransaction, +} from '@libs/ReportUtils'; +import { + allHavePendingRTERViolation, + getAllReportTransactions, + isDuplicate as isDuplicateTransactionUtils, + isExpensifyCardTransaction, + isOnHold as isOnHoldTransactionUtils, + isPayAtEndExpense as isPayAtEndExpenseTransactionUtils, + isPending, + isReceiptBeingScanned, + shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, +} from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import * as IOU from '@userActions/IOU'; -import * as TransactionActions from '@userActions/Transaction'; +import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidAction, deleteMoneyRequest, deleteTrackExpense, payInvoice, payMoneyRequest, submitReport} from '@userActions/IOU'; +import {markAsCash as markAsCashAction} from '@userActions/Transaction'; import CONST from '@src/CONST'; import useDelegateUserDetails from '@src/hooks/useDelegateUserDetails'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -40,7 +69,6 @@ import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusB import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import type {ActionHandledType} from './ProcessMoneyReportHoldMenu'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; -import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; import ExportWithDropdownMenu from './ReportActionItem/ExportWithDropdownMenu'; import SettlementButton from './SettlementButton'; @@ -67,8 +95,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const route = useRoute(); - const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`); - const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID || CONST.DEFAULT_NUMBER_ID}`); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID || CONST.DEFAULT_NUMBER_ID}`); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); const [session] = useOnyx(ONYXKEYS.SESSION); const requestParentReportAction = useMemo(() => { @@ -81,49 +111,45 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true}); const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult); const transaction = - transactions?.[ - `${ONYXKEYS.COLLECTION.TRANSACTION}${ - ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) && ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID - }` - ] ?? undefined; + transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${isMoneyRequestAction(requestParentReportAction) && getOriginalMessage(requestParentReportAction)?.IOUTransactionID}`] ?? + undefined; const styles = useThemeStyles(); const theme = useTheme(); const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false); - const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport); - const isOnHold = TransactionUtils.isOnHold(transaction); - const isDeletedParentAction = !!requestParentReportAction && ReportActionsUtils.isDeletedAction(requestParentReportAction); - const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID); + const {reimbursableSpend} = getMoneyRequestSpendBreakdown(moneyRequestReport); + const isOnHold = isOnHoldTransactionUtils(transaction); + const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction); + const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID); // Only the requestor can delete the request, admins can only edit it. const isActionOwner = typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID; - const canDeleteRequest = isActionOwner && ReportUtils.canDeleteTransaction(moneyRequestReport) && !isDeletedParentAction; + const canDeleteRequest = isActionOwner && canDeleteTransaction(moneyRequestReport) && !isDeletedParentAction; const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [paymentType, setPaymentType] = useState(); const [requestType, setRequestType] = useState(); - const allTransactions = useMemo(() => TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID, transactions), [moneyRequestReport?.reportID, transactions]); - const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport, policy); + const allTransactions = useMemo(() => getAllReportTransactions(moneyRequestReport?.reportID, transactions), [moneyRequestReport?.reportID, transactions]); + const canAllowSettlement = hasUpdatedTotal(moneyRequestReport, policy); const policyType = policy?.type; - const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport); - const connectedIntegration = PolicyUtils.getConnectedIntegration(policy); + const isDraft = isOpenExpenseReport(moneyRequestReport); + const connectedIntegration = getConnectedIntegration(policy); const navigateBackToAfterDelete = useRef(); - const hasHeldExpenses = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID); - const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t)); - const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t)); + const hasHeldExpenses = hasHeldExpensesReportUtils(moneyRequestReport?.reportID); + const hasScanningReceipt = getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => isReceiptBeingScanned(t)); + const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => isExpensifyCardTransaction(t) && isPending(t)); const transactionIDs = allTransactions.map((t) => t.transactionID); - const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID]); - const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID, moneyRequestReport, policy); - const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport?.reportID); - const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction); - const isArchivedReport = ReportUtils.isArchivedReport(moneyRequestReport); - const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: ReportUtils.getArchiveReason}); + const hasAllPendingRTERViolations = allHavePendingRTERViolation([transaction?.transactionID]); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transaction?.transactionID, moneyRequestReport, policy); + const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(moneyRequestReport?.reportID); + const isPayAtEndExpense = isPayAtEndExpenseTransactionUtils(transaction); + const isArchivedReport = isArchivedReportUtils(moneyRequestReport); + const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: getArchiveReason}); const getCanIOUBePaid = useCallback( - (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere), + (onlyShowPayElsewhere = false) => canIOUBePaidAction(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere), [moneyRequestReport, chatReport, policy, transaction], ); const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]); @@ -131,13 +157,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]); const shouldShowMarkAsCashButton = - hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isCurrentUserSubmitter(moneyRequestReport?.reportID))); + hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(moneyRequestReport?.reportID))); const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere; - const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions, [moneyRequestReport, policy, hasOnlyPendingTransactions]); + const shouldShowApproveButton = useMemo(() => canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions, [moneyRequestReport, policy, hasOnlyPendingTransactions]); - const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport); + const shouldDisableApproveButton = shouldShowApproveButton && !isAllowedToApproveExpenseReport(moneyRequestReport); const currentUserAccountID = getCurrentUserAccountID(); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; @@ -151,16 +177,16 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea !shouldShowBrokenConnectionViolation && (moneyRequestReport?.ownerAccountID === currentUserAccountID || isAdmin || moneyRequestReport?.managerID === currentUserAccountID); - const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(moneyRequestReport); + const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && canBeExported(moneyRequestReport); const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !hasAllPendingRTERViolations && !shouldShowExportIntegrationButton && !shouldShowBrokenConnectionViolation; - const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); + const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(moneyRequestReport); const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; const shouldShowStatusBar = hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions; - const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar; + const shouldShowNextStep = !isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar; const shouldShowAnyButton = isDuplicate || shouldShowSettlementButton || @@ -169,10 +195,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea shouldShowNextStep || shouldShowMarkAsCashButton || shouldShowExportIntegrationButton; - const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); - const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency); - const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton); - const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID); + const bankAccountRoute = getBankAccountRoute(chatReport); + const formattedAmount = convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency); + const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton); + const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID); const displayedAmount = isAnyTransactionOnHold && canAllowSettlement && hasValidNonHeldAmount ? nonHeldAmount : formattedAmount; const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout); const {isDelegateAccessRestricted} = useDelegateUserDetails(); @@ -192,10 +218,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setIsNoDelegateAccessMenuVisible(true); } else if (isAnyTransactionOnHold) { setIsHoldMenuVisible(true); - } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) { - IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness); + } else if (isInvoiceReport(moneyRequestReport)) { + payInvoice(type, chatReport, moneyRequestReport, payAsBusiness); } else { - IOU.payMoneyRequest(type, chatReport, moneyRequestReport, true); + payMoneyRequest(type, chatReport, moneyRequestReport, true); } }, [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport], @@ -208,19 +234,17 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea } else if (isAnyTransactionOnHold) { setIsHoldMenuVisible(true); } else { - IOU.approveMoneyRequest(moneyRequestReport, true); + approveMoneyRequest(moneyRequestReport, true); } }; const deleteTransaction = useCallback(() => { if (requestParentReportAction) { - const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) - ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID - : undefined; - if (ReportActionsUtils.isTrackExpenseAction(requestParentReportAction)) { - navigateBackToAfterDelete.current = IOU.deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, true); + const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined; + if (isTrackExpenseAction(requestParentReportAction)) { + navigateBackToAfterDelete.current = deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, true); } else { - navigateBackToAfterDelete.current = IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, true); + navigateBackToAfterDelete.current = deleteMoneyRequest(iouTransactionID, requestParentReportAction, true); } } @@ -231,15 +255,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea if (!requestParentReportAction) { return; } - const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) - ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID - : undefined; + const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined; const reportID = transactionThreadReport?.reportID; if (!iouTransactionID || !reportID) { return; } - TransactionActions.markAsCash(iouTransactionID, reportID); + markAsCashAction(iouTransactionID, reportID); }, [requestParentReportAction, transactionThreadReport?.reportID]); const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => ( @@ -304,30 +326,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea ); useEffect(() => { - if (isLoadingHoldUseExplained) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (isLoadingHoldUseExplained || dismissedHoldUseExplanation || !isOnHold) { return; } - setShouldShowHoldMenu(isOnHold && !dismissedHoldUseExplanation); + Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute())); }, [dismissedHoldUseExplanation, isLoadingHoldUseExplained, isOnHold]); - useEffect(() => { - if (!shouldShowHoldMenu) { - return; - } - - if (isSmallScreenWidth) { - if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD.route) { - Navigation.goBack(); - } - } else { - Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute())); - } - }, [isSmallScreenWidth, shouldShowHoldMenu]); - - const handleHoldRequestClose = () => { - IOU.dismissHoldUseExplanation(); - }; - useEffect(() => { if (canDeleteRequest) { return; @@ -400,7 +405,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea success={isWaitingForSubmissionFromCurrentUser} text={translate('common.submit')} style={[styles.mnw120, styles.pv2, styles.pr0]} - onPress={() => IOU.submitReport(moneyRequestReport)} + onPress={() => submitReport(moneyRequestReport)} isDisabled={shouldDisableSubmitButton} /> @@ -462,7 +467,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea success={isWaitingForSubmissionFromCurrentUser} text={translate('common.submit')} style={[styles.flex1, styles.pr0]} - onPress={() => IOU.submitReport(moneyRequestReport)} + onPress={() => submitReport(moneyRequestReport)} isDisabled={shouldDisableSubmitButton} /> )} @@ -507,20 +512,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea isVisible={isDeleteRequestModalVisible} onConfirm={deleteTransaction} onCancel={() => setIsDeleteRequestModalVisible(false)} - onModalHide={() => ReportUtils.navigateBackOnDeleteTransaction(navigateBackToAfterDelete.current)} + onModalHide={() => navigateBackOnDeleteTransaction(navigateBackToAfterDelete.current)} prompt={translate('iou.deleteConfirmation', {count: 1})} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} danger shouldEnableNewFocusManagement /> - {isSmallScreenWidth && shouldShowHoldMenu && ( - - )} ); } diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index f253c757050f..32480e37d8ee 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -1,6 +1,6 @@ import {useRoute} from '@react-navigation/native'; import type {ReactNode} from 'react'; -import React, {useCallback, useEffect, useState} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -9,13 +9,24 @@ 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 isDuplicateTransactionUtils, + isExpensifyCardTransaction, + isOnHold as isOnHoldTransactionUtils, + isPending, + isReceiptBeingScanned, + shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils, +} from '@libs/TransactionUtils'; import variables from '@styles/variables'; -import * as IOU from '@userActions/IOU'; -import * as TransactionActions from '@userActions/Transaction'; +import {markAsCash as markAsCashAction} from '@userActions/Transaction'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -29,7 +40,6 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; -import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; type MoneyRequestHeaderProps = { /** The report currently being looked at */ @@ -50,10 +60,10 @@ 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 [transaction] = useOnyx( `${ONYXKEYS.COLLECTION.TRANSACTION}${ - ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? -1 : -1 + isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID : CONST.DEFAULT_NUMBER_ID }`, ); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); @@ -62,26 +72,24 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); - const isOnHold = TransactionUtils.isOnHold(transaction); - const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? ''); + const isOnHold = isOnHoldTransactionUtils(transaction); + const isDuplicate = isDuplicateTransactionUtils(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 = allHavePendingRTERViolation([transaction?.transactionID]); - const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', parentReport, policy); + const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(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 ?? '-1', reportID ?? ''); + markAsCashAction(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) => ( ), }; } - if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1', transactionViolations))) { + if (hasPendingRTERViolation(getTransactionViolations(transaction?.transactionID, transactionViolations))) { return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')}; } if (isScanning) { @@ -123,106 +131,90 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre const statusBarProps = getStatusBarProps(); useEffect(() => { - if (isLoadingHoldUseExplained) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (isLoadingHoldUseExplained || dismissedHoldUseExplanation || !isOnHold) { return; } - setShouldShowHoldMenu(isOnHold && !dismissedHoldUseExplanation); + Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute())); }, [dismissedHoldUseExplanation, isLoadingHoldUseExplained, isOnHold]); - useEffect(() => { - if (!shouldShowHoldMenu) { - return; - } - - if (isSmallScreenWidth) { - if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD.route) { - Navigation.goBack(); - } - } else { - Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD.getRoute(Navigation.getReportRHPActiveRoute())); - } - }, [isSmallScreenWidth, shouldShowHoldMenu]); - - const handleHoldRequestClose = () => { - IOU.dismissHoldUseExplanation(); - }; - return ( - <> - - - {shouldShowMarkAsCashButton && !shouldUseNarrowLayout && ( -