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 && (
-
- )}
- {isDuplicate && !shouldUseNarrowLayout && (
-
- {shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
-
-
-
- )}
- {isDuplicate && shouldUseNarrowLayout && (
-
-
+
+
+ {shouldShowMarkAsCashButton && !shouldUseNarrowLayout && (
+
)}
- {!!statusBarProps && (
-
-
-
+ {isDuplicate && !shouldUseNarrowLayout && (
+
- {isSmallScreenWidth && shouldShowHoldMenu && (
-
+
+ {shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
+
+
+
+ )}
+ {isDuplicate && shouldUseNarrowLayout && (
+
+
+ )}
+ {!!statusBarProps && (
+
+
+
)}
- >
+
);
}
diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx
index bb76ea0290f3..6ace3f5cdd37 100644
--- a/src/components/ProcessMoneyRequestHoldMenu.tsx
+++ b/src/components/ProcessMoneyRequestHoldMenu.tsx
@@ -1,38 +1,29 @@
import {useNavigation} from '@react-navigation/native';
-import React, {useEffect, useRef} from 'react';
+import React, {useEffect, useMemo} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
-import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
-import Button from './Button';
+import variables from '@styles/variables';
+import FeatureTrainingModal from './FeatureTrainingModal';
import HoldMenuSectionList from './HoldMenuSectionList';
-import type {PopoverAnchorPosition} from './Modal/types';
-import Popover from './Popover';
+import * as Illustrations from './Icon/Illustrations';
import Text from './Text';
import TextPill from './TextPill';
type ProcessMoneyRequestHoldMenuProps = {
- /** Whether the content is visible */
- isVisible: boolean;
-
/** Method to trigger when pressing outside of the popover menu to close it */
onClose: () => void;
/** Method to trigger when pressing confirm button */
onConfirm: () => void;
-
- /** The anchor position of the popover menu */
- anchorPosition?: PopoverAnchorPosition;
-
- /** The anchor alignment of the popover menu */
- anchorAlignment?: AnchorAlignment;
};
-function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosition, anchorAlignment}: ProcessMoneyRequestHoldMenuProps) {
+function ProcessMoneyRequestHoldMenu({onClose, onConfirm}: ProcessMoneyRequestHoldMenuProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const popoverRef = useRef(null);
const navigation = useNavigation();
+ const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout();
useEffect(() => {
const unsub = navigation.addListener('beforeRemove', () => {
@@ -41,32 +32,33 @@ function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosit
return unsub;
}, [navigation, onClose]);
+ const title = useMemo(
+ () => (
+
+ {translate('iou.holdEducationalTitle')}
+ {translate('iou.holdEducationalText')}
+
+ ),
+ [onboardingIsMediumOrLargerScreenWidth, styles.flexRow, styles.alignItemsCenter, styles.mb1, styles.mb2, styles.textHeadline, styles.mr2, styles.holdRequestInline, translate],
+ );
+
return (
-
-
-
- {translate('iou.holdEducationalTitle')}
- {translate('violations.hold')}
-
-
-
-
-
+
+
);
}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index e976ae1f67db..d8576e31cd7d 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1056,13 +1056,11 @@ const translations = {
}),
payOnly: 'Pay only',
approveOnly: 'Approve only',
- holdEducationalTitle: 'This expense is on',
- whatIsHoldTitle: 'What is hold?',
- whatIsHoldExplain: 'Hold is our way of streamlining financial collaboration. "Reject" is so harsh!',
- holdIsTemporaryTitle: 'Hold is usually temporary',
- holdIsTemporaryExplain: "Hold is used to clear up confusion or clarify an important detail before payment. Don't worry, it's not permanent!",
- deleteHoldTitle: "Delete whatever won't be paid",
- deleteHoldExplain: "In the rare case where something's put on hold and won't be paid, it's on the person requesting payment to delete it.",
+ holdEducationalTitle: 'This request is on',
+ holdEducationalText: 'hold',
+ whatIsHoldExplain: 'Hold is like hitting “pause” on an expense to ask for more details before approval or payment.',
+ holdIsLeftBehind: 'Held expenses are left behind even if you approve an entire report.',
+ unholdWhenReady: "Unhold expenses when you're ready to approve or pay.",
set: 'set',
changed: 'changed',
removed: 'removed',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index ae32a90bbcec..3b5ac1a8c7d3 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1054,13 +1054,11 @@ const translations = {
approveOnly: 'Solo aprobar',
hold: 'Retener',
unhold: 'Desbloquear',
- holdEducationalTitle: 'Este gasto está',
- whatIsHoldTitle: '¿Qué es Bloquear?',
- whatIsHoldExplain: 'Bloquear es nuestra forma de agilizar la colaboración financiera. ¡"Rechazar" es tan duro!',
- holdIsTemporaryTitle: 'Bloquear suele ser temporal',
- holdIsTemporaryExplain: 'Se utiliza bloquear para aclarar confusión o aclarar un detalle importante antes del pago, no es permanente.',
- deleteHoldTitle: 'Eliminar lo que no se pagará',
- deleteHoldExplain: 'En el raro caso de que algo se bloquee y no se pague, la persona que solicita el pago debe eliminarlo.',
+ holdEducationalTitle: 'Esta solicitud está',
+ holdEducationalText: 'retenida',
+ whatIsHoldExplain: 'Retener es como "pausar" un gasto para solicitar más detalles antes de aprobarlo o pagarlo.',
+ holdIsLeftBehind: 'Si apruebas un informe, los gastos retenidos se quedan fuera de esa aprobación.',
+ unholdWhenReady: 'Desbloquea los gastos cuando estés listo para aprobarlos o pagarlos.',
set: 'estableció',
changed: 'cambió',
removed: 'eliminó',
diff --git a/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx
index b5bf352c75f9..db110d53bc63 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/FeatureTrainingModalNavigator.tsx
@@ -4,6 +4,7 @@ import NoDropZone from '@components/DragAndDrop/NoDropZone';
import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
import type {FeatureTrainingNavigatorParamList} from '@libs/Navigation/types';
+import ProcessMoneyRequestHoldPage from '@pages/ProcessMoneyRequestHoldPage';
import TrackTrainingPage from '@pages/TrackTrainingPage';
import SCREENS from '@src/SCREENS';
@@ -18,6 +19,10 @@ function FeatureTrainingModalNavigator() {
name={SCREENS.FEATURE_TRAINING_ROOT}
component={TrackTrainingPage}
/>
+
diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
index 91533e66b5f0..95f82f4a2fdf 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx
@@ -187,10 +187,6 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
component={ModalStackNavigators.PrivateNotesModalStackNavigator}
options={hideKeyboardOnSwipe}
/>
-
['config'] = {
path: ROUTES.TRACK_TRAINING_MODAL,
exact: true,
},
+ [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: ROUTES.PROCESS_MONEY_REQUEST_HOLD.route,
},
},
[NAVIGATORS.WELCOME_VIDEO_MODAL_NAVIGATOR]: {
@@ -1365,11 +1366,6 @@ const config: LinkingOptions['config'] = {
[SCREENS.REFERRAL_DETAILS]: ROUTES.REFERRAL_DETAILS_MODAL.route,
},
},
- [SCREENS.RIGHT_MODAL.PROCESS_MONEY_REQUEST_HOLD]: {
- screens: {
- [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: ROUTES.PROCESS_MONEY_REQUEST_HOLD.route,
- },
- },
[SCREENS.RIGHT_MODAL.TRAVEL]: {
screens: {
[SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 088f624c7dc3..40290552b048 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1379,6 +1379,7 @@ type SignInNavigatorParamList = {
type FeatureTrainingNavigatorParamList = {
[SCREENS.FEATURE_TRAINING_ROOT]: undefined;
+ [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: undefined;
};
type ReferralDetailsNavigatorParamList = {
@@ -1388,10 +1389,6 @@ type ReferralDetailsNavigatorParamList = {
};
};
-type ProcessMoneyRequestHoldNavigatorParamList = {
- [SCREENS.PROCESS_MONEY_REQUEST_HOLD_ROOT]: undefined;
-};
-
type PrivateNotesNavigatorParamList = {
[SCREENS.PRIVATE_NOTES.LIST]: {
backTo?: Routes;
@@ -1468,7 +1465,6 @@ type RightModalNavigatorParamList = {
[SCREENS.RIGHT_MODAL.FLAG_COMMENT]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.EDIT_REQUEST]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.SIGN_IN]: NavigatorScreenParams;
- [SCREENS.RIGHT_MODAL.PROCESS_MONEY_REQUEST_HOLD]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.REFERRAL]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: NavigatorScreenParams;
[SCREENS.RIGHT_MODAL.TRANSACTION_DUPLICATE]: NavigatorScreenParams;
diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts
index c8a007458242..9ec9bd5f09e7 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});
});
@@ -363,13 +363,18 @@ function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmiss
const optimisticData: OnyxUpdate[] = [];
const failureData: OnyxUpdate[] = [];
- const optimisticReportActions: OnyxUpdate[] = transactionsReportActions.map((action, index) => ({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? '-1'}`,
- value: {
- [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? '']: optimisticDissmidedViolationReportActions.at(index) as ReportAction,
- },
- }));
+ const optimisticReportActions: 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]: optimisticDissmidedViolationReportAction as ReportAction,
+ }
+ : undefined,
+ };
+ });
const optimisticDataTransactionViolations: OnyxUpdate[] = currentTransactionViolations.map((transactionViolations) => ({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionViolations.transactionID}`,
@@ -411,25 +416,35 @@ function dismissDuplicateTransactionViolation(transactionIDs: string[], dissmiss
},
}));
- const failureReportActions: OnyxUpdate[] = transactionsReportActions.map((action, index) => ({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? '-1'}`,
- value: {
- [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? '']: null,
- },
- }));
+ const failureReportActions: 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,
+ };
+ });
failureData.push(...failureDataTransactionViolations);
failureData.push(...failureDataTransaction);
failureData.push(...failureReportActions);
- const successData: OnyxUpdate[] = transactionsReportActions.map((action, index) => ({
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${action?.childReportID ?? '-1'}`,
- value: {
- [optimisticDissmidedViolationReportActions.at(index)?.reportActionID ?? '']: 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
// parent reports than the one we set optimistically, resulting in duplicate report actions. Therefore, we send the BE
@@ -461,7 +476,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,
diff --git a/src/pages/ProcessMoneyRequestHoldPage.tsx b/src/pages/ProcessMoneyRequestHoldPage.tsx
index 8872c251be3f..893e02920375 100644
--- a/src/pages/ProcessMoneyRequestHoldPage.tsx
+++ b/src/pages/ProcessMoneyRequestHoldPage.tsx
@@ -1,22 +1,12 @@
import {useFocusEffect} from '@react-navigation/native';
-import React, {useCallback, useMemo, useRef} from 'react';
-import {InteractionManager, View} from 'react-native';
-import Button from '@components/Button';
-import HeaderPageLayout from '@components/HeaderPageLayout';
-import HoldMenuSectionList from '@components/HoldMenuSectionList';
-import Text from '@components/Text';
-import TextPill from '@components/TextPill';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
+import React, {useCallback, useRef} from 'react';
+import {InteractionManager} from 'react-native';
+import ProcessMoneyRequestHoldMenu from '@components/ProcessMoneyRequestHoldMenu';
import blurActiveElement from '@libs/Accessibility/blurActiveElement';
-import Navigation from '@libs/Navigation/Navigation';
-import * as IOU from '@userActions/IOU';
+import {dismissHoldUseExplanation} from '@userActions/IOU';
import CONST from '@src/CONST';
function ProcessMoneyRequestHoldPage() {
- const styles = useThemeStyles();
- const {translate} = useLocalize();
-
const focusTimeoutRef = useRef(null);
useFocusEffect(
useCallback(() => {
@@ -30,38 +20,14 @@ function ProcessMoneyRequestHoldPage() {
);
const onConfirm = useCallback(() => {
- IOU.dismissHoldUseExplanation();
- Navigation.goBack();
+ dismissHoldUseExplanation();
}, []);
- const footerComponent = useMemo(
- () => (
-
- ),
- [onConfirm, translate],
- );
-
return (
- Navigation.goBack()}
- testID={ProcessMoneyRequestHoldPage.displayName}
- >
-
-
- {translate('iou.holdEducationalTitle')}
- {translate('violations.hold')}
-
-
-
-
+
);
}
diff --git a/src/pages/TrackTrainingPage.tsx b/src/pages/TrackTrainingPage.tsx
index 1fc71e72ab17..de665674349e 100644
--- a/src/pages/TrackTrainingPage.tsx
+++ b/src/pages/TrackTrainingPage.tsx
@@ -1,7 +1,7 @@
import React, {useCallback} from 'react';
import FeatureTrainingModal from '@components/FeatureTrainingModal';
import useLocalize from '@hooks/useLocalize';
-import * as Link from '@userActions/Link';
+import {openExternalLink} from '@userActions/Link';
import CONST from '@src/CONST';
const VIDEO_ASPECT_RATIO = 1560 / 1280;
@@ -10,7 +10,7 @@ function TrackTrainingPage() {
const {translate} = useLocalize();
const onHelp = useCallback(() => {
- Link.openExternalLink(CONST.FEATURE_TRAINING[CONST.FEATURE_TRAINING.CONTENT_TYPES.TRACK_EXPENSE]?.LEARN_MORE_LINK);
+ openExternalLink(CONST.FEATURE_TRAINING[CONST.FEATURE_TRAINING.CONTENT_TYPES.TRACK_EXPENSE]?.LEARN_MORE_LINK);
}, []);
return (
@@ -20,7 +20,7 @@ function TrackTrainingPage() {
helpText={translate('common.learnMore')}
onHelp={onHelp}
videoURL={CONST.FEATURE_TRAINING[CONST.FEATURE_TRAINING.CONTENT_TYPES.TRACK_EXPENSE]?.VIDEO_URL}
- videoAspectRatio={VIDEO_ASPECT_RATIO}
+ illustrationAspectRatio={VIDEO_ASPECT_RATIO}
/>
);
}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index d01aef6469f4..06feb42b3fe2 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -1738,10 +1738,6 @@ const styles = (theme: ThemeColors) =>
overflow: 'hidden',
} satisfies ViewStyle),
- welcomeVideoNarrowLayout: {
- width: variables.onboardingModalWidth,
- },
-
onlyEmojisText: {
fontSize: variables.fontSizeOnlyEmojis,
lineHeight: variables.fontSizeOnlyEmojisHeight,
@@ -4846,7 +4842,7 @@ const styles = (theme: ThemeColors) =>
holdRequestInline: {
...headlineFont,
...whiteSpace.preWrap,
- color: theme.heading,
+ color: theme.textLight,
fontSize: variables.fontSizeXLarge,
lineHeight: variables.lineHeightXXLarge,
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index 56921f7dc488..5379b631a73d 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -211,6 +211,7 @@ export default {
restrictedActionIllustrationHeight: 136,
photoUploadPopoverWidth: 335,
onboardingModalWidth: 500,
+ holdEducationModalWidth: 400,
fontSizeToWidthRatio: getValueUsingPixelRatio(0.8, 1),
// Emoji related variables
@@ -244,7 +245,6 @@ export default {
cardMiniatureBorderRadius: 2,
cardNameWidth: 156,
- holdMenuIconSize: 64,
updateAnimationW: 390,
updateAnimationH: 240,
updateTextViewContainerWidth: 310,