Skip to content

Show required-field validation summary above expense confirmation footer - #106450

Open
MelvinBot wants to merge 8 commits into
mainfrom
claude-requiredFieldFooterSummary
Open

MelvinBot wants to merge 8 commits into
mainfrom
claude-requiredFieldFooterSummary

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

When the amount, date or merchant blocked Create, the error only showed under that field. If the field was scrolled out of view, pressing Create looked like it did nothing.

Now a message shows above the button after a blocked press: "Please enter the required fields." when something is missing, or "Please fix the errors in the form before continuing." when nothing is missing but a value is invalid. The errors under the fields stay, and the message goes away once everything is fixed.

To keep that message correct, these three fields no longer share a single error. Their errors are worked out from the current values, and that one result blocks Create, shows the field errors and picks the message.

This changes a few things for users:

  • Entering only the amount no longer clears the merchant's "This field is required" while the merchant is still blank. That was a bug on main, and the new message would have disappeared with it.
  • Field errors stay when you open another page, like Category, and come back.
  • A blank amount to a person says "This field is required". A typed 0 still says "Invalid amount".
  • A blank required merchant says "This field is required" on every expense, including edited splits. "Please enter a valid merchant" now only shows for something the user typed.
  • A typed (none) when editing a split to a person is now flagged as invalid, like on other expenses.
  • When scanning several receipts, Create now checks all the fields on every receipt. The receipt on screen comes first, then Create moves you to the next receipt with an error.

The "fix the errors" line is now one translated sentence, and the two distance pages use it too.

Fixed Issues

$ #106237
PROPOSAL: #106237 (comment)

Tests

**Prerequisite: **

  • Use a Control workspace. In More features, turn on Taxes and Rules. Then in Rules, turn on Attendee tracking, so the expense form is long enough to scroll.
  • Have two receipt images ready, plus one blank image for Test 7.
  • Run these on Web, mobile Web, iOS and Android.
  • "Open the report form" means: go to Spend > Reports, open a draft report on that workspace, then click More > Add expense > Create expense.

Test 1: Missing amount on a scanned receipt

  1. Open the report form and select Scan.
  2. Upload a receipt and click Show more.
  3. Enter a Merchant and pick a Date. Leave Amount empty.
  4. Scroll to the bottom of the form and click Create expense.
  5. Verify the expense is not created and "Please enter the required fields." appears directly above the button.
  6. Scroll up and verify "This field is required" appears under Amount.
  7. Enter an Amount and verify both messages disappear.
  8. Click Create expense and verify the expense is added to the report.

Test 2: Scanned receipt with nothing typed in

  1. Open the report form, select Scan and upload a receipt.
  2. Click Show more, change nothing and click Create expense.
  3. Verify no error appears and the expense is added to the report.

Test 3: Missing fields on a manual expense

  1. Open the report form and select Manual.
  2. Leave Amount and Merchant empty, scroll to the bottom and click Create expense.
  3. Verify "This field is required" appears under both fields and "Please enter the required fields." appears above the button.
  4. Enter only an Amount. Verify the Amount error disappears, while the Merchant error and the message above the button stay.
  5. Click Create expense and verify the expense is not created.
  6. Enter a Merchant and verify all the errors disappear.
  7. Click the X on the Date field, then click Create expense.
  8. Verify "This field is required" appears under Date and "Please enter the required fields." appears above the button.
  9. Pick a Date and verify both messages disappear.

Test 4: Invalid merchant, and leaving the page

  1. On the same form, type "(none)" as the Merchant and click Create expense.
  2. Verify "Please enter a valid merchant" appears under Merchant and "Please fix the errors in the form before continuing." appears above the button.
  3. Paste more than 255 characters into Merchant. Verify "Character limit exceeded" appears and the message above the button stays the same.
  4. Clear the Merchant. Verify it says "This field is required" and the message above the button changes to "Please enter the required fields."
  5. Click Category, pick any category and return to the form. Verify the error and the message are still there.
  6. Enter a normal Merchant and verify all the errors disappear.
  7. Click Create expense and verify the expense is added to the report.

Test 5: Expense to a person

  1. Go to Inbox and click the main + button (bottom left on a computer, bottom right on a phone), then Create expense, then Manual.
  2. Click the recipient at the top of the form and choose a person.
  3. Leave Amount empty and click Create expense.
  4. Verify "This field is required" appears under Amount, "Please enter the required fields." appears above the button, and Merchant shows no error.
  5. Enter 0 as the Amount and click Create expense.
  6. Verify "Invalid amount" appears under Amount and "Please fix the errors in the form before continuing." appears above the button.
  7. Enter 5 as the Amount and verify both messages disappear.

Test 6: Several receipts

  1. Open the report form and select Scan.
  2. Add two receipts. On a computer, click Choose files and select both images. On a phone, turn on the multi-scan button next to the shutter, take two photos and tap the arrow button.
  3. Verify the page shows "1 of 2" and the button says "Create 2 expenses".
  4. Click Show more. Enter an Amount, type "(none)" as the Merchant and pick a Date.
  5. Use the arrows at the top to go to "2 of 2". Click Show more and enter only an Amount.
  6. Click Create 2 expenses.
  7. Verify the page stays on "2 of 2", "This field is required" appears under Merchant and Date, and "Please enter the required fields." appears above the button.
  8. Enter a Merchant and pick a Date. Verify the field errors disappear and the message above the button changes to "Please fix the errors in the form before continuing."
  9. Click Create 2 expenses.
  10. Verify nothing is created, the page moves to "1 of 2" and "Please enter a valid merchant" appears under Merchant.
  11. Enter a normal Merchant and verify both messages disappear.
  12. Click Create 2 expenses and verify both expenses are added to the report.

Test 7: Editing a split whose receipt could not be read

  1. Open a group chat with at least two other people.
  2. Click the + in the message box, then Split expense, then Scan.
  3. Upload the blank image and click Split expense.
  4. When the card in the chat shows "Missing amount", click it.
  5. Click Show more. Enter an Amount, type "(none)" as the Merchant and click Split expense.
  6. Verify "Please enter a valid merchant" appears under Merchant and "Please fix the errors in the form before continuing." appears above the button.
  7. Enter a normal Merchant and verify both messages disappear.
  8. Click Split expense and verify the chat shows the split with that merchant.

Test 8: Normal distance expense

  1. Open a draft report on the workspace and click More > Add expense > Track distance.
  2. Select Manual, enter 5 and click Next.
  3. Click Create expense and verify no error appears and the expense is added to the report.
  • Verify that no errors appear in the JS console

Offline tests

Same as tests

QA Steps

Same as tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

@MelvinBot
MelvinBot requested a review from a team November 10, 2026 06:26
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Nov 10, 2026
@MelvinBot

This comment has been minimized.

@OSBotify

This comment has been minimized.

@MelvinBot

This comment has been minimized.

@MelvinBot

This comment has been minimized.

@TaduJR

This comment was marked as resolved.

…anges

Co-authored-by: TaduJR <TaduJR@users.noreply.github.com>
@MelvinBot
MelvinBot force-pushed the claude-requiredFieldFooterSummary branch from f709f14 to ed00b6f Compare November 10, 2026 11:10
@MelvinBot

This comment has been minimized.

@OSBotify

Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 22213bec..89f21780 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -237,7 +237,7 @@ const translations: TranslationDeepObject<typeof en> = {
             phoneNumber: `Bitte gib eine vollständige Telefonnummer ein
 (z. B. ${CONST.FORMATTED_EXAMPLE_PHONE_NUMBER})`,
             fieldRequired: 'Dieses Feld ist erforderlich',
-            fixErrorsBeforeContinuing: 'Bitte behebe die Fehler im Formular, bevor du fortfährst.',
+            fixErrorsBeforeContinuing: 'Bitte beheben Sie die Fehler im Formular, bevor Sie fortfahren.',
             requestModified: 'Diese Anfrage wird gerade von einem anderen Mitglied bearbeitet',
             characterLimitExceedCounter: (length: number, limit: number) => `Zeichenlimit überschritten (${length}/${limit})`,
             dateInvalid: 'Bitte wähle ein gültiges Datum aus',
@@ -1756,7 +1756,7 @@ const translations: TranslationDeepObject<typeof en> = {
             atLeastTwoDifferentWaypoints: 'Bitte gib mindestens zwei verschiedene Adressen ein',
             splitExpenseMultipleParticipantsErrorMessage: 'Eine Ausgabe kann nicht zwischen einem Workspace und anderen Mitgliedern aufgeteilt werden. Bitte aktualisiere deine Auswahl.',
             invalidMerchant: 'Bitte gib einen gültigen Händler ein',
-            enterRequiredFields: 'Bitte fülle die Pflichtfelder aus.',
+            enterRequiredFields: 'Bitte füllen Sie die erforderlichen Felder aus.',
             atLeastOneAttendee: 'Mindestens eine teilnehmende Person muss ausgewählt werden',
             invalidQuantity: 'Bitte gib eine gültige Menge ein',
             quantityGreaterThanZero: 'Die Menge muss größer als null sein',
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 4de669a7..d46f708d 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -241,7 +241,7 @@ const translations: TranslationDeepObject<typeof en> = {
             phoneNumber: `Παρακαλούμε εισαγάγετε έναν πλήρη αριθμό τηλεφώνου
 (π.χ. ${CONST.FORMATTED_EXAMPLE_PHONE_NUMBER})`,
             fieldRequired: 'Αυτό το πεδίο είναι υποχρεωτικό',
-            fixErrorsBeforeContinuing: 'Παρακαλώ διορθώστε τα σφάλματα στη φόρμα πριν συνεχίσετε.',
+            fixErrorsBeforeContinuing: 'Παρακαλούμε διορθώστε τα σφάλματα στη φόρμα πριν συνεχίσετε.',
             requestModified: 'Αυτό το αίτημα τροποποιείται από άλλο μέλος',
             characterLimitExceedCounter: (length: number, limit: number) => `Υπέρβαση ορίου χαρακτήρων (${length}/${limit})`,
             dateInvalid: 'Παρακαλώ επιλέξτε έγκυρη ημερομηνία',
@@ -1824,7 +1824,7 @@ const translations: TranslationDeepObject<typeof en> = {
             atLeastTwoDifferentWaypoints: 'Παρακαλούμε εισαγάγετε τουλάχιστον δύο διαφορετικές διευθύνσεις',
             splitExpenseMultipleParticipantsErrorMessage: 'Μια δαπάνη δεν μπορεί να κατανεμηθεί μεταξύ ενός χώρου εργασίας και άλλων μελών. Παρακαλούμε ενημερώστε την επιλογή σας.',
             invalidMerchant: 'Παρακαλούμε εισαγάγετε έγκυρο έμπορο',
-            enterRequiredFields: 'Παρακαλούμε συμπληρώστε τα υποχρεωτικά πεδία.',
+            enterRequiredFields: 'Παρακαλούμε εισαγάγετε τα απαιτούμενα πεδία.',
             atLeastOneAttendee: 'Πρέπει να επιλεγεί τουλάχιστον ένας συμμετέχων',
             invalidQuantity: 'Παρακαλούμε εισαγάγετε μια έγκυρη ποσότητα',
             quantityGreaterThanZero: 'Η ποσότητα πρέπει να είναι μεγαλύτερη από το μηδέν',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 5247d6c6..ebea2b85 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -228,7 +228,7 @@ const translations: TranslationDeepObject<typeof en> = {
             acceptTerms: 'Debes aceptar los Términos de Servicio para continuar',
             phoneNumber: `Por favor, introduce un número de teléfono completo\n(ej. ${CONST.FORMATTED_EXAMPLE_PHONE_NUMBER})`,
             fieldRequired: 'Este campo es obligatorio',
-            fixErrorsBeforeContinuing: 'Por favor, corrige los errores en el formulario antes de continuar.',
+            fixErrorsBeforeContinuing: 'Por favor, corrige los errores del formulario antes de continuar.',
             requestModified: 'Esta solicitud está siendo modificada por otro miembro',
             characterLimitExceedCounter: (length, limit) => `Se superó el límite de caracteres (${length}/${limit})`,
             dateInvalid: 'Por favor, selecciona una fecha válida',
@@ -1762,7 +1762,7 @@ const translations: TranslationDeepObject<typeof en> = {
             atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes',
             splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con miembros individuales. Por favor, actualiza tu selección.',
             invalidMerchant: 'Por favor, introduce un comerciante válido',
-            enterRequiredFields: 'Por favor, completa los campos obligatorios.',
+            enterRequiredFields: 'Por favor, introduce los campos obligatorios.',
             atLeastOneAttendee: 'Debe seleccionarse al menos un asistente',
             invalidQuantity: 'Por favor, introduce una cantidad válida',
             quantityGreaterThanZero: 'La cantidad debe ser mayor que cero',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index a3832449..0baeef38 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -1764,7 +1764,7 @@ const translations: TranslationDeepObject<typeof en> = {
             atLeastTwoDifferentWaypoints: 'Veuillez saisir au moins deux adresses différentes',
             splitExpenseMultipleParticipantsErrorMessage: "Une dépense ne peut pas être répartie entre un espace de travail et d'autres membres. Veuillez modifier votre sélection.",
             invalidMerchant: 'Veuillez saisir un commerçant valide',
-            enterRequiredFields: 'Veuillez remplir les champs obligatoires.',
+            enterRequiredFields: 'Veuillez renseigner les champs obligatoires.',
             atLeastOneAttendee: 'Au moins un participant doit être sélectionné',
             invalidQuantity: 'Veuillez saisir une quantité valide',
             quantityGreaterThanZero: 'La quantité doit être supérieure à zéro',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 521bb9ab..97c933fa 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -237,7 +237,7 @@ const translations: TranslationDeepObject<typeof en> = {
             phoneNumber: `Inserisci un numero di telefono completo
 (ad es. ${CONST.FORMATTED_EXAMPLE_PHONE_NUMBER})`,
             fieldRequired: 'Questo campo è obbligatorio',
-            fixErrorsBeforeContinuing: 'Per favore, correggi gli errori nel modulo prima di continuare.',
+            fixErrorsBeforeContinuing: 'Correggi gli errori nel modulo prima di continuare.',
             requestModified: 'Questa richiesta viene modificata da un altro membro',
             characterLimitExceedCounter: (length: number, limit: number) => `Limite di caratteri superato (${length}/${limit})`,
             dateInvalid: 'Seleziona una data valida',
@@ -1754,7 +1754,7 @@ const translations: TranslationDeepObject<typeof en> = {
             atLeastTwoDifferentWaypoints: 'Inserisci almeno due indirizzi diversi',
             splitExpenseMultipleParticipantsErrorMessage: 'Una spesa non può essere suddivisa tra uno spazio di lavoro e altri membri. Aggiorna la tua selezione.',
             invalidMerchant: 'Inserisci un esercente valido',
-            enterRequiredFields: 'Compila i campi obbligatori.',
+            enterRequiredFields: 'Inserisci i campi obbligatori.',
             atLeastOneAttendee: 'Devi selezionare almeno un partecipante',
             invalidQuantity: 'Inserisci una quantità valida',
             quantityGreaterThanZero: 'La quantità deve essere maggiore di zero',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 140b1eb7..37706b88 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -237,7 +237,7 @@ const translations: TranslationDeepObject<typeof en> = {
             phoneNumber: `Voer een volledig telefoonnummer in
 (bijv. ${CONST.FORMATTED_EXAMPLE_PHONE_NUMBER})`,
             fieldRequired: 'Dit veld is verplicht',
-            fixErrorsBeforeContinuing: 'Corrigeer de fouten in het formulier voordat je verdergaat.',
+            fixErrorsBeforeContinuing: 'Los de fouten in het formulier op voordat je verdergaat.',
             requestModified: 'Dit verzoek wordt bewerkt door een ander lid',
             characterLimitExceedCounter: (length: number, limit: number) => `Limiet voor aantal tekens overschreden (${length}/${limit})`,
             dateInvalid: 'Selecteer een geldige datum',
@@ -1750,7 +1750,7 @@ const translations: TranslationDeepObject<typeof en> = {
             atLeastTwoDifferentWaypoints: 'Voer minimaal twee verschillende adressen in',
             splitExpenseMultipleParticipantsErrorMessage: 'Een uitgave kan niet worden opgesplitst tussen een werkruimte en andere leden. Werk je selectie alsjeblieft bij.',
             invalidMerchant: 'Voer een geldige leverancier in',
-            enterRequiredFields: 'Vul de verplichte velden in.',
+            enterRequiredFields: 'Vul alsjeblieft de verplichte velden in.',
             atLeastOneAttendee: 'Er moet ten minste één deelnemer worden geselecteerd',
             invalidQuantity: 'Voer een geldige hoeveelheid in',
             quantityGreaterThanZero: 'Hoeveelheid moet groter zijn dan nul',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index a89e9c77..63787be4 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -243,7 +243,7 @@ const translations: TranslationDeepObject<typeof en> = {
             phoneNumber: `Proszę podać pełny numer telefonu
 (np. ${CONST.FORMATTED_EXAMPLE_PHONE_NUMBER})`,
             fieldRequired: 'To pole jest wymagane',
-            fixErrorsBeforeContinuing: 'Napraw błędy w formularzu przed kontynuowaniem.',
+            fixErrorsBeforeContinuing: 'Popraw błędy w formularzu przed kontynuowaniem.',
             requestModified: 'Inny członek właśnie modyfikuje to żądanie',
             characterLimitExceedCounter: (length: number, limit: number) => `Przekroczono limit znaków (${length}/${limit})`,
             dateInvalid: 'Wybierz prawidłową datę',
@@ -1790,7 +1790,7 @@ const translations: TranslationDeepObject<typeof en> = {
             atLeastTwoDifferentWaypoints: 'Wprowadź co najmniej dwa różne adresy',
             splitExpenseMultipleParticipantsErrorMessage: 'Wydatek nie może być podzielony między przestrzeń roboczą a innych członków. Zaktualizuj swój wybór.',
             invalidMerchant: 'Wprowadź prawidłowego sprzedawcę',
-            enterRequiredFields: 'Wypełnij wymagane pola.',
+            enterRequiredFields: 'Wpisz wymagane pola.',
             atLeastOneAttendee: 'Co najmniej jeden uczestnik musi zostać wybrany',
             invalidQuantity: 'Wprowadź prawidłową ilość',
             quantityGreaterThanZero: 'Ilość musi być większa niż zero',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index c420680d..efe1f0a1 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -237,7 +237,7 @@ const translations: TranslationDeepObject<typeof en> = {
             phoneNumber: `Insira um número de telefone completo
 (p.ex., ${CONST.FORMATTED_EXAMPLE_PHONE_NUMBER})`,
             fieldRequired: 'Este campo é obrigatório',
-            fixErrorsBeforeContinuing: 'Por favor, corrija os erros no formulário antes de continuar.',
+            fixErrorsBeforeContinuing: 'Corrija os erros no formulário antes de continuar.',
             requestModified: 'Esta solicitação está sendo modificada por outra pessoa',
             characterLimitExceedCounter: (length: number, limit: number) => `Limite de caracteres excedido (${length}/${limit})`,
             dateInvalid: 'Selecione uma data válida',
@@ -1751,7 +1751,7 @@ const translations: TranslationDeepObject<typeof en> = {
             atLeastTwoDifferentWaypoints: 'Insira pelo menos dois endereços diferentes',
             splitExpenseMultipleParticipantsErrorMessage: 'Uma despesa não pode ser dividida entre um espaço de trabalho e outros membros. Atualize sua seleção.',
             invalidMerchant: 'Insira um comerciante válido',
-            enterRequiredFields: 'Preencha os campos obrigatórios.',
+            enterRequiredFields: 'Digite os campos obrigatórios.',
             atLeastOneAttendee: 'Pelo menos um participante deve ser selecionado',
             invalidQuantity: 'Insira uma quantidade válida',
             quantityGreaterThanZero: 'A quantidade deve ser maior que zero',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index d969991e..52b7e30f 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1687,7 +1687,7 @@ const translations: TranslationDeepObject<typeof en> = {
             atLeastTwoDifferentWaypoints: '请输入至少两个不同的地址',
             splitExpenseMultipleParticipantsErrorMessage: '无法将一笔报销同时分摊到一个工作区和其他成员。请更新你的选择。',
             invalidMerchant: '请输入有效的商家',
-            enterRequiredFields: '请填写必填字段。',
+            enterRequiredFields: '请输入必填字段。',
             atLeastOneAttendee: '必须选择至少一位参加者',
             invalidQuantity: '请输入有效的数量',
             quantityGreaterThanZero: '数量必须大于零',

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@MelvinBot

This comment has been minimized.

@kacper-mikolajczak

Copy link
Copy Markdown
Contributor

Hi! We are aware Melvin tester runs are failing. Please if you could update this branch to latest main and re-run it - this should resolve the issue. If not, reach out to me directly. Thanks for your patience!

@TaduJR

This comment was marked as resolved.

Co-authored-by: TaduJR <TaduJR@users.noreply.github.com>
@MelvinBot

This comment has been minimized.

@TaduJR

This comment was marked as resolved.

@TaduJR

This comment was marked as resolved.

@chatgpt-codex-connector

This comment has been minimized.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Co-authored-by: TaduJR <TaduJR@users.noreply.github.com>
@MelvinBot

This comment has been minimized.

@MelvinBot

This comment has been minimized.

@TaduJR

This comment was marked as resolved.

Co-authored-by: TaduJR <TaduJR@users.noreply.github.com>
@MelvinBot

This comment has been minimized.

@MelvinBot

This comment has been minimized.

@TaduJR

This comment was marked as resolved.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Marked the PR as ready for review.


view run

@melvin-bot
melvin-bot Bot removed the request for review from a team November 11, 2026 07:45
@melvin-bot

melvin-bot Bot commented Nov 11, 2026

Copy link
Copy Markdown

@TaduJR Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot requested review from heyjennahay and removed request for a team November 11, 2026 07:45

@TaduJR TaduJR left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@melvin-bot
melvin-bot Bot requested a review from rlinoz November 11, 2026 07:50
@rlinoz

rlinoz commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

@TaduJR @MelvinBot correct me if I wrong, but this became a big change because we are trying to not clear the error when we have more errors to fix?

  1. User has 3 errors, clicks create
  2. We display all errors, including the footer
  3. User fixes 1 error
  4. footer error keeps being displayed (and this bit is what needs so many changes?)

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Yes—your sequence is correct for the amount/date/merchant errors, but step 4 doesn’t inherently require this whole refactor.

The central design choice was to derive those errors together, so submission blocking, inline errors, and the footer agree. Fixing one field removes only its error; the footer remains until none are left (implementation).

The PR also includes additional behavior fixes:

  • Keeping revealed errors when navigating away and back.
  • Distinguishing missing values from invalid typed values.
  • Handling typed merchants consistently in edited splits.
  • Checking every receipt in a multi-scan before submitting.

So partial correction motivated the shared-error-map approach, but the additional fixes explain the broader scope. A narrower footer-only change was possible; this implementation chose to unify those validation paths rather than only expose the existing error string.


view run

@rlinoz

rlinoz commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot do we have this pattern somewhere? Or do we usually hide the footer error after the first fix and wait until next submission?

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Yes—we already have this pattern, but custom forms aren’t consistent. Based on the source:

  • Shared FormProvider: validation runs on change by default, and the footer alert remains visible while errors remain. After a failed submit, correcting one field revalidates the form rather than hiding the summary until resubmission (alert logic, change handling).
  • Personal expense rules: also keep the footer visible after partial correction, updating its message to the remaining problem (validation).
  • Counterexample—workspace spend rules: explicitly clear the footer when opening a field editor, then show it again on the next invalid submission (implementation).

So persistent feedback matches the shared-form default, while hide-until-resubmit also exists in custom flows. This supports the behavior chosen here, but doesn’t establish that this particular refactor—or its full scope—is necessary.


view run

rlinoz

This comment was marked as resolved.

@trjExpensify
trjExpensify requested review from trjExpensify and removed request for heyjennahay November 11, 2026 22:36

@trjExpensify trjExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To recap the problem we were solving here:

  1. The user had more fields than the available scroll view.
  2. They scroll down and click "Create expense"
  3. The field above the scroll view gets an error
  4. The user doesn't see it and thinks the "Create expense" button is unresponsive.

@Expensify/design this PR seems to add back the error above the footer button at all times - but that was removed at some point I think, maybe duplicity? Are we okay with that, or do we only want the footer error to show there when a field in error is above the scroll view?

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

We probably removed it in the footer button because we usually don't show the footer error if there is only one error on screen. I think I would ultimately prefer it to scroll to the error rather than add another error because then there's technically two errors on screen which feels unnecessary. ScrollTo behaviors can be a bit iffy though, so if we're worried about that, then I think the footer button makes sense.

do we only want the footer error to show there when a field in error is above the scroll view?

I think I'd prefer scrollTo over this since then we're already doing viewport detection stuff.

@trjExpensify

Copy link
Copy Markdown
Contributor

Yeah, this was reported and observed on desktop as well.

@TaduJR

TaduJR commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot please sync this PR with my branch again, and change nothing else.

  1. Merge the latest main into this branch.
  2. Apply exactly the src and tests changes from TaduJR/App@63cc781...e9d4871 (the same as git diff 63cc7818eba e9d487197c2 -- src tests). It has two commits:
    • the test cleanup from rlinoz's review;
    • the design change: a blocked Create now scrolls to the first field that needs fixing, instead of showing a message above the button.
  3. Don't regenerate translations. The PR no longer adds any translation key, so all 11 files in src/languages/, IOURequestStepDistanceMap.tsx and DynamicIOURequestStepDistance.tsx should end up identical to main.
  4. Check: git diff e9d487197c2 HEAD -- src tests should only show changes from main commits newer than 6813364a26b.
  5. Leave the PR description as it is. I'll update it separately.

Also mark this comment as resolved and hide it #106450 (review)

MelvinBot and others added 2 commits November 12, 2026 19:17
Co-authored-by: TaduJR <TaduJR@users.noreply.github.com>
Co-authored-by: TaduJR <TaduJR@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner November 12, 2026 19:24
@melvin-bot
melvin-bot Bot requested review from TaduJR and removed request for a team November 12, 2026 19:24
@melvin-bot

melvin-bot Bot commented Nov 12, 2026

Copy link
Copy Markdown

@TaduJR Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@MelvinBot

MelvinBot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Synced and pushed c250bf3, without force-pushing.

  • Merged main and applied your exact diff.
  • Verified differences from e9d487197c2 are only newer main changes.
  • All 11 locale files and both distance pages match main; no translations regenerated.
  • Description unchanged.
  • Resolved the linked review’s 15 threads and hid the review.

454 targeted tests and typecheck passed. Full-suite and final lint rerun timed out; Web verification was blocked by a blank page.


view run · no recording available

@codecov

codecov Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

@TaduJR

TaduJR commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

@MelvinBot please sync this PR with my branch again, and change nothing else.

  1. Merge the latest main.
  2. Apply the src and tests changes from these commits, in order (skip my main merge 344b8a6e328):
  3. Check: git diff ea35526f32c HEAD -- src tests should only show changes from main commits newer than 32d3fefb8ef.
  4. Don't regenerate translations, and leave the PR description as it is.

Co-authored-by: TaduJR <TaduJR@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Synced and pushed 31e2e70.

  • Merged main, applied all six patches in order, and skipped 344b8a6e328.
  • Verified src/tests exactly match ea35526f32c.
  • No translations regenerated; PR description unchanged.

39,519 tests passed, along with typecheck, lint, React Compiler, and Storybook. Web verification was unavailable because no authorized live session was available.


view run

@TaduJR

TaduJR commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Please replace the description with the below one

Description

Explanation of Change

When Create was blocked by a missing or invalid amount, merchant or date, the error only showed under that field. If the user had scrolled past it, pressing Create looked like it did nothing.

Now a blocked Create scrolls the list to the first field that needs fixing (amount, then merchant, then date) so its error is in view. If the field is already on screen, the list stays put. If the field is taller than the visible area, the list scrolls to its bottom, where the error is. The field isn't focused, so the keyboard and the date picker don't pop open. Design preferred this over adding a second error above the button: #106450 (comment)

To make this reliable, the amount, merchant and date errors are now worked out from the expense's current values instead of being saved and cleared in several places. That also fixes a few cases where errors disappeared or read wrong:

  • Entering only the amount no longer clears the merchant's "This field is required" while the merchant is still empty.
  • Field errors stay after opening another page, like Category, and coming back, until they're fixed.
  • A blank amount on an expense to a person says "This field is required" instead of "Invalid amount". A typed $0 still says "Invalid amount".
  • A blank required merchant says "This field is required" everywhere, including when editing a split. "Please enter a valid merchant" only shows for something the user typed.

When scanning several receipts:

  • Create checks the amount, merchant and date of every receipt, since it submits all of them. Before, an invalid value on another receipt could be submitted. Now Create switches to that receipt and scrolls to the field. The receipt on screen is always fixed first.
  • In the compact layout, missing attendees or an invalid tax amount now opens "Show more", so the error is visible.

Fixed Issues

$ #106237
PROPOSAL: #106237 (comment)

Tests

**Prerequisite: **

  • Use a Control workspace with Taxes and Rules turned on in More features. In Rules > General, turn on Attendee tracking and set "Require fields for all expenses" to Category, so the form is long enough to scroll.
  • Have two receipt images and one blank image ready.
    ]- "Open the report form" means: go to Spend > Reports, open a draft report on the workspace (or click Create report), then click More > Add expense > Create expense.
  • If the whole form fits on your screen, it won't scroll. The error should still show under the field.

Test 1: Missing amount on a scanned receipt

  1. Open the report form, select Scan, upload a receipt and click Show more.
  2. Enter a Merchant and pick a Date. Leave Amount empty.
  3. Scroll to the bottom and click Create expense.
  4. Verify nothing is created and the form scrolls up to Amount, with "This field is required" under it.
  5. Enter an Amount, click Create expense and verify the expense is added to the report.

Test 2: Scanned receipt with nothing typed in

  1. Open the report form, select Scan, upload a receipt, click Show more and click Create expense without changing anything.
  2. Verify no error appears and the expense is added to the report.

Test 3: Missing fields on a manual expense

  1. Open the report form and select Manual. Leave Amount and Merchant empty, scroll to the bottom and click Create expense.
  2. Verify the form scrolls up to Amount and "This field is required" appears under Amount and Merchant.
  3. Click Create expense again and verify the form doesn't move.
  4. Enter only an Amount, scroll to the bottom and click Create expense. Verify the form scrolls to Merchant and only Merchant shows the error.
  5. Enter a Merchant, click the X on Date and click Create expense. Verify "This field is required" appears under Date.
  6. Pick a Date and verify the error disappears.

Test 4: Invalid merchant, and leaving the page

  1. On the same form, type "(none)" as the Merchant and click Create expense. Verify "Please enter a valid merchant" appears.
  2. Paste more than 255 characters into Merchant. Verify "Character limit exceeded" appears.
  3. Clear the Merchant and click Create expense. Verify "This field is required" appears.
  4. Pick any Category and return to the form. Verify the error is still there.
  5. Enter a normal Merchant, click Create expense and verify the expense is added to the report.

Test 5: Expense to a person

  1. Click the + button (at the bottom of the left menu on a computer, at the bottom right on a phone), then Create expense, then Manual.
  2. Click the recipient at the top of the form and choose a person.
  3. Leave Amount empty and click Create expense. Verify "This field is required" appears under Amount and not under Merchant.
  4. Enter 0 and click Create expense. Verify "Invalid amount" appears.
  5. Enter 5 and verify the error disappears.

Test 6: Several receipts

  1. Open the report form, select Scan and add two receipts. On a computer, click Choose files and select both. On a phone, turn on multi-scan next to the shutter and take two photos, or pick two from the gallery. Verify the page shows "1 of 2" and the button says "Create 2 expenses".
  2. Click Show more. Enter an Amount, type "(none)" as the Merchant and pick a Date.
  3. Use the arrows at the top to go to "2 of 2". Click Show more and enter only an Amount.
  4. Scroll to the bottom and click Create 2 expenses. Verify the page stays on "2 of 2", scrolls to Merchant, and "This field is required" appears under Merchant and Date.
  5. Enter a Merchant, pick a Date and click Create 2 expenses. Verify nothing is created, the page moves to "1 of 2" and "Please enter a valid merchant" appears.
  6. Enter a normal Merchant, click Create 2 expenses and verify both expenses are added to the report.

Test 7: Editing a split whose receipt could not be read

  1. In a group chat with at least two other people, click the + in the message box, then Split expense, then Scan. Upload the blank image and click Split expense.
  2. When the card in the chat shows "Missing amount", click it, scroll down and click Show more.
  3. Enter an Amount, type "(none)" as the Merchant and click Split expense. Verify "Please enter a valid merchant" appears.
  4. Enter a normal Merchant, click Split expense and verify the chat shows the split.

Test 8: Normal distance expense

  1. Open a draft report and click More > Add expense > Track distance. Select Manual, enter 5 and click Next.
  2. Click Create expense and verify the expense is added with no error.
  • Verify that no errors appear in the JS console

Offline tests

Same as tests

QA Steps

Same as tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants