From 0adf45f0b3122e9b8d0ddfa899b52658030b491b Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Thu, 6 Aug 2026 18:57:11 +0530 Subject: [PATCH 1/8] Add travelCodingSync beta Gates the Travel booking-coding sync feature. No consumer yet; the Travel settings toggle it gates lands in a follow-up. Co-Authored-By: Claude Opus 5 (1M context) --- src/CONST/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d9669c18a459..05a321003e57 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1010,6 +1010,7 @@ const CONST = { GLOBAL_REIMBURSEMENT_FX: 'globalReimbursementFX', DEFAULT_LETTER_AVATARS: 'defaultLetterAvatars', NETSUITE_OAUTH: 'netSuiteOAuth', + TRAVEL_CODING_SYNC: 'travelCodingSync', }, BUTTON_STATES: { DEFAULT: 'default', From 2d4ddbcc04eda4d377445784b7634f9daf546d54 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 14 Aug 2026 20:21:38 +0530 Subject: [PATCH 2/8] Add the Travel coding sync toggle to workspace Travel settings --- src/languages/de.ts | 4 ++++ src/languages/el.ts | 5 +++++ src/languages/en.ts | 4 ++++ src/languages/es.ts | 5 +++++ src/languages/fr.ts | 5 +++++ src/languages/it.ts | 4 ++++ src/languages/ja.ts | 8 ++++++- src/languages/nl.ts | 4 ++++ src/languages/pl.ts | 4 ++++ src/languages/pt-BR.ts | 4 ++++ src/languages/zh-hans.ts | 5 ++++- .../workspace/travel/BookOrManageYourTrip.tsx | 22 +++++++++++++++++++ src/types/onyx/TravelSettings.ts | 3 +++ 13 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index cf8dacb2f0de..b829093de5df 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6133,6 +6133,10 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU bookOrManageYourTrip: {title: 'Reisebuchung', subtitle: 'Glückwunsch! Du kannst in diesem Arbeitsbereich jetzt Reisen buchen und verwalten.', ctaText: 'Reisen verwalten'}, settings: { autoAddTripName: {title: 'Reisenamen zu Ausgaben hinzufügen', subtitle: 'Reisenamen für in Expensify gebuchte Reisen automatisch zu Spesenbeschreibungen hinzufügen.'}, + codingSync: { + title: 'Fahrten bei der Buchung kodieren', + subtitle: 'Fordern Sie Reisende beim Check-out nach den Kategorien, Tags und Berichtsfeldern dieses Workspaces, damit Reisen bereits vorkodiert ankommen.', + }, }, travelInvoicing: { travelBookingSection: { diff --git a/src/languages/el.ts b/src/languages/el.ts index 5b6b8391c4d1..3ea38723a28a 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -6267,6 +6267,11 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ title: 'Προσθέστε ονόματα ταξιδιών στις δαπάνες', subtitle: 'Προσθέτετε αυτόματα τα ονόματα ταξιδιών στις περιγραφές εξόδων για ταξίδια που κλείνονται στο Expensify.', }, + codingSync: { + title: 'Κωδικοποιήστε τα επαγγελματικά ταξίδια κατά την κράτηση', + subtitle: + 'Ζητήστε από τους ταξιδιώτες, κατά την ολοκλήρωση κράτησης, τις κατηγορίες, τις ετικέτες και τα πεδία αναφορών αυτού του χώρου εργασίας, ώστε τα ταξίδια να φτάνουν ήδη κωδικοποιημένα.', + }, }, travelInvoicing: { travelBookingSection: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 2884f2968382..e20ff46fb675 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6223,6 +6223,10 @@ const translations = { title: 'Add trip names to expenses', subtitle: 'Automatically add trip names to expense descriptions for travel booked in Expensify.', }, + codingSync: { + title: 'Code trips at booking', + subtitle: 'Ask travelers for this workspace’s categories, tags, and report fields at checkout, so trips arrive already coded.', + }, }, travelInvoicing: { travelBookingSection: { diff --git a/src/languages/es.ts b/src/languages/es.ts index db2d74a3dbf0..b1075796fe14 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6011,6 +6011,11 @@ ${amount} para ${merchant} - ${date}`, title: 'Añadir nombres de viajes a los gastos', subtitle: 'Añade automáticamente los nombres de viajes a las descripciones de los gastos reservados en Expensify.', }, + codingSync: { + title: 'Codifica viajes al reservar', + subtitle: + 'Pide a las personas que viajan las categorías, etiquetas y campos de informe de este workspace al finalizar la reserva, para que los viajes lleguen ya codificados.', + }, }, travelInvoicing: { travelBookingSection: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 0a7e3518a21d..768a87f47cb9 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6154,6 +6154,11 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. title: 'Ajouter des noms de voyage aux dépenses', subtitle: 'Ajouter automatiquement les noms de voyage aux descriptions des dépenses pour les voyages réservés dans Expensify.', }, + codingSync: { + title: 'Coder les déplacements à la réservation', + subtitle: + 'Demander aux voyageurs les catégories, tags et champs de note de frais de cet espace de travail au moment du paiement, afin que les déplacements arrivent déjà codés.', + }, }, travelInvoicing: { travelBookingSection: { diff --git a/src/languages/it.ts b/src/languages/it.ts index b6ed31040144..8109fc9fef35 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6107,6 +6107,10 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. title: 'Aggiungi nomi di viaggi alle spese', subtitle: 'Aggiungi automaticamente i nomi dei viaggi alle descrizioni delle spese per i viaggi prenotati in Expensify.', }, + codingSync: { + title: 'Codifica i viaggi al momento della prenotazione', + subtitle: 'Chiedi ai viaggiatori le categorie, i tag e i campi di rendiconto di questo workspace al checkout, così i viaggi arrivano già codificati.', + }, }, travelInvoicing: { travelBookingSection: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index ee2e82bc9ddd..a92277098100 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6033,7 +6033,13 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO ctaText: 'リクエストを送信しました', }, bookOrManageYourTrip: {title: '出張予約', subtitle: 'おめでとうございます!このワークスペースで旅行の予約と管理を行う準備が整いました。', ctaText: '出張を管理'}, - settings: {autoAddTripName: {title: '経費に出張名を追加', subtitle: 'Expensifyで予約した出張について、経費の説明に出張名を自動的に追加します。'}}, + settings: { + autoAddTripName: {title: '経費に出張名を追加', subtitle: 'Expensifyで予約した出張について、経費の説明に出張名を自動的に追加します。'}, + codingSync: { + title: '予約時に出張をコード化', + subtitle: 'チェックアウト時にこのワークスペースのカテゴリ、タグ、レポート項目を旅行者に入力してもらい、出張データがあらかじめコード化された状態で届くようにします。', + }, + }, travelInvoicing: { travelBookingSection: { title: '出張予約', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 24defae3e317..e9edee621f95 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6097,6 +6097,10 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ }, settings: { autoAddTripName: {title: 'Reisnamen aan uitgaven toevoegen', subtitle: 'Voeg automatisch reisnamen toe aan onkostomemo’s voor reizen die in Expensify zijn geboekt.'}, + codingSync: { + title: 'Reizen coderen bij boeken', + subtitle: 'Vraag reizigers bij het uitchecken om de categorieën, tags en rapportvelden van deze workspace, zodat reizen al gecodeerd binnenkomen.', + }, }, travelInvoicing: { travelBookingSection: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d260cc752367..074a9c603fa4 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6074,6 +6074,10 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy }, settings: { autoAddTripName: {title: 'Dodaj nazwy podróży do wydatków', subtitle: 'Automatycznie dodawaj nazwy podróży do opisów wydatków za podróże zarezerwowane w Expensify.'}, + codingSync: { + title: 'Koduj podróże przy rezerwacji', + subtitle: 'Poproś podróżujących o kategorie, tagi i pola raportu tego przestrzeni roboczej przy wymeldowaniu, aby podróże trafiały już zakodowane.', + }, }, travelInvoicing: { travelBookingSection: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 771050f8bcad..04ffda13caa5 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6096,6 +6096,10 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS title: 'Adicionar nomes de viagem às despesas', subtitle: 'Adicione automaticamente os nomes das viagens às descrições das despesas para viagens reservadas no Expensify.', }, + codingSync: { + title: 'Classificar viagens na reserva', + subtitle: 'Peça aos viajantes as categorias, tags e campos de relatório deste workspace no checkout, para que as viagens cheguem já classificadas.', + }, }, travelInvoicing: { travelBookingSection: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index e29622ce0acf..34d4066f3575 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5899,7 +5899,10 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM ctaText: '请求已发送', }, bookOrManageYourTrip: {title: '差旅预订', subtitle: '恭喜!您现在可以在此工作区预订和管理差旅了。', ctaText: '管理差旅'}, - settings: {autoAddTripName: {title: '将行程名称添加到报销单', subtitle: '为在 Expensify 中预订的行程,自动将行程名称添加到报销事由描述中。'}}, + settings: { + autoAddTripName: {title: '将行程名称添加到报销单', subtitle: '为在 Expensify 中预订的行程,自动将行程名称添加到报销事由描述中。'}, + codingSync: {title: '在预订时标记出差用途', subtitle: '在结账时向出行人员询问此工作区的类别、标签和报表字段,让行程在到达时就已完成编码。'}, + }, travelInvoicing: { travelBookingSection: { title: '差旅预订', diff --git a/src/pages/workspace/travel/BookOrManageYourTrip.tsx b/src/pages/workspace/travel/BookOrManageYourTrip.tsx index 7b921c46001c..ecc200ad0772 100644 --- a/src/pages/workspace/travel/BookOrManageYourTrip.tsx +++ b/src/pages/workspace/travel/BookOrManageYourTrip.tsx @@ -45,6 +45,13 @@ function GetStartedTravel({policyID}: GetStartedTravelProps) { setPolicyTravelSettings(policy, {autoAddTripName: enabled}); }; + const isCodingSyncBetaEnabled = isBetaEnabled(CONST.BETAS.TRAVEL_CODING_SYNC); + const isCodingSyncEnabled = policy?.travelSettings?.isCodingSyncEnabled ?? false; + + const toggleCodingSync = (enabled: boolean) => { + setPolicyTravelSettings(policy, {isCodingSyncEnabled: enabled}); + }; + const handleManageTravel = () => { if (isPreventSpotnanaTravelEnabled) { showConfirmModal({ @@ -95,6 +102,21 @@ function GetStartedTravel({policyID}: GetStartedTravelProps) { pendingAction={policy?.pendingFields?.travelSettings} wrapperStyle={styles.mt3} /> + {isCodingSyncBetaEnabled && ( + + )} diff --git a/src/types/onyx/TravelSettings.ts b/src/types/onyx/TravelSettings.ts index 519dad78a03e..8cf0efc2822a 100644 --- a/src/types/onyx/TravelSettings.ts +++ b/src/types/onyx/TravelSettings.ts @@ -27,6 +27,9 @@ type WorkspaceTravelSettings = { /** Whether to automatically add trip names to expense descriptions during travel */ autoAddTripName?: boolean; + /** Whether the workspace's categories, tags and report fields are sent to Spotnana as booking questions. Absent means off. */ + isCodingSyncEnabled?: boolean; + /** Legal entity tax ID, collected during Travel enablement on non-USD workspaces so Solutions can provision a DK number */ taxID?: string; }; From 9396e5023bafd23d8bcc7d3d7700594f13ee0f14 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 14 Aug 2026 20:35:09 +0530 Subject: [PATCH 3/8] Use a straight apostrophe in the coding sync subtitle --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index e20ff46fb675..3f787ae7f7c1 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6225,7 +6225,7 @@ const translations = { }, codingSync: { title: 'Code trips at booking', - subtitle: 'Ask travelers for this workspace’s categories, tags, and report fields at checkout, so trips arrive already coded.', + subtitle: "Ask travelers for this workspace's categories, tags, and report fields at checkout, so trips arrive already coded.", }, }, travelInvoicing: { From 96025564522635874f21e97e226c2cd2bbd359ac Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 16 Aug 2026 04:13:52 +0530 Subject: [PATCH 4/8] Update the coding sync toggle copy and translations Lead with what the toggle does (push coding to Expensify Travel) instead of the booking-time effect, and fix a typo in the subtitle. --- src/languages/de.ts | 4 ++-- src/languages/el.ts | 4 ++-- src/languages/en.ts | 4 ++-- src/languages/es.ts | 4 ++-- src/languages/fr.ts | 4 ++-- src/languages/it.ts | 5 +++-- src/languages/ja.ts | 4 ++-- src/languages/nl.ts | 4 ++-- src/languages/pl.ts | 4 ++-- src/languages/pt-BR.ts | 4 ++-- src/languages/zh-hans.ts | 2 +- 11 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 46d907244b74..522513a5975e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6136,8 +6136,8 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU settings: { autoAddTripName: {title: 'Reisenamen zu Ausgaben hinzufügen', subtitle: 'Reisenamen für in Expensify gebuchte Reisen automatisch zu Spesenbeschreibungen hinzufügen.'}, codingSync: { - title: 'Fahrten bei der Buchung kodieren', - subtitle: 'Fordern Sie Reisende beim Check-out nach den Kategorien, Tags und Berichtsfeldern dieses Workspaces, damit Reisen bereits vorkodiert ankommen.', + title: 'Synchronisierungscodes mit Expensify Travel verknüpfen', + subtitle: 'Übertragen Sie die Kategorien, Tags und Berichts­felder dieses Workspaces zu Expensify Travel, damit Reisende sie zum Buchungszeitpunkt beantworten.', }, }, travelInvoicing: { diff --git a/src/languages/el.ts b/src/languages/el.ts index 4a07447db7fc..ee80faa8e3a5 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -6270,9 +6270,9 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ subtitle: 'Προσθέτετε αυτόματα τα ονόματα ταξιδιών στις περιγραφές εξόδων για ταξίδια που κλείνονται στο Expensify.', }, codingSync: { - title: 'Κωδικοποιήστε τα επαγγελματικά ταξίδια κατά την κράτηση', + title: 'Συγχρονισμός κωδικοποίησης με το Expensify Travel', subtitle: - 'Ζητήστε από τους ταξιδιώτες, κατά την ολοκλήρωση κράτησης, τις κατηγορίες, τις ετικέτες και τα πεδία αναφορών αυτού του χώρου εργασίας, ώστε τα ταξίδια να φτάνουν ήδη κωδικοποιημένα.', + 'Σπρώξτε τις κατηγορίες, τις ετικέτες και τα πεδία αναφοράς αυτού του χώρου εργασίας στο Expensify Travel, ώστε οι ταξιδιώτες να τα απαντούν κατά την ώρα της κράτησης.', }, }, travelInvoicing: { diff --git a/src/languages/en.ts b/src/languages/en.ts index c0aab8d8bcd3..9ce5c35642d1 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6226,8 +6226,8 @@ const translations = { subtitle: 'Automatically add trip names to expense descriptions for travel booked in Expensify.', }, codingSync: { - title: 'Code trips at booking', - subtitle: "Ask travelers for this workspace's categories, tags, and report fields at checkout, so trips arrive already coded.", + title: 'Sync coding to Expensify Travel', + subtitle: "Push this workspace's categories, tags, and report fields to Expensify Travel so travelers answer them at booking time.", }, }, travelInvoicing: { diff --git a/src/languages/es.ts b/src/languages/es.ts index ba65ed5a48a0..e5db7afd58ca 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6014,9 +6014,9 @@ ${amount} para ${merchant} - ${date}`, subtitle: 'Añade automáticamente los nombres de viajes a las descripciones de los gastos reservados en Expensify.', }, codingSync: { - title: 'Codifica viajes al reservar', + title: 'Sincronizar codificación con Expensify Travel', subtitle: - 'Pide a las personas que viajan las categorías, etiquetas y campos de informe de este workspace al finalizar la reserva, para que los viajes lleguen ya codificados.', + 'Envía las categorías, etiquetas y campos de informe de este espacio de trabajo a Expensify Travel para que los viajeros los completen en el momento de la reserva.', }, }, travelInvoicing: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 5c6c87c09a92..33361ef6c21f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6157,9 +6157,9 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. subtitle: 'Ajouter automatiquement les noms de voyage aux descriptions des dépenses pour les voyages réservés dans Expensify.', }, codingSync: { - title: 'Coder les déplacements à la réservation', + title: 'Synchroniser le codage avec Expensify Travel', subtitle: - 'Demander aux voyageurs les catégories, tags et champs de note de frais de cet espace de travail au moment du paiement, afin que les déplacements arrivent déjà codés.', + 'Envoyez les catégories, tags et champs de note de frais de cet espace de travail vers Expensify Travel pour que les voyageurs y répondent au moment de la réservation.', }, }, travelInvoicing: { diff --git a/src/languages/it.ts b/src/languages/it.ts index d3ec83bf5ca9..4a2a058f728c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6110,8 +6110,9 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. subtitle: 'Aggiungi automaticamente i nomi dei viaggi alle descrizioni delle spese per i viaggi prenotati in Expensify.', }, codingSync: { - title: 'Codifica i viaggi al momento della prenotazione', - subtitle: 'Chiedi ai viaggiatori le categorie, i tag e i campi di rendiconto di questo workspace al checkout, così i viaggi arrivano già codificati.', + title: 'Sincronizza la codifica con Expensify Travel', + subtitle: + 'Invia le categorie, i tag e i campi di rendiconto di questo spazio di lavoro a Expensify Travel così che chi viaggia li compili al momento della prenotazione.', }, }, travelInvoicing: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 0e285f58cdbf..e7c735b21be5 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6038,8 +6038,8 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO settings: { autoAddTripName: {title: '経費に出張名を追加', subtitle: 'Expensifyで予約した出張について、経費の説明に出張名を自動的に追加します。'}, codingSync: { - title: '予約時に出張をコード化', - subtitle: 'チェックアウト時にこのワークスペースのカテゴリ、タグ、レポート項目を旅行者に入力してもらい、出張データがあらかじめコード化された状態で届くようにします。', + title: 'コーディングを Expensify Travel と同期する', + subtitle: 'このワークスペースのカテゴリ、タグ、レポートフィールドを Expensify Travel にプッシュして、出張者が予約時にそれらに回答できるようにします。', }, }, travelInvoicing: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 9c1175b34323..c0a71b3d2b84 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6100,8 +6100,8 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ settings: { autoAddTripName: {title: 'Reisnamen aan uitgaven toevoegen', subtitle: 'Voeg automatisch reisnamen toe aan onkostomemo’s voor reizen die in Expensify zijn geboekt.'}, codingSync: { - title: 'Reizen coderen bij boeken', - subtitle: 'Vraag reizigers bij het uitchecken om de categorieën, tags en rapportvelden van deze workspace, zodat reizen al gecodeerd binnenkomen.', + title: 'Synchroniseer codering met Expensify Travel', + subtitle: 'Push de categorieën, tags en rapportvelden van deze workspace naar Expensify Travel zodat reizigers ze bij het boeken invullen.', }, }, travelInvoicing: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index a9cabf26cdcb..83c80c544e92 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6077,8 +6077,8 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy settings: { autoAddTripName: {title: 'Dodaj nazwy podróży do wydatków', subtitle: 'Automatycznie dodawaj nazwy podróży do opisów wydatków za podróże zarezerwowane w Expensify.'}, codingSync: { - title: 'Koduj podróże przy rezerwacji', - subtitle: 'Poproś podróżujących o kategorie, tagi i pola raportu tego przestrzeni roboczej przy wymeldowaniu, aby podróże trafiały już zakodowane.', + title: 'Synchronizuj kodowanie z Expensify Travel', + subtitle: 'Przekaż kategorie, tagi i pola raportu tego workspace’u do Expensify Travel, żeby podróżujący odpowiadali na nie podczas rezerwacji.', }, }, travelInvoicing: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 968c4ce82b92..2773711995d3 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6099,8 +6099,8 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS subtitle: 'Adicione automaticamente os nomes das viagens às descrições das despesas para viagens reservadas no Expensify.', }, codingSync: { - title: 'Classificar viagens na reserva', - subtitle: 'Peça aos viajantes as categorias, tags e campos de relatório deste workspace no checkout, para que as viagens cheguem já classificadas.', + title: 'Sincronizar codificação com o Expensify Travel', + subtitle: 'Envie as categorias, tags e campos de relatório deste workspace para o Expensify Travel para que os viajantes os respondam no momento da reserva.', }, }, travelInvoicing: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 8e4596ca36bc..7f8e9cbf886b 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5903,7 +5903,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM bookOrManageYourTrip: {title: '差旅预订', subtitle: '恭喜!您现在可以在此工作区预订和管理差旅了。', ctaText: '管理差旅'}, settings: { autoAddTripName: {title: '将行程名称添加到报销单', subtitle: '为在 Expensify 中预订的行程,自动将行程名称添加到报销事由描述中。'}, - codingSync: {title: '在预订时标记出差用途', subtitle: '在结账时向出行人员询问此工作区的类别、标签和报表字段,让行程在到达时就已完成编码。'}, + codingSync: {title: '将编码同步到 Expensify Travel', subtitle: '将此工作区的类别、标签和报表字段推送到 Expensify Travel,以便出行人能在预订时填写。'}, }, travelInvoicing: { travelBookingSection: { From 89a2cbb2e392ee70239519c319ac4552359b1f17 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 16 Aug 2026 04:25:09 +0530 Subject: [PATCH 5/8] Revert travel settings to a definite value on failure and surface errors previousTravelSettings could omit a key that was just toggled on, so merging it back on failure left the optimistic value in place instead of clearing it. Also wire errorFields.travelSettings into both toggle rows so a rejected update shows an error instead of silently reverting. --- src/libs/actions/Policy/Travel.ts | 11 +- .../workspace/travel/BookOrManageYourTrip.tsx | 5 + tests/actions/PolicyTravelSettingsTest.ts | 114 ++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 tests/actions/PolicyTravelSettingsTest.ts diff --git a/src/libs/actions/Policy/Travel.ts b/src/libs/actions/Policy/Travel.ts index 5b0fec1fb679..e46c9fa24fb1 100644 --- a/src/libs/actions/Policy/Travel.ts +++ b/src/libs/actions/Policy/Travel.ts @@ -3,6 +3,7 @@ import type {EnablePolicyTravelParams, SetPolicyTravelSettingsParams} from '@lib import {WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import {getObjectKeys} from '@libs/ObjectUtils'; import {goBackWhenEnableFeature} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; @@ -72,6 +73,14 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: const policyID = policy.id; const previousTravelSettings = policy?.travelSettings; + // Revert each changed key to its prior value, defaulting to null so a key that was absent + // before the optimistic update (e.g. a toggle that reads as off) is cleared by the merge + // instead of being left at its optimistic value. + const revertedSettings: Partial = {}; + for (const key of getObjectKeys(settings)) { + (revertedSettings as Record)[key] = previousTravelSettings?.[key] ?? null; + } + const onyxData: OnyxData = { optimisticData: [ { @@ -101,7 +110,7 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - travelSettings: previousTravelSettings, + travelSettings: revertedSettings, pendingFields: { travelSettings: null, }, diff --git a/src/pages/workspace/travel/BookOrManageYourTrip.tsx b/src/pages/workspace/travel/BookOrManageYourTrip.tsx index ecc200ad0772..92af332d7729 100644 --- a/src/pages/workspace/travel/BookOrManageYourTrip.tsx +++ b/src/pages/workspace/travel/BookOrManageYourTrip.tsx @@ -10,6 +10,7 @@ import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import {clearPolicyErrorField} from '@libs/actions/Policy/Policy'; import {setPolicyTravelSettings} from '@libs/actions/Policy/Travel'; import {openTravelDotLink} from '@libs/openTravelDotLink'; @@ -100,6 +101,8 @@ function GetStartedTravel({policyID}: GetStartedTravelProps) { disabledAction={withReadOnlyFallback()} showLockIcon={!canWriteMoreFeatures} pendingAction={policy?.pendingFields?.travelSettings} + errors={policy?.errorFields?.travelSettings ?? undefined} + onCloseError={() => clearPolicyErrorField(policyID, 'travelSettings')} wrapperStyle={styles.mt3} /> {isCodingSyncBetaEnabled && ( @@ -114,6 +117,8 @@ function GetStartedTravel({policyID}: GetStartedTravelProps) { disabledAction={withReadOnlyFallback()} showLockIcon={!canWriteMoreFeatures} pendingAction={policy?.pendingFields?.travelSettings} + errors={policy?.errorFields?.travelSettings ?? undefined} + onCloseError={() => clearPolicyErrorField(policyID, 'travelSettings')} wrapperStyle={styles.mt3} /> )} diff --git a/tests/actions/PolicyTravelSettingsTest.ts b/tests/actions/PolicyTravelSettingsTest.ts new file mode 100644 index 000000000000..d58b17660f68 --- /dev/null +++ b/tests/actions/PolicyTravelSettingsTest.ts @@ -0,0 +1,114 @@ +import {setPolicyTravelSettings} from '@libs/actions/Policy/Travel'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; + +import Onyx from 'react-native-onyx'; + +import type {MockFetch} from '../utils/TestHelper'; + +import createRandomPolicy from '../utils/collections/policies'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +describe('actions/Policy/Travel', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + let mockFetch: MockFetch; + beforeEach(() => { + mockFetch = TestHelper.getGlobalFetchMock(); + global.fetch = mockFetch; + return Onyx.clear().then(waitForBatchedUpdates); + }); + + describe('setPolicyTravelSettings', () => { + it('clears a toggled-on setting on failure when it was absent beforehand', async () => { + const fakePolicy: Policy = {...createRandomPolicy(0), travelSettings: undefined}; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + + mockFetch?.pause?.(); + setPolicyTravelSettings(fakePolicy, {isCodingSyncEnabled: true}); + await waitForBatchedUpdates(); + mockFetch?.fail?.(); + await mockFetch?.resume?.(); + await waitForBatchedUpdates(); + + const policy = await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value); + }, + }); + }); + + expect(policy?.travelSettings?.isCodingSyncEnabled).toBeUndefined(); + expect(policy?.pendingFields?.travelSettings).toBeFalsy(); + }); + + it('reverts a toggled setting to its prior value on failure', async () => { + const fakePolicy: Policy = {...createRandomPolicy(0), travelSettings: {autoAddTripName: true}}; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + + mockFetch?.pause?.(); + setPolicyTravelSettings(fakePolicy, {autoAddTripName: false}); + await waitForBatchedUpdates(); + mockFetch?.fail?.(); + await mockFetch?.resume?.(); + await waitForBatchedUpdates(); + + const policy = await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value); + }, + }); + }); + + expect(policy?.travelSettings?.autoAddTripName).toBe(true); + expect(policy?.errorFields?.travelSettings).toBeTruthy(); + }); + + it('applies the optimistic update and clears pending state on success', async () => { + const fakePolicy: Policy = {...createRandomPolicy(0), travelSettings: undefined}; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + + mockFetch?.pause?.(); + setPolicyTravelSettings(fakePolicy, {isCodingSyncEnabled: true}); + await waitForBatchedUpdates(); + + const optimisticPolicy = await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value); + }, + }); + }); + expect(optimisticPolicy?.travelSettings?.isCodingSyncEnabled).toBe(true); + expect(optimisticPolicy?.pendingFields?.travelSettings).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + + await mockFetch?.resume?.(); + await waitForBatchedUpdates(); + + const settledPolicy = await new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value); + }, + }); + }); + expect(settledPolicy?.travelSettings?.isCodingSyncEnabled).toBe(true); + expect(settledPolicy?.pendingFields?.travelSettings).toBeFalsy(); + }); + }); +}); From 1cc3dc91185ce1fff91a263ac46d50ee0be716cd Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 16 Aug 2026 05:16:22 +0530 Subject: [PATCH 6/8] Track travel settings pending and error state per setting Both travel toggles read one shared travelSettings key, so a failed update to one greyed out and showed an error on the other. --- src/libs/actions/Policy/Travel.ts | 27 ++++--- .../workspace/travel/BookOrManageYourTrip.tsx | 12 +-- src/types/onyx/Policy.ts | 2 +- tests/actions/PolicyTravelSettingsTest.ts | 81 ++++++++++--------- 4 files changed, 63 insertions(+), 59 deletions(-) diff --git a/src/libs/actions/Policy/Travel.ts b/src/libs/actions/Policy/Travel.ts index e46c9fa24fb1..97611b812b49 100644 --- a/src/libs/actions/Policy/Travel.ts +++ b/src/libs/actions/Policy/Travel.ts @@ -9,6 +9,7 @@ import {goBackWhenEnableFeature} from '@libs/PolicyUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; +import type {ErrorFields, PendingFields} from '@src/types/onyx/OnyxCommon'; import type {OnyxData} from '@src/types/onyx/Request'; import type {OnyxEntry} from 'react-native-onyx'; @@ -77,8 +78,18 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: // before the optimistic update (e.g. a toggle that reads as off) is cleared by the merge // instead of being left at its optimistic value. const revertedSettings: Partial = {}; + + // Pending and error state is tracked per setting so that updating one setting does not + // put every other travel setting's row into a pending or failed state. + const pendingSettings: PendingFields = {}; + const clearedPendingSettings: Record = {}; + const settingErrors: ErrorFields = {}; + for (const key of getObjectKeys(settings)) { (revertedSettings as Record)[key] = previousTravelSettings?.[key] ?? null; + pendingSettings[key] = CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; + clearedPendingSettings[key] = null; + settingErrors[key] = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'); } const onyxData: OnyxData = { @@ -88,9 +99,7 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { travelSettings: {...previousTravelSettings, ...settings}, - pendingFields: { - travelSettings: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, + pendingFields: pendingSettings, }, }, ], @@ -99,9 +108,7 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - pendingFields: { - travelSettings: null, - }, + pendingFields: clearedPendingSettings, }, }, ], @@ -111,12 +118,8 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { travelSettings: revertedSettings, - pendingFields: { - travelSettings: null, - }, - errorFields: { - travelSettings: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - }, + pendingFields: clearedPendingSettings, + errorFields: settingErrors, }, }, ], diff --git a/src/pages/workspace/travel/BookOrManageYourTrip.tsx b/src/pages/workspace/travel/BookOrManageYourTrip.tsx index 92af332d7729..0a225b543ec7 100644 --- a/src/pages/workspace/travel/BookOrManageYourTrip.tsx +++ b/src/pages/workspace/travel/BookOrManageYourTrip.tsx @@ -100,9 +100,9 @@ function GetStartedTravel({policyID}: GetStartedTravelProps) { disabled={!canWriteMoreFeatures} disabledAction={withReadOnlyFallback()} showLockIcon={!canWriteMoreFeatures} - pendingAction={policy?.pendingFields?.travelSettings} - errors={policy?.errorFields?.travelSettings ?? undefined} - onCloseError={() => clearPolicyErrorField(policyID, 'travelSettings')} + pendingAction={policy?.pendingFields?.autoAddTripName} + errors={policy?.errorFields?.autoAddTripName ?? undefined} + onCloseError={() => clearPolicyErrorField(policyID, 'autoAddTripName')} wrapperStyle={styles.mt3} /> {isCodingSyncBetaEnabled && ( @@ -116,9 +116,9 @@ function GetStartedTravel({policyID}: GetStartedTravelProps) { disabled={!canWriteMoreFeatures} disabledAction={withReadOnlyFallback()} showLockIcon={!canWriteMoreFeatures} - pendingAction={policy?.pendingFields?.travelSettings} - errors={policy?.errorFields?.travelSettings ?? undefined} - onCloseError={() => clearPolicyErrorField(policyID, 'travelSettings')} + pendingAction={policy?.pendingFields?.isCodingSyncEnabled} + errors={policy?.errorFields?.isCodingSyncEnabled ?? undefined} + onCloseError={() => clearPolicyErrorField(policyID, 'isCodingSyncEnabled')} wrapperStyle={styles.mt3} /> )} diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 31968e2770fe..aedee19f7849 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -3164,7 +3164,7 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the policy requires purchases to be on a company card */ requireCompanyCardsEnabled?: boolean; } & Partial, - 'addWorkspaceRoom' | keyof ACHAccount | keyof Attributes | 'isHREnabled' | 'isTimeTrackingEnabled' | 'timeTrackingDefaultRate' + 'addWorkspaceRoom' | keyof ACHAccount | keyof Attributes | keyof WorkspaceTravelSettings | 'isHREnabled' | 'isTimeTrackingEnabled' | 'timeTrackingDefaultRate' >; /** Stages of policy connection sync */ diff --git a/tests/actions/PolicyTravelSettingsTest.ts b/tests/actions/PolicyTravelSettingsTest.ts index d58b17660f68..4461285959a3 100644 --- a/tests/actions/PolicyTravelSettingsTest.ts +++ b/tests/actions/PolicyTravelSettingsTest.ts @@ -12,6 +12,18 @@ import createRandomPolicy from '../utils/collections/policies'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +function getPolicy(policyID: string): Promise { + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + callback: (value) => { + Onyx.disconnect(connection); + resolve(value); + }, + }); + }); +} + describe('actions/Policy/Travel', () => { beforeAll(() => { Onyx.init({keys: ONYXKEYS}); @@ -36,18 +48,10 @@ describe('actions/Policy/Travel', () => { await mockFetch?.resume?.(); await waitForBatchedUpdates(); - const policy = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - callback: (value) => { - Onyx.disconnect(connection); - resolve(value); - }, - }); - }); + const policy = await getPolicy(fakePolicy.id); expect(policy?.travelSettings?.isCodingSyncEnabled).toBeUndefined(); - expect(policy?.pendingFields?.travelSettings).toBeFalsy(); + expect(policy?.pendingFields?.isCodingSyncEnabled).toBeFalsy(); }); it('reverts a toggled setting to its prior value on failure', async () => { @@ -61,18 +65,10 @@ describe('actions/Policy/Travel', () => { await mockFetch?.resume?.(); await waitForBatchedUpdates(); - const policy = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - callback: (value) => { - Onyx.disconnect(connection); - resolve(value); - }, - }); - }); + const policy = await getPolicy(fakePolicy.id); expect(policy?.travelSettings?.autoAddTripName).toBe(true); - expect(policy?.errorFields?.travelSettings).toBeTruthy(); + expect(policy?.errorFields?.autoAddTripName).toBeTruthy(); }); it('applies the optimistic update and clears pending state on success', async () => { @@ -83,32 +79,37 @@ describe('actions/Policy/Travel', () => { setPolicyTravelSettings(fakePolicy, {isCodingSyncEnabled: true}); await waitForBatchedUpdates(); - const optimisticPolicy = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - callback: (value) => { - Onyx.disconnect(connection); - resolve(value); - }, - }); - }); + const optimisticPolicy = await getPolicy(fakePolicy.id); expect(optimisticPolicy?.travelSettings?.isCodingSyncEnabled).toBe(true); - expect(optimisticPolicy?.pendingFields?.travelSettings).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + expect(optimisticPolicy?.pendingFields?.isCodingSyncEnabled).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); await mockFetch?.resume?.(); await waitForBatchedUpdates(); - const settledPolicy = await new Promise((resolve) => { - const connection = Onyx.connect({ - key: `${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, - callback: (value) => { - Onyx.disconnect(connection); - resolve(value); - }, - }); - }); + const settledPolicy = await getPolicy(fakePolicy.id); expect(settledPolicy?.travelSettings?.isCodingSyncEnabled).toBe(true); - expect(settledPolicy?.pendingFields?.travelSettings).toBeFalsy(); + expect(settledPolicy?.pendingFields?.isCodingSyncEnabled).toBeFalsy(); + }); + + it('leaves the other travel settings free of pending and error state', async () => { + const fakePolicy: Policy = {...createRandomPolicy(0), travelSettings: {autoAddTripName: true}}; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + + mockFetch?.pause?.(); + setPolicyTravelSettings(fakePolicy, {isCodingSyncEnabled: true}); + await waitForBatchedUpdates(); + + const pendingPolicy = await getPolicy(fakePolicy.id); + expect(pendingPolicy?.pendingFields?.autoAddTripName).toBeFalsy(); + + mockFetch?.fail?.(); + await mockFetch?.resume?.(); + await waitForBatchedUpdates(); + + const failedPolicy = await getPolicy(fakePolicy.id); + expect(failedPolicy?.errorFields?.isCodingSyncEnabled).toBeTruthy(); + expect(failedPolicy?.errorFields?.autoAddTripName).toBeFalsy(); + expect(failedPolicy?.travelSettings?.autoAddTripName).toBe(true); }); }); }); From bcf851a9a00d6902c6f3fc97f08c96988eb24634 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Sun, 16 Aug 2026 05:16:23 +0530 Subject: [PATCH 7/8] Fix the German coding sync copy The title read as linking synchronization codes rather than syncing expense coding. --- src/languages/de.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 522513a5975e..780d7afe6f16 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6136,8 +6136,8 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU settings: { autoAddTripName: {title: 'Reisenamen zu Ausgaben hinzufügen', subtitle: 'Reisenamen für in Expensify gebuchte Reisen automatisch zu Spesenbeschreibungen hinzufügen.'}, codingSync: { - title: 'Synchronisierungscodes mit Expensify Travel verknüpfen', - subtitle: 'Übertragen Sie die Kategorien, Tags und Berichts­felder dieses Workspaces zu Expensify Travel, damit Reisende sie zum Buchungszeitpunkt beantworten.', + title: 'Codierung mit Expensify Travel synchronisieren', + subtitle: 'Kategorien, Tags und Berichtsfelder dieses Arbeitsbereichs an Expensify Travel übertragen, damit Reisende sie beim Buchen beantworten.', }, }, travelInvoicing: { From 06858a1a8aabeac507d453ce72115999794cc974 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Tue, 18 Aug 2026 23:25:40 +0530 Subject: [PATCH 8/8] Clear a travel setting's error when its update is retried A failed update left an entry in errorFields that nothing removed, so an admin who retried without dismissing the banner kept seeing the failure under a toggle that had just succeeded. --- src/libs/actions/Policy/Travel.ts | 10 ++++++---- tests/actions/PolicyTravelSettingsTest.ts | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Policy/Travel.ts b/src/libs/actions/Policy/Travel.ts index 97611b812b49..c5c6d3006021 100644 --- a/src/libs/actions/Policy/Travel.ts +++ b/src/libs/actions/Policy/Travel.ts @@ -82,13 +82,13 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: // Pending and error state is tracked per setting so that updating one setting does not // put every other travel setting's row into a pending or failed state. const pendingSettings: PendingFields = {}; - const clearedPendingSettings: Record = {}; + const clearedSettingFields: Record = {}; const settingErrors: ErrorFields = {}; for (const key of getObjectKeys(settings)) { (revertedSettings as Record)[key] = previousTravelSettings?.[key] ?? null; pendingSettings[key] = CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE; - clearedPendingSettings[key] = null; + clearedSettingFields[key] = null; settingErrors[key] = ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'); } @@ -100,6 +100,7 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: value: { travelSettings: {...previousTravelSettings, ...settings}, pendingFields: pendingSettings, + errorFields: clearedSettingFields, }, }, ], @@ -108,7 +109,8 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - pendingFields: clearedPendingSettings, + pendingFields: clearedSettingFields, + errorFields: clearedSettingFields, }, }, ], @@ -118,7 +120,7 @@ function setPolicyTravelSettings(policy: OnyxEntry, settings: key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { travelSettings: revertedSettings, - pendingFields: clearedPendingSettings, + pendingFields: clearedSettingFields, errorFields: settingErrors, }, }, diff --git a/tests/actions/PolicyTravelSettingsTest.ts b/tests/actions/PolicyTravelSettingsTest.ts index 4461285959a3..f9c157f575bc 100644 --- a/tests/actions/PolicyTravelSettingsTest.ts +++ b/tests/actions/PolicyTravelSettingsTest.ts @@ -91,6 +91,26 @@ describe('actions/Policy/Travel', () => { expect(settledPolicy?.pendingFields?.isCodingSyncEnabled).toBeFalsy(); }); + it('clears the error from a failed update when the retry succeeds', async () => { + const fakePolicy: Policy = {...createRandomPolicy(0), travelSettings: undefined}; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy); + + mockFetch?.fail?.(); + setPolicyTravelSettings(fakePolicy, {isCodingSyncEnabled: true}); + await waitForBatchedUpdates(); + + const failedPolicy = await getPolicy(fakePolicy.id); + expect(failedPolicy?.errorFields?.isCodingSyncEnabled).toBeTruthy(); + + mockFetch?.succeed?.(); + setPolicyTravelSettings(failedPolicy, {isCodingSyncEnabled: true}); + await waitForBatchedUpdates(); + + const retriedPolicy = await getPolicy(fakePolicy.id); + expect(retriedPolicy?.travelSettings?.isCodingSyncEnabled).toBe(true); + expect(retriedPolicy?.errorFields?.isCodingSyncEnabled).toBeFalsy(); + }); + it('leaves the other travel settings free of pending and error state', async () => { const fakePolicy: Policy = {...createRandomPolicy(0), travelSettings: {autoAddTripName: true}}; await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${fakePolicy.id}`, fakePolicy);