Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
4c2fcd1
Improve RTER messaging and add submit confirmation for pending card m…
MelvinBot Mar 13, 2026
691c306
Refactor: extract hasAnyPendingRTERViolation utility function
MelvinBot Mar 13, 2026
ee6d5bb
Fix ESLint array-type error and Prettier formatting
MelvinBot Mar 13, 2026
e7fa4c9
Fix: conditionally show pending match message in moved transaction
MelvinBot Mar 13, 2026
e57101c
Fix: run prettier on zh-hans.ts to fix line length formatting
MelvinBot Mar 13, 2026
a16f7ba
Address Codex review: deduplicate RTER confirmation and mark-as-cash …
MelvinBot Mar 13, 2026
62e59dc
Fix: run Prettier on files with formatting issues
MelvinBot Mar 13, 2026
29b45ec
Fix: guard against missing transactionID instead of defaulting to emp…
MelvinBot Mar 13, 2026
5db8fc6
Remove automatic case moved transaction message changes
MelvinBot Mar 13, 2026
651b282
Fix: make hasAnyPendingRTERViolation mark-as-cash aware
MelvinBot Mar 13, 2026
c21a6f5
Merge remote-tracking branch 'origin/main' into claude-improveRterMov…
MelvinBot Mar 13, 2026
aa569dd
Merge remote-tracking branch 'origin/main' into claude-improveRterMov…
MelvinBot Mar 18, 2026
14d30ab
Refactor: extract useConfirmPendingRTERAndProceed hook to deduplicate…
MelvinBot Mar 19, 2026
9e1c88a
Merge main into claude-improveRterMovedTransactionMessage and resolve…
MelvinBot Mar 20, 2026
9c230f4
Merge remote-tracking branch 'origin/main' into claude-improveRterMov…
MelvinBot Mar 23, 2026
b6f4b19
Merge main and resolve conflicts in MoneyReportHeader and MoneyReques…
MelvinBot Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 62 additions & 42 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {InteractionManager, View} from 'react-native';
import type {ValueOf} from 'type-fest';
import useActiveAdminPolicies from '@hooks/useActiveAdminPolicies';
import useConfirmModal from '@hooks/useConfirmModal';
import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDefaultExpensePolicy from '@hooks/useDefaultExpensePolicy';
import useDeleteTransactions from '@hooks/useDeleteTransactions';
Expand Down Expand Up @@ -124,6 +125,7 @@ import {
allHavePendingRTERViolation,
getChildTransactions,
getOriginalTransactionWithSplitInfo,
hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils,
hasCustomUnitOutOfPolicyViolation as hasCustomUnitOutOfPolicyViolationTransactionUtils,
hasDuplicateTransactions,
isDistanceRequest,
Expand Down Expand Up @@ -155,7 +157,7 @@ import {
unapproveExpenseReport,
} from '@userActions/IOU';
import {setDeleteTransactionNavigateBackUrl} from '@userActions/Report';
import {markAsCash as markAsCashAction} from '@userActions/Transaction';
import {markAsCash as markAsCashAction, markPendingRTERTransactionsAsCash} from '@userActions/Transaction';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -476,6 +478,12 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
() => allHavePendingRTERViolation(transactions, violations, email ?? '', accountID, moneyRequestReport, policy),
[transactions, violations, email, accountID, moneyRequestReport, policy],
);
// Check if any transactions have pending RTER violations (for showing the submit confirmation modal)
const hasAnyPendingRTERViolation = useMemo(
() => hasAnyPendingRTERViolationTransactionUtils(transactions, allTransactionViolations, email ?? '', accountID, moneyRequestReport, policy),
[transactions, allTransactionViolations, email, accountID, moneyRequestReport, policy],
);

// Check if user should see broken connection violation warning.
const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationForMultipleTransactions(transactions, moneyRequestReport, policy, violations, email ?? '', accountID);
const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(moneyRequestReport?.reportID);
Expand Down Expand Up @@ -878,6 +886,12 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
],
);

const handleMarkPendingRTERTransactionsAsCash = useCallback(() => {
markPendingRTERTransactionsAsCash(transactions, allTransactionViolations, reportActions);
}, [transactions, allTransactionViolations, reportActions]);

const confirmPendingRTERAndProceed = useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation, handleMarkPendingRTERTransactionsAsCash);

const handleSubmitReport = useCallback(
(skipAnimation = false) => {
if (!moneyRequestReport || shouldBlockSubmit) {
Expand All @@ -887,37 +901,40 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
showDWEModal();
return;
}
submitReport({
expenseReport: moneyRequestReport,
policy,
currentUserAccountIDParam: accountID,
currentUserEmailParam: email ?? '',
hasViolations,
isASAPSubmitBetaEnabled,
expenseReportCurrentNextStepDeprecated: nextStep,
userBillingGraceEndPeriods,
amountOwed,
onSubmitted: () => {
if (skipAnimation) {
return;
}
startSubmittingAnimation();
},
ownerBillingGraceEndPeriod,
});
if (currentSearchQueryJSON && !isOffline) {
search({
searchKey: currentSearchKey,
shouldCalculateTotals,
offset: 0,
queryJSON: currentSearchQueryJSON,
isOffline,
isLoading: !!currentSearchResults?.search?.isLoading,
const doSubmit = () => {
submitReport({
expenseReport: moneyRequestReport,
policy,
currentUserAccountIDParam: accountID,
currentUserEmailParam: email ?? '',
hasViolations,
isASAPSubmitBetaEnabled,
expenseReportCurrentNextStepDeprecated: nextStep,
userBillingGraceEndPeriods,
amountOwed,
onSubmitted: () => {
if (skipAnimation) {
return;
}
startSubmittingAnimation();
},
ownerBillingGraceEndPeriod,
});
}
if (skipAnimation) {
clearSelectedTransactions(true);
}
if (currentSearchQueryJSON && !isOffline) {
search({
searchKey: currentSearchKey,
shouldCalculateTotals,
offset: 0,
queryJSON: currentSearchQueryJSON,
isOffline,
isLoading: !!currentSearchResults?.search?.isLoading,
});
}
if (skipAnimation) {
clearSelectedTransactions(true);
}
};
confirmPendingRTERAndProceed(doSubmit);
},
[
moneyRequestReport,
Expand All @@ -939,6 +956,7 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
shouldCalculateTotals,
currentSearchResults?.search?.isLoading,
clearSelectedTransactions,
confirmPendingRTERAndProceed,
ownerBillingGraceEndPeriod,
],
);
Expand Down Expand Up @@ -1743,17 +1761,19 @@ function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = fa
showDWEModal();
return;
}
submitReport({
expenseReport: moneyRequestReport,
policy,
currentUserAccountIDParam: accountID,
currentUserEmailParam: email ?? '',
hasViolations,
isASAPSubmitBetaEnabled,
expenseReportCurrentNextStepDeprecated: nextStep,
userBillingGraceEndPeriods,
amountOwed,
ownerBillingGraceEndPeriod,
confirmPendingRTERAndProceed(() => {
submitReport({
expenseReport: moneyRequestReport,
policy,
currentUserAccountIDParam: accountID,
currentUserEmailParam: email ?? '',
hasViolations,
isASAPSubmitBetaEnabled,
expenseReportCurrentNextStepDeprecated: nextStep,
userBillingGraceEndPeriods,
amountOwed,
ownerBillingGraceEndPeriod,
});
});
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {showContextMenuForReport} from '@components/ShowContextMenuContext';
import StatusBadge from '@components/StatusBadge';
import Text from '@components/Text';
import useConfirmModal from '@hooks/useConfirmModal';
import useConfirmPendingRTERAndProceed from '@hooks/useConfirmPendingRTERAndProceed';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
Expand Down Expand Up @@ -78,11 +79,12 @@ import {
import shouldAdjustScroll from '@libs/shouldAdjustScroll';
import {startSpan} from '@libs/telemetry/activeSpans';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import {hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils';
import {hasAnyPendingRTERViolation as hasAnyPendingRTERViolationTransactionUtils, hasPendingUI, isManagedCardTransaction, isPending} from '@libs/TransactionUtils';
import colors from '@styles/theme/colors';
import variables from '@styles/variables';
import {approveMoneyRequest, canIOUBePaid as canIOUBePaidIOUActions, payInvoice, payMoneyRequest, submitReport} from '@userActions/IOU';
import {openOldDotLink} from '@userActions/Link';
import {markPendingRTERTransactionsAsCash} from '@userActions/Transaction';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -203,6 +205,11 @@ function MoneyRequestReportPreviewContent({
const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector});
const isDEWBetaEnabled = isBetaEnabled(CONST.BETAS.NEW_DOT_DEW);
const hasViolations = hasViolationsReportUtils(iouReport?.reportID, transactionViolations, currentUserAccountID, currentUserEmail);
const hasAnyPendingRTERViolation = useMemo(
() => hasAnyPendingRTERViolationTransactionUtils(transactions, transactionViolations, currentUserEmail, currentUserAccountID, iouReport, policy),
[transactions, transactionViolations, currentUserEmail, currentUserAccountID, iouReport, policy],
);

const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector});

const getCanIOUBePaid = useCallback(
Expand Down Expand Up @@ -260,6 +267,12 @@ function MoneyRequestReportPreviewContent({
const {showDelegateNoAccessModal} = useDelegateNoAccessActions();
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportID}`);

const handleMarkPendingRTERTransactionsAsCash = useCallback(() => {
markPendingRTERTransactionsAsCash(transactions, transactionViolations, Object.values(reportActions ?? {}));
}, [transactions, transactionViolations, reportActions]);

const confirmPendingRTERAndProceed = useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation, handleMarkPendingRTERTransactionsAsCash);

// The submit button should be success green color only if the user is submitter and the policy does not have Scheduled Submit turned on
// Or if the report has been reopened or retracted
const isWaitingForSubmissionFromCurrentUser = useMemo(
Expand Down Expand Up @@ -765,18 +778,20 @@ function MoneyRequestReportPreviewContent({
showDEWModal();
return;
}
submitReport({
expenseReport: iouReport,
policy,
currentUserAccountIDParam: currentUserAccountID,
currentUserEmailParam: currentUserEmail,
hasViolations,
isASAPSubmitBetaEnabled,
expenseReportCurrentNextStepDeprecated: iouReportNextStep,
userBillingGraceEndPeriods,
amountOwed,
onSubmitted: startSubmittingAnimation,
ownerBillingGraceEndPeriod,
confirmPendingRTERAndProceed(() => {
submitReport({
expenseReport: iouReport,
policy,
currentUserAccountIDParam: currentUserAccountID,
currentUserEmailParam: currentUserEmail,
hasViolations,
isASAPSubmitBetaEnabled,
expenseReportCurrentNextStepDeprecated: iouReportNextStep,
userBillingGraceEndPeriods,
amountOwed,
onSubmitted: startSubmittingAnimation,
ownerBillingGraceEndPeriod,
});
});
}}
isSubmittingAnimationRunning={isSubmittingAnimationRunning}
Expand Down
36 changes: 36 additions & 0 deletions src/hooks/useConfirmPendingRTERAndProceed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {useCallback} from 'react';
import {ModalActions} from '@components/Modal/Global/ModalContext';
import useConfirmModal from '@hooks/useConfirmModal';

Check warning on line 3 in src/hooks/useConfirmPendingRTERAndProceed.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Unexpected subpath import via alias '@hooks/useConfirmModal'. Use './useConfirmModal' instead

Check warning on line 3 in src/hooks/useConfirmPendingRTERAndProceed.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Unexpected subpath import via alias '@hooks/useConfirmModal'. Use './useConfirmModal' instead
import useLocalize from '@hooks/useLocalize';

Check warning on line 4 in src/hooks/useConfirmPendingRTERAndProceed.ts

View workflow job for this annotation

GitHub Actions / ESLint check

Unexpected subpath import via alias '@hooks/useLocalize'. Use './useLocalize' instead

Check warning on line 4 in src/hooks/useConfirmPendingRTERAndProceed.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Unexpected subpath import via alias '@hooks/useLocalize'. Use './useLocalize' instead

/**
* Hook that returns a callback to confirm pending RTER violations before proceeding with an action.
* If there are pending RTER violations, a confirmation modal is shown asking the user to mark them as cash.
*/
function useConfirmPendingRTERAndProceed(hasAnyPendingRTERViolation: boolean, onMarkAsCash: () => void) {
const {showConfirmModal} = useConfirmModal();
const {translate} = useLocalize();

return useCallback(
(onProceed: () => void) => {
if (!hasAnyPendingRTERViolation) {
onProceed();
return;
}
showConfirmModal({
title: translate('iou.pendingMatchSubmitTitle'),
prompt: translate('iou.pendingMatchSubmitDescription'),
confirmText: translate('common.yes'),
cancelText: translate('common.no'),
}).then((result) => {
if (result.action === ModalActions.CONFIRM) {
onMarkAsCash();
}
onProceed();
});
},
[hasAnyPendingRTERViolation, showConfirmModal, translate, onMarkAsCash],
);
}

export default useConfirmPendingRTERAndProceed;
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,8 @@ const translations: TranslationDeepObject<typeof en> = {
pendingMatch: 'Ausstehende Zuordnung',
pendingMatchWithCreditCardDescription: 'Beleg wartet auf Abgleich mit Kartenumsatz. Als Barzahlung markieren, um abzubrechen.',
markAsCash: 'Als Bar markieren',
pendingMatchSubmitTitle: 'Bericht einreichen',
pendingMatchSubmitDescription: 'Einige Ausgaben warten auf die Zuordnung mit einer Kreditkartentransaktion. Möchten Sie sie als Bar markieren?',
routePending: 'Routing ausstehend ...',
automaticallyEnterExpenseDetails: 'Concierge wird automatisch die Ausgabendetails für Sie eingeben, oder Sie können sie manuell hinzufügen.',
receiptScanning: () => ({
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1296,6 +1296,8 @@ const translations = {
pendingMatch: 'Pending match',
pendingMatchWithCreditCardDescription: 'Receipt pending match with card transaction. Mark as cash to cancel.',
markAsCash: 'Mark as cash',
pendingMatchSubmitTitle: 'Submit report',
pendingMatchSubmitDescription: 'Some expenses are awaiting a match with a credit card transaction. Do you want to mark them as cash?',
routePending: 'Route pending...',
automaticallyEnterExpenseDetails: 'Concierge will automatically enter the expense details for you, or you can add them manually.',
receiptScanning: () => ({
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,8 @@ const translations: TranslationDeepObject<typeof en> = {
pendingMatchWithCreditCard: 'Recibo pendiente de adjuntar con la transacción de la tarjeta',
pendingMatchWithCreditCardDescription: 'Recibo pendiente de adjuntar con la transacción de la tarjeta. Márcalo como efectivo para cancelar.',
markAsCash: 'Marcar como efectivo',
pendingMatchSubmitTitle: 'Enviar informe',
pendingMatchSubmitDescription: 'Algunos gastos están pendientes de coincidencia con una transacción de tarjeta de crédito. ¿Deseas marcarlos como efectivo?',
routePending: 'Ruta pendiente...',
findExpense: 'Buscar gasto',
deletedTransaction: (amount, merchant) => `eliminó un gasto (${amount} para ${merchant})`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,8 @@ const translations: TranslationDeepObject<typeof en> = {
pendingMatch: 'Correspondance en attente',
pendingMatchWithCreditCardDescription: 'Reçu en attente de rapprochement avec une transaction par carte. Marquez comme paiement en espèces pour annuler.',
markAsCash: 'Marquer comme espèces',
pendingMatchSubmitTitle: 'Soumettre le rapport',
pendingMatchSubmitDescription: 'Certaines dépenses sont en attente de rapprochement avec une transaction par carte de crédit. Voulez-vous les marquer comme espèces ?',
routePending: 'Acheminement en attente...',
automaticallyEnterExpenseDetails: 'Concierge saisira automatiquement les détails de la dépense pour vous, ou vous pouvez les ajouter manuellement.',
receiptScanning: () => ({
Expand Down
2 changes: 2 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,8 @@ const translations: TranslationDeepObject<typeof en> = {
pendingMatch: 'Corrispondenza in sospeso',
pendingMatchWithCreditCardDescription: 'Ricevuta in attesa di abbinamento con la transazione della carta. Contrassegna come contante per annullare.',
markAsCash: 'Segna come contante',
pendingMatchSubmitTitle: 'Invia report',
pendingMatchSubmitDescription: 'Alcune spese sono in attesa di abbinamento con una transazione della carta di credito. Vuoi segnarle come contante?',
routePending: 'Instradamento in sospeso...',
automaticallyEnterExpenseDetails: 'Concierge inserirà automaticamente i dettagli della spesa per te, oppure puoi aggiungerli manualmente.',
receiptScanning: () => ({
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1230,6 +1230,8 @@ const translations: TranslationDeepObject<typeof en> = {
pendingMatch: '保留中の照合',
pendingMatchWithCreditCardDescription: 'レシートはカード取引との照合待ちです。現金としてマークしてキャンセルします。',
markAsCash: '現金としてマーク',
pendingMatchSubmitTitle: 'レポートを提出',
pendingMatchSubmitDescription: '一部の経費がクレジットカード取引との照合待ちです。現金としてマークしますか?',
routePending: 'ルート保留中…',
automaticallyEnterExpenseDetails: 'コンシェルジュが自動的に経費の詳細を入力するか、手動で追加することができます。',
receiptScanning: () => ({
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,8 @@ const translations: TranslationDeepObject<typeof en> = {
pendingMatch: 'Overeenkomst in behandeling',
pendingMatchWithCreditCardDescription: 'Bon wordt nog gekoppeld aan kaarttransactie. Markeer als contant om te annuleren.',
markAsCash: 'Markeren als contant',
pendingMatchSubmitTitle: 'Rapport indienen',
pendingMatchSubmitDescription: 'Sommige uitgaven wachten op koppeling met een creditcardtransactie. Wilt u ze als contant markeren?',
routePending: 'Routeren in behandeling...',
automaticallyEnterExpenseDetails: 'Concierge zal automatisch de uitgavendetails voor je invoeren, of je kunt ze handmatig toevoegen.',
receiptScanning: () => ({
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1246,6 +1246,8 @@ const translations: TranslationDeepObject<typeof en> = {
pendingMatch: 'Oczekujące dopasowanie',
pendingMatchWithCreditCardDescription: 'Oczekuje na dopasowanie paragonu do transakcji kartą. Oznacz jako gotówkę, aby anulować.',
markAsCash: 'Oznacz jako gotówkę',
pendingMatchSubmitTitle: 'Wyślij raport',
pendingMatchSubmitDescription: 'Niektóre wydatki oczekują na dopasowanie z transakcją kartą kredytową. Czy chcesz oznaczyć je jako gotówkę?',
routePending: 'Trasa w toku…',
automaticallyEnterExpenseDetails: 'Concierge automatycznie wprowadzi szczegóły wydatku za Ciebie lub możesz dodać je ręcznie.',
receiptScanning: () => ({
Expand Down
Loading
Loading