From 9facbae4519095a549165f0fc04fe63e85cecf40 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 8 Jun 2026 10:44:59 +0200 Subject: [PATCH 01/21] Update delete workspace error message --- .../HTMLRenderers/ConciergeLinkRenderer.tsx | 4 +++- src/components/RenderHTML.tsx | 15 ++++++++++++--- src/languages/en.ts | 2 +- src/pages/workspace/WorkspaceOverviewPage.tsx | 10 +++++++++- src/pages/workspace/WorkspacesListPage.tsx | 10 +++++++++- 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx index c010323ea54f..15a8db50b778 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx @@ -2,7 +2,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import React from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; -import {TNodeChildrenRenderer} from 'react-native-render-html'; +import {TNodeChildrenRenderer, useRendererProps} from 'react-native-render-html'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -22,11 +22,13 @@ function ConciergeLinkRenderer({tnode, style}: ConciergeLinkRendererProps) { const [betas] = useOnyx(ONYXKEYS.BETAS); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const {onPress: onPressFromProps} = useRendererProps('concierge-link'); /** * Simple wrapper to create a stable reference without passing event args to navigation function. */ const navigateToConciergeChat = () => { + onPressFromProps?.(); navigateToConciergeChatAction(conciergeReportID, introSelected, currentUserAccountID, isSelfTourViewed, betas, false); }; diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx index 6d31af35218c..cd8af3952425 100644 --- a/src/components/RenderHTML.tsx +++ b/src/components/RenderHTML.tsx @@ -5,11 +5,13 @@ import useHasTextAncestor from '@hooks/useHasTextAncestor'; import useWindowDimensions from '@hooks/useWindowDimensions'; import Parser from '@libs/Parser'; import BulletItemRenderer from './HTMLEngineProvider/HTMLRenderers/BulletItemRenderer'; +import ConciergeLinkRenderer from './HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer'; import OLRenderer from './HTMLEngineProvider/HTMLRenderers/OLRenderer'; import SparklesIconRenderer from './HTMLEngineProvider/HTMLRenderers/SparklesIconRenderer'; import ULRenderer from './HTMLEngineProvider/HTMLRenderers/ULRenderer'; type LinkPressHandler = NonNullable['onPress']; +type ConciergeLinkPressHandler = () => void; // Matches &#91; (→ "[") and &#93; (→ "]"). Index 7 is the distinguishing digit ('1' vs '3'). const RE_BRACKET_ESCAPE = /&#9[13];/g; @@ -25,6 +27,9 @@ type RenderHTMLProps = { /** Callback to handle link press */ onLinkPress?: LinkPressHandler; + /** Callback to handle concierge-link press */ + onConciergeLinkPress?: ConciergeLinkPressHandler; + /** Whether the rendered text should be selectable */ isSelectable?: boolean; }; @@ -33,7 +38,7 @@ type RenderHTMLProps = { // Configuration for RenderHTML is handled in a top-level component providing // context to RenderHTMLSource components. See https://git.io/JRcZb // The provider is available at src/components/HTMLEngineProvider/ -function RenderHTML({html: htmlParam, onLinkPress, isSelectable}: RenderHTMLProps) { +function RenderHTML({html: htmlParam, onLinkPress, onConciergeLinkPress, isSelectable}: RenderHTMLProps) { const hasTextAncestor = useHasTextAncestor(); if (__DEV__ && hasTextAncestor) { throw new Error('RenderHTML must not be rendered inside a component, as it will break the layout on iOS. Render it as a sibling instead.'); @@ -57,12 +62,16 @@ function RenderHTML({html: htmlParam, onLinkPress, isSelectable}: RenderHTMLProp a: { onPress: onLinkPress, }, + 'concierge-link': { + onPress: onConciergeLinkPress, + }, }; - }, [onLinkPress]); + }, [onLinkPress, onConciergeLinkPress]); const renderers = { /* eslint-disable @typescript-eslint/naming-convention */ 'bullet-item': BulletItemRenderer, + 'concierge-link': ConciergeLinkRenderer, 'sparkles-icon': SparklesIconRenderer, ol: OLRenderer, ul: ULRenderer, @@ -75,7 +84,7 @@ function RenderHTML({html: htmlParam, onLinkPress, isSelectable}: RenderHTMLProp /> ); - return onLinkPress ? ( + return onLinkPress || onConciergeLinkPress ? ( reach out to Concierge to remove them.', outstandingBalanceWarning: 'You have an outstanding balance that must be settled before deleting your last workspace. Please go to your subscription settings to resolve the payment.', settleBalance: 'Go to subscription', diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index fd5f0aabfeb1..5196d51e6d5a 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -17,6 +17,7 @@ import {usePersonalDetails} from '@components/OnyxListItemProvider'; import PDFThumbnail from '@components/PDFThumbnail'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import RenderHTML from '@components/RenderHTML'; import Section from '@components/Section'; import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; @@ -375,6 +376,13 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa clearDeleteWorkspaceError(policyID); }; + const policyLastErrorMessagePrompt = policyLastErrorMessage ? ( + + ) : null; + useEffect(() => { if (isLoadingBill) { return; @@ -630,7 +638,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa isVisible={isDeleteWorkspaceErrorModalOpen} onConfirm={hideDeleteWorkspaceErrorModal} onCancel={hideDeleteWorkspaceErrorModal} - prompt={policyLastErrorMessage} + prompt={policyLastErrorMessagePrompt} confirmText={translate('common.buttonConfirm')} shouldShowCancelButton={false} success={false} diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 0ad20fd550bb..2492eec88866 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -8,6 +8,7 @@ import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import type {PopoverMenuItem} from '@components/PopoverMenu'; +import RenderHTML from '@components/RenderHTML'; import type {TableHandle} from '@components/Table'; import type {WorkspaceRowData, WorkspaceTableColumnKey} from '@components/Tables/WorkspaceListTable'; import WorkspaceListTable from '@components/Tables/WorkspaceListTable'; @@ -234,6 +235,13 @@ function WorkspacesListPage() { dismissWorkspaceError(policyToDelete.id, policyToDelete.pendingAction); }; + const policyToDeleteLatestErrorMessagePrompt = policyToDeleteLatestErrorMessage ? ( + + ) : null; + const confirmLeaveAndHideModal = () => { if (!policyToLeave) { return; @@ -682,7 +690,7 @@ function WorkspacesListPage() { isVisible={isDeleteWorkspaceErrorModalOpen} onConfirm={hideDeleteWorkspaceErrorModal} onCancel={hideDeleteWorkspaceErrorModal} - prompt={policyToDeleteLatestErrorMessage} + prompt={policyToDeleteLatestErrorMessagePrompt} confirmText={translate('common.buttonConfirm')} shouldShowCancelButton={false} success={false} From 8ae1a01a2c65f69d74bc9273c5eb9a10e65fdb9c Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 8 Jun 2026 11:24:25 +0200 Subject: [PATCH 02/21] Fix eslint --- .../HTMLRenderers/ConciergeLinkRenderer.tsx | 6 +++++- src/components/RenderHTML.tsx | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx index 15a8db50b778..a5cfc7ae312d 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx @@ -15,6 +15,10 @@ import ONYXKEYS from '@src/ONYXKEYS'; type ConciergeLinkRendererProps = CustomRendererProps; +type ConciergeLinkRendererConfig = { + onPress?: () => void; +}; + function ConciergeLinkRenderer({tnode, style}: ConciergeLinkRendererProps) { const styles = useThemeStyles(); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); @@ -22,7 +26,7 @@ function ConciergeLinkRenderer({tnode, style}: ConciergeLinkRendererProps) { const [betas] = useOnyx(ONYXKEYS.BETAS); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const {onPress: onPressFromProps} = useRendererProps('concierge-link'); + const {onPress: onPressFromProps} = useRendererProps('concierge-link') as ConciergeLinkRendererConfig; /** * Simple wrapper to create a stable reference without passing event args to navigation function. diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx index cd8af3952425..39c78da60ea0 100644 --- a/src/components/RenderHTML.tsx +++ b/src/components/RenderHTML.tsx @@ -62,6 +62,7 @@ function RenderHTML({html: htmlParam, onLinkPress, onConciergeLinkPress, isSelec a: { onPress: onLinkPress, }, + /* eslint-disable @typescript-eslint/naming-convention */ 'concierge-link': { onPress: onConciergeLinkPress, }, From 700097bd40c1575d72b824808f1363ade63f7edc Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 8 Jun 2026 12:55:52 +0200 Subject: [PATCH 03/21] Adjust to main --- src/components/DotIndicatorMessage.tsx | 31 +++++++++++++++++-- .../HTMLRenderers/ConciergeLinkRenderer.tsx | 2 +- src/pages/workspace/WorkspaceOverviewPage.tsx | 16 +++++----- src/pages/workspace/WorkspacesListPage.tsx | 15 ++++----- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index c9326e086696..bfe8ad93eb2d 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -17,8 +17,11 @@ import type {TranslationKeyError} from '@src/types/onyx/OnyxCommon'; import type {ReceiptError} from '@src/types/onyx/Transaction'; import Button from './Button'; import Icon from './Icon'; +import RenderHTML from './RenderHTML'; import Text from './Text'; +const HTML_TAG_PATTERN = /<\/?[a-z][^>]*>/i; + type DotIndicatorMessageProps = { /** * In most cases this should just be errors from onxyData @@ -74,7 +77,31 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles, dismissErr } const displayMessage = isTranslationKeyError(message) ? translate(message.translationKey) : message; - const formattedMessage = typeof displayMessage === 'string' ? Str.htmlDecode(displayMessage) : displayMessage; + + if (typeof displayMessage !== 'string') { + return ( + + {displayMessage} + + ); + } + + if (HTML_TAG_PATTERN.test(displayMessage)) { + const html = isErrorMessage ? `${displayMessage}` : `${displayMessage}`; + + return ( + + ); + } return ( - {formattedMessage} + {Str.htmlDecode(displayMessage)} ); }; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx index a5cfc7ae312d..e53f47ba7017 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx @@ -26,7 +26,7 @@ function ConciergeLinkRenderer({tnode, style}: ConciergeLinkRendererProps) { const [betas] = useOnyx(ONYXKEYS.BETAS); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const {onPress: onPressFromProps} = useRendererProps('concierge-link') as ConciergeLinkRendererConfig; + const {onPress: onPressFromProps} = (useRendererProps('concierge-link') ?? {}) as ConciergeLinkRendererConfig; /** * Simple wrapper to create a stable reference without passing event args to navigation function. diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 9b04e5da2e2a..b4bd186b48bd 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -398,6 +398,13 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const prevIsPendingDeleteValue = prevIsPendingDeleteRef.current; prevIsPendingDeleteRef.current = isPendingDelete; + const policyLastErrorMessagePrompt = ( + + ); + if (isOffline && policyLastErrorMessage && hasExpensifyCard) { if (isErrorModalShowingRef.current) { return; @@ -405,7 +412,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa isErrorModalShowingRef.current = true; showConfirmModal({ title: translate('workspace.common.delete'), - prompt: policyLastErrorMessage, + prompt: policyLastErrorMessagePrompt, confirmText: translate('common.buttonConfirm'), shouldShowCancelButton: false, success: false, @@ -434,13 +441,6 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa } isErrorModalShowingRef.current = true; - const policyLastErrorMessagePrompt = ( - - ); - showConfirmModal({ title: translate('workspace.common.delete'), prompt: policyLastErrorMessagePrompt, diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index d4df879b5ee0..04c18490ed90 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -292,6 +292,13 @@ function WorkspacesListPage() { const prevIsPendingDelete = prevIsPendingDeleteRef.current; prevIsPendingDeleteRef.current = isPendingDelete; + const policyToDeleteLatestErrorMessagePrompt = ( + + ); + // Handle showing error modal when offline and error occurs if (isOffline && policyToDeleteLatestErrorMessage) { if (isErrorModalShowingRef.current) { @@ -300,7 +307,7 @@ function WorkspacesListPage() { isErrorModalShowingRef.current = true; showConfirmModal({ title: translate('workspace.common.delete'), - prompt: policyToDeleteLatestErrorMessage, + prompt: policyToDeleteLatestErrorMessagePrompt, confirmText: translate('common.buttonConfirm'), cancelText: translate('common.cancel'), success: false, @@ -325,12 +332,6 @@ function WorkspacesListPage() { } isErrorModalShowingRef.current = true; - const policyToDeleteLatestErrorMessagePrompt = ( - - ); showConfirmModal({ title: translate('workspace.common.delete'), prompt: policyToDeleteLatestErrorMessagePrompt, From 0ff99a4dad2ad1d54963b14aaf9878d36bd9a94c Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Mon, 8 Jun 2026 15:23:11 +0200 Subject: [PATCH 04/21] close modal when using link --- src/pages/workspace/WorkspaceOverviewPage.tsx | 5 ++++- src/pages/workspace/WorkspacesListPage.tsx | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index b4bd186b48bd..5de62c298547 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -401,7 +401,10 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const policyLastErrorMessagePrompt = ( { + closeModal(); + hideDeleteWorkspaceErrorModal(); + }} /> ); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 04c18490ed90..f30f27b6fb5e 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -295,7 +295,10 @@ function WorkspacesListPage() { const policyToDeleteLatestErrorMessagePrompt = ( { + closeModal(); + hideDeleteWorkspaceErrorModal(); + }} /> ); From f4d8790c3bbc250503cf1767053cfbb1a80e8724 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Fri, 12 Jun 2026 08:58:40 +0200 Subject: [PATCH 05/21] Explain why eslint-disable is needed --- src/components/RenderHTML.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/RenderHTML.tsx b/src/components/RenderHTML.tsx index 39c78da60ea0..771eb10abec9 100644 --- a/src/components/RenderHTML.tsx +++ b/src/components/RenderHTML.tsx @@ -62,6 +62,7 @@ function RenderHTML({html: htmlParam, onLinkPress, onConciergeLinkPress, isSelec a: { onPress: onLinkPress, }, + // Custom HTML renderer keys must use hyphenated tag names per react-native-render-html API /* eslint-disable @typescript-eslint/naming-convention */ 'concierge-link': { onPress: onConciergeLinkPress, From 74c13dd21175deb75f235961ae6caeaa0b514f84 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski <56261019+jnowakow@users.noreply.github.com> Date: Fri, 12 Jun 2026 03:43:03 -0700 Subject: [PATCH 06/21] Conditionally assign error message Co-authored-by: Linh Vo --- src/pages/workspace/WorkspacesListPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index f30f27b6fb5e..742491860e50 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -292,7 +292,7 @@ function WorkspacesListPage() { const prevIsPendingDelete = prevIsPendingDeleteRef.current; prevIsPendingDeleteRef.current = isPendingDelete; - const policyToDeleteLatestErrorMessagePrompt = ( + const policyToDeleteLatestErrorMessagePrompt = policyToDeleteLatestErrorMessage && ( { From 53efab699f9510c7316977dd561640df6d7dea9c Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Fri, 12 Jun 2026 12:50:17 +0200 Subject: [PATCH 07/21] conditionally assign html error --- src/pages/workspace/WorkspaceOverviewPage.tsx | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 5de62c298547..b7d141675c7d 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -398,7 +398,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const prevIsPendingDeleteValue = prevIsPendingDeleteRef.current; prevIsPendingDeleteRef.current = isPendingDelete; - const policyLastErrorMessagePrompt = ( + const policyLastErrorMessagePrompt = !!policyLastErrorMessage && ( { diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 742491860e50..d06dbca8c2c5 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -292,7 +292,7 @@ function WorkspacesListPage() { const prevIsPendingDelete = prevIsPendingDeleteRef.current; prevIsPendingDeleteRef.current = isPendingDelete; - const policyToDeleteLatestErrorMessagePrompt = policyToDeleteLatestErrorMessage && ( + const policyToDeleteLatestErrorMessagePrompt = !!policyToDeleteLatestErrorMessage && ( { From 065312184510f22e734287e7eaddbc3aacba4375 Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Fri, 12 Jun 2026 14:43:20 +0200 Subject: [PATCH 08/21] add translations --- src/languages/de.ts | 2 +- src/languages/es.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 32c68c42a5b1..8e74fc3d85c5 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4323,7 +4323,7 @@ ${amount} für ${merchant} – ${date}`, defaultNote: `Belege, die an ${CONST.EMAIL.RECEIPTS} gesendet werden, erscheinen in diesem Workspace.`, deleteConfirmation: 'Möchten Sie diesen Workspace wirklich löschen?', deleteWithCardsConfirmation: 'Möchtest du diesen Workspace wirklich löschen? Dadurch werden alle Kartenfeeds und zugewiesenen Karten entfernt.', - deleteOpenExpensifyCardsError: 'Ihr Unternehmen hat noch aktive Expensify Cards.', + deleteOpenExpensifyCardsError: 'Ihre Firma hat noch Expensify Karten. Bitte wenden Sie sich an Concierge, um sie zu entfernen.', outstandingBalanceWarning: 'Sie haben einen offenen Saldo, der beglichen werden muss, bevor Sie Ihren letzten Workspace löschen können. Bitte gehen Sie zu Ihren Abonnementeinstellungen, um die Zahlung abzuschließen.', settleBalance: 'Zu Abo wechseln', diff --git a/src/languages/es.ts b/src/languages/es.ts index a776b7479367..7ef3cab434e4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4204,7 +4204,7 @@ ${amount} para ${merchant} - ${date}`, defaultNote: `Los recibos enviados a ${CONST.EMAIL.RECEIPTS} aparecerán en este espacio de trabajo.`, deleteConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo?', deleteWithCardsConfirmation: '¿Estás seguro de que quieres eliminar este espacio de trabajo? Se eliminarán todos los datos de las tarjetas y las tarjetas asignadas.', - deleteOpenExpensifyCardsError: 'Su empresa todavía tiene tarjetas Expensify activas.', + deleteOpenExpensifyCardsError: 'Tu empresa todavía tiene Tarjetas Expensify. Por favor, contacta con Concierge para eliminarlas.', outstandingBalanceWarning: 'Tienes un saldo pendiente que debe liquidarse antes de eliminar tu último espacio de trabajo. Por favor, ve a la configuración de tu suscripción para resolver el pago.', settleBalance: 'Ir a Suscripción', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 7ba2b143d6e2..d8ea247d9648 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4335,7 +4335,7 @@ ${amount} pour ${merchant} - ${date}`, defaultNote: `Les reçus envoyés à ${CONST.EMAIL.RECEIPTS} apparaîtront dans cet espace de travail.`, deleteConfirmation: 'Voulez-vous vraiment supprimer cet espace de travail ?', deleteWithCardsConfirmation: 'Voulez-vous vraiment supprimer cet espace de travail ? Cela supprimera tous les flux de cartes et les cartes assignées.', - deleteOpenExpensifyCardsError: 'Votre entreprise a encore des cartes Expensify actives.', + deleteOpenExpensifyCardsError: 'Votre entreprise a encore des Cartes Expensify. Veuillez contacter Concierge pour les supprimer.', outstandingBalanceWarning: 'Vous avez un solde impayé qui doit être réglé avant de supprimer votre dernier espace de travail. Veuillez accéder à vos paramètres d’abonnement pour résoudre le paiement.', settleBalance: 'Aller à l’abonnement', diff --git a/src/languages/it.ts b/src/languages/it.ts index 16c336b978b3..55120b41b1e3 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4309,7 +4309,7 @@ ${amount} per ${merchant} - ${date}`, defaultNote: `Le ricevute inviate a ${CONST.EMAIL.RECEIPTS} verranno visualizzate in questo workspace.`, deleteConfirmation: 'Sei sicuro di voler eliminare questo spazio di lavoro?', deleteWithCardsConfirmation: 'Sei sicuro di voler eliminare questo spazio di lavoro? Questa azione rimuoverà tutti i feed delle carte e le carte assegnate.', - deleteOpenExpensifyCardsError: 'La tua azienda ha ancora carte Expensify attive.', + deleteOpenExpensifyCardsError: 'La tua azienda ha ancora delle Carte Expensify. Per favore, contatta Concierge per rimuoverle.', outstandingBalanceWarning: 'Hai un saldo in sospeso che deve essere saldato prima di eliminare il tuo ultimo workspace. Vai alle impostazioni dell’abbonamento per risolvere il pagamento.', settleBalance: 'Vai all’abbonamento', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2b1a926f2547..2e0bb0ae6c2f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4278,7 +4278,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' defaultNote: `${CONST.EMAIL.RECEIPTS} に送信されたレシートは、このワークスペースに表示されます。`, deleteConfirmation: 'このワークスペースを削除してもよろしいですか?', deleteWithCardsConfirmation: 'このワークスペースを削除してもよろしいですか? すべてのカードフィードと割り当て済みカードが削除されます。', - deleteOpenExpensifyCardsError: 'あなたの会社にはまだ有効なExpensifyカードがあります。', + deleteOpenExpensifyCardsError: '御社にはまだ Expensify カードが残っています。削除するには、Concierge までお問い合わせください。', outstandingBalanceWarning: '最後のワークスペースを削除する前に精算する必要がある未払残高があります。支払いを解決するには、サブスクリプション設定に移動してください。', settleBalance: 'サブスクリプションに移動', unavailable: '利用できないワークスペース', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 58c0c33708d9..44f302d79e17 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4305,7 +4305,7 @@ ${amount} voor ${merchant} - ${date}`, defaultNote: `Bonnetjes die naar ${CONST.EMAIL.RECEIPTS} worden gestuurd, verschijnen in deze workspace.`, deleteConfirmation: 'Weet je zeker dat je deze werkruimte wilt verwijderen?', deleteWithCardsConfirmation: 'Weet je zeker dat je deze werkruimte wilt verwijderen? Hiermee worden alle kaartfeeds en toegewezen kaarten verwijderd.', - deleteOpenExpensifyCardsError: 'Uw bedrijf heeft nog actieve Expensify Cards.', + deleteOpenExpensifyCardsError: 'Je bedrijf heeft nog Expensify Kaarten. Neem contact op met Concierge om ze te verwijderen.', outstandingBalanceWarning: 'Je hebt een openstaand saldo dat moet worden vereffend voordat je je laatste werkruimte kunt verwijderen. Ga naar je abonnementsinstellingen om de betaling af te ronden.', settleBalance: 'Ga naar abonnement', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index ae2c2b5fa0a2..d0fb7aaa0b20 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4297,7 +4297,7 @@ ${amount} dla ${merchant} - ${date}`, defaultNote: `Paragony wysłane na ${CONST.EMAIL.RECEIPTS} pojawią się w tym obszarze roboczym.`, deleteConfirmation: 'Czy na pewno chcesz usunąć tę przestrzeń roboczą?', deleteWithCardsConfirmation: 'Na pewno chcesz usunąć tę przestrzeń roboczą? Spowoduje to usunięcie wszystkich źródeł kart i przypisanych kart.', - deleteOpenExpensifyCardsError: 'Twoja firma nadal ma aktywne karty Expensify.', + deleteOpenExpensifyCardsError: 'Twoja firma wciąż ma Karty Expensify. Prosimy, skontaktuj się z Concierge, aby je usunąć.', outstandingBalanceWarning: 'Masz zaległe saldo, które musi zostać uregulowane przed usunięciem ostatniego miejsca pracy. Przejdź do ustawień subskrypcji, aby uregulować płatność.', settleBalance: 'Przejdź do subskrypcji', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index de0e271772de..08d3edf9b4c5 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4298,7 +4298,7 @@ ${amount} para ${merchant} - ${date}`, defaultNote: `Recibos enviados para ${CONST.EMAIL.RECEIPTS} aparecerão neste workspace.`, deleteConfirmation: 'Tem certeza de que deseja excluir este workspace?', deleteWithCardsConfirmation: 'Tem certeza de que deseja excluir este workspace? Isso removerá todos os feeds de cartão e cartões atribuídos.', - deleteOpenExpensifyCardsError: 'Sua empresa ainda possui cartões Expensify ativos.', + deleteOpenExpensifyCardsError: 'Sua empresa ainda tem Cartões Expensify. Por favor, fale com o Concierge para removê-los.', outstandingBalanceWarning: 'Você tem um saldo pendente que precisa ser quitado antes de excluir seu último espaço de trabalho. Acesse as configurações de assinatura para resolver o pagamento.', settleBalance: 'Ir para a assinatura', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 1df8087de864..84fa44e59e06 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4202,7 +4202,7 @@ ${amount},商户:${merchant} - 日期:${date}`, defaultNote: `发送到 ${CONST.EMAIL.RECEIPTS} 的收据将显示在此工作区中。`, deleteConfirmation: '确定要删除此工作区吗?', deleteWithCardsConfirmation: '确定要删除此工作区吗?这将移除所有卡片数据源和已分配的卡片。', - deleteOpenExpensifyCardsError: '您的公司仍有未关闭的 Expensify 卡。', + deleteOpenExpensifyCardsError: '您的公司仍在使用 Expensify 卡。请联系 Concierge以停用它们。', outstandingBalanceWarning: '您有一笔未结清的余额,必须在删除最后一个工作区之前结清。请前往订阅设置以解决付款问题。', settleBalance: '前往订阅', unavailable: '工作区不可用', From 18939ab4cd1b1dddaf9feb7031449d6a63fcb2dd Mon Sep 17 00:00:00 2001 From: Jan Nowakowski Date: Fri, 12 Jun 2026 18:38:29 +0200 Subject: [PATCH 09/21] fix lint --- .../HTMLRenderers/ConciergeLinkRenderer.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx index e53f47ba7017..8dfa664618d2 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx @@ -1,7 +1,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding'; import React from 'react'; import type {StyleProp, TextStyle} from 'react-native'; -import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; +import type {CustomRendererProps, RenderersProps, TPhrasing, TText} from 'react-native-render-html'; import {TNodeChildrenRenderer, useRendererProps} from 'react-native-render-html'; import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils'; import Text from '@components/Text'; @@ -19,6 +19,12 @@ type ConciergeLinkRendererConfig = { onPress?: () => void; }; +type ConciergeLinkRenderersProps = RenderersProps & { + // Custom HTML renderer keys must use hyphenated tag names per react-native-render-html API + /* eslint-disable @typescript-eslint/naming-convention */ + 'concierge-link': ConciergeLinkRendererConfig; +}; + function ConciergeLinkRenderer({tnode, style}: ConciergeLinkRendererProps) { const styles = useThemeStyles(); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); @@ -26,7 +32,7 @@ function ConciergeLinkRenderer({tnode, style}: ConciergeLinkRendererProps) { const [betas] = useOnyx(ONYXKEYS.BETAS); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); - const {onPress: onPressFromProps} = (useRendererProps('concierge-link') ?? {}) as ConciergeLinkRendererConfig; + const {onPress: onPressFromProps} = useRendererProps('concierge-link') ?? {}; /** * Simple wrapper to create a stable reference without passing event args to navigation function. From 21fa9b41d93bb1a211173e4f54808b92dbb5edcb Mon Sep 17 00:00:00 2001 From: staszekscp Date: Wed, 17 Jun 2026 14:56:17 +0200 Subject: [PATCH 10/21] Fix narrow screen --- src/pages/workspace/WorkspaceOverviewPage.tsx | 53 +++++++++-------- src/pages/workspace/WorkspacesListPage.tsx | 59 +++++++++---------- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index b7d141675c7d..f412e5aaa2e0 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -61,6 +61,7 @@ import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import { canEditWorkspaceSettings, @@ -398,32 +399,44 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const prevIsPendingDeleteValue = prevIsPendingDeleteRef.current; prevIsPendingDeleteRef.current = isPendingDelete; - const policyLastErrorMessagePrompt = !!policyLastErrorMessage && ( - { - closeModal(); - hideDeleteWorkspaceErrorModal(); - }} - /> - ); - - if (isOffline && policyLastErrorMessage && hasExpensifyCard) { - if (isErrorModalShowingRef.current) { + const showDeleteWorkspaceErrorModal = () => { + if (isErrorModalShowingRef.current || !policyLastErrorMessage) { return; } + isErrorModalShowingRef.current = true; showConfirmModal({ title: translate('workspace.common.delete'), - prompt: policyLastErrorMessagePrompt, + prompt: ( + { + closeModal(); + hideDeleteWorkspaceErrorModal(); + }} + /> + ), confirmText: translate('common.buttonConfirm'), shouldShowCancelButton: false, success: false, + // Avoid stacking browser history entries when replacing the delete confirmation modal on narrow layouts. + shouldHandleNavigationBack: false, }).then(() => { isErrorModalShowingRef.current = false; hideDeleteWorkspaceErrorModal(); }); - return; + }; + + // Wait until the delete confirmation modal finishes its dismiss transition before showing the error + // modal. On narrow layouts it uses a bottom-docked modal with navigation-back handling, and opening + // the next modal too early can leave stale history state that prevents it from appearing. + const scheduleDeleteWorkspaceErrorModal = () => { + const handle = TransitionTracker.runAfterTransitions({callback: showDeleteWorkspaceErrorModal}); + return () => handle.cancel(); + }; + + if (isOffline && policyLastErrorMessage && hasExpensifyCard) { + return scheduleDeleteWorkspaceErrorModal(); } if (!prevIsPendingDeleteValue || isPendingDelete || !policyID) { @@ -442,18 +455,8 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa if (!isFocused || isErrorModalShowingRef.current) { return; } - isErrorModalShowingRef.current = true; - showConfirmModal({ - title: translate('workspace.common.delete'), - prompt: policyLastErrorMessagePrompt, - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - success: false, - }).then(() => { - isErrorModalShowingRef.current = false; - hideDeleteWorkspaceErrorModal(); - }); + return scheduleDeleteWorkspaceErrorModal(); }, [isOffline, hideDeleteWorkspaceErrorModal, showConfirmModal, translate, policyLastErrorMessage, isPendingDelete, isFocused, policyID, closeModal, hasExpensifyCard]); const onDeleteWorkspace = () => { diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index d06dbca8c2c5..7191da7fb9a1 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -292,34 +292,46 @@ function WorkspacesListPage() { const prevIsPendingDelete = prevIsPendingDeleteRef.current; prevIsPendingDeleteRef.current = isPendingDelete; - const policyToDeleteLatestErrorMessagePrompt = !!policyToDeleteLatestErrorMessage && ( - { - closeModal(); - hideDeleteWorkspaceErrorModal(); - }} - /> - ); - - // Handle showing error modal when offline and error occurs - if (isOffline && policyToDeleteLatestErrorMessage) { - if (isErrorModalShowingRef.current) { + const showDeleteWorkspaceErrorModal = () => { + if (isErrorModalShowingRef.current || !policyToDeleteLatestErrorMessage) { return; } + isErrorModalShowingRef.current = true; showConfirmModal({ title: translate('workspace.common.delete'), - prompt: policyToDeleteLatestErrorMessagePrompt, + prompt: ( + { + closeModal(); + hideDeleteWorkspaceErrorModal(); + }} + /> + ), confirmText: translate('common.buttonConfirm'), cancelText: translate('common.cancel'), success: false, shouldShowCancelButton: false, + // Avoid stacking browser history entries when replacing the delete confirmation modal on narrow layouts. + shouldHandleNavigationBack: false, }).then(() => { isErrorModalShowingRef.current = false; hideDeleteWorkspaceErrorModal(); }); - return; + }; + + // Wait until the delete confirmation modal finishes its dismiss transition before showing the error + // modal. On narrow layouts it uses a bottom-docked modal with navigation-back handling, and opening + // the next modal too early can leave stale history state that prevents it from appearing. + const scheduleDeleteWorkspaceErrorModal = () => { + const handle = TransitionTracker.runAfterTransitions({callback: showDeleteWorkspaceErrorModal}); + return () => handle.cancel(); + }; + + // Handle showing error modal when offline and error occurs + if (isOffline && policyToDeleteLatestErrorMessage) { + return scheduleDeleteWorkspaceErrorModal(); } if (!prevIsPendingDelete || isPendingDelete || !policyIDToDelete) { @@ -330,22 +342,7 @@ function WorkspacesListPage() { return; } - if (isErrorModalShowingRef.current) { - return; - } - isErrorModalShowingRef.current = true; - - showConfirmModal({ - title: translate('workspace.common.delete'), - prompt: policyToDeleteLatestErrorMessagePrompt, - confirmText: translate('common.buttonConfirm'), - cancelText: translate('common.cancel'), - success: false, - shouldShowCancelButton: false, - }).then(() => { - isErrorModalShowingRef.current = false; - hideDeleteWorkspaceErrorModal(); - }); + return scheduleDeleteWorkspaceErrorModal(); }, [isOffline, hideDeleteWorkspaceErrorModal, showConfirmModal, translate, policyToDeleteLatestErrorMessage, isPendingDelete, isFocused, policyIDToDelete, closeModal]); const startChangeOwnershipFlow = (policyID: string | undefined) => { From 689536cda61cd89b5f5eefdf9ba5e65e8220a16d Mon Sep 17 00:00:00 2001 From: staszekscp Date: Thu, 18 Jun 2026 11:48:04 +0200 Subject: [PATCH 11/21] Finish working version --- src/libs/actions/Policy/Policy.ts | 19 ++- src/pages/workspace/WorkspaceOverviewPage.tsx | 110 +++++++---------- .../deleteWorkspace/DeleteWorkspaceFlow.tsx | 113 ++++++++++-------- 3 files changed, 115 insertions(+), 127 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index bd13c0f0aa45..63d8dff1241f 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -406,6 +406,13 @@ function deleteWorkspace(params: DeleteWorkspaceActionParams) { const filteredPolicies = Object.values(policies ?? {}).filter((p): p is Policy => p?.id !== policyID); const workspaceAccountID = policy?.policyAccountID; + if (hasDeleteWorkspaceExpensifyCardsError) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.common.deleteOpenExpensifyCardsError'), + }); + return; + } + const optimisticData: Array< OnyxUpdate< | typeof ONYXKEYS.COLLECTION.POLICY @@ -429,7 +436,7 @@ function deleteWorkspace(params: DeleteWorkspaceActionParams) { value: { avatarURL: '', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - errors: hasDeleteWorkspaceExpensifyCardsError ? ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.common.deleteOpenExpensifyCardsError') : null, + errors: null, }, }, { @@ -613,16 +620,6 @@ function deleteWorkspace(params: DeleteWorkspaceActionParams) { }, }); - if (hasDeleteWorkspaceExpensifyCardsError) { - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { - errors: null, - }, - }); - } - reportIDToOptimisticCloseReportActionID[reportID] = optimisticClosedReportAction.reportActionID; for (const transactionViolationKey of Object.keys(transactionViolations ?? {})) { diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index f412e5aaa2e0..31fadd1477db 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -8,6 +8,7 @@ import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import ConfirmModal from '@components/ConfirmModal'; import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; import {useLockedAccountActions, useLockedAccountState} from '@components/LockedAccountModalProvider'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -45,7 +46,6 @@ import {clearInviteDraft, clearWorkspaceOwnerChangeFlow, requestWorkspaceOwnerCh import { calculateBillNewDot, clearAvatarErrors, - clearDeleteWorkspaceError, clearPolicyErrorField, deletePolicyRulesDocument, deleteWorkspace, @@ -61,7 +61,6 @@ import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import TransitionTracker from '@libs/Navigation/TransitionTracker'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import { canEditWorkspaceSettings, @@ -148,8 +147,6 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing !isEmptyObject(cardFeeds) || !isEmptyObject(cardsList) || ((policy?.areExpensifyCardsEnabled || policy?.areCompanyCardsEnabled) && policy?.policyAccountID); - const hasExpensifyCard = !!policy?.areExpensifyCardsEnabled && !isEmptyObject(cardsList); - const formattedAddress = !isEmptyObject(policy) && !isEmptyObject(policy.address) ? formatAddressToString(policy.address) : ''; const {reportsToArchive, transactionViolations} = useTransactionViolationOfWorkspace(policyID); @@ -272,7 +269,6 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const isPendingDelete = isPendingDeletePolicy(policy); const prevIsPendingDelete = usePrevious(isPendingDelete); const prevIsPendingDeleteRef = useRef(isPendingDelete); - const isErrorModalShowingRef = useRef(false); const policyLastErrorMessage = getLatestErrorMessage(policy); const mentionReportContextValue = useMemo(() => ({policyID, currentReportID: undefined, exactlyMatch: true}), [policyID]); @@ -316,7 +312,24 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - const hasDeleteWorkspaceExpensifyCardsError = !!hasExpensifyCard && !!isOffline; + const hasExpensifyCardsEnabledOnWorkspace = !!policy?.areExpensifyCardsEnabled && !!policy?.policyAccountID; + const hasDeleteWorkspaceExpensifyCardsError = !!hasExpensifyCardsEnabledOnWorkspace && !!isOffline; + const [shouldShowDeleteWorkspaceErrorModal, setShouldShowDeleteWorkspaceErrorModal] = useState(false); + + const hideDeleteWorkspaceErrorModal = useCallback(() => { + setShouldShowDeleteWorkspaceErrorModal(false); + }, []); + + const isDeleteWorkspaceErrorModalOpen = shouldShowDeleteWorkspaceErrorModal && hasExpensifyCardsEnabledOnWorkspace && isFocused; + + const deleteWorkspaceErrorPrompt = ( + + + + ); const confirmDelete = () => { if (!policyID || !policyName) { @@ -342,13 +355,10 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa currentUserAccountID: accountID, accountIDToLogin: accountIDToLogin ?? {}, }); - if (isOffline) { - closeModal(); - - if (hasDeleteWorkspaceExpensifyCardsError) { - return; - } + if (hasDeleteWorkspaceExpensifyCardsError) { + setShouldShowDeleteWorkspaceErrorModal(true); + } else if (isOffline) { goBackFromInvalidPolicy(); } }; @@ -360,7 +370,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa confirmText: translate('common.delete'), cancelText: translate('common.cancel'), danger: true, - isConfirmLoading: isPendingDelete, + ...(hasDeleteWorkspaceExpensifyCardsError ? {} : {isConfirmLoading: isPendingDelete}), }).then((result) => { if (result.action !== ModalActions.CONFIRM) { return; @@ -384,10 +394,6 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa goBackFromInvalidPolicy(); }; - const hideDeleteWorkspaceErrorModal = () => { - clearDeleteWorkspaceError(policyID); - }; - useEffect(() => { if (isLoadingBill) { return; @@ -399,44 +405,8 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const prevIsPendingDeleteValue = prevIsPendingDeleteRef.current; prevIsPendingDeleteRef.current = isPendingDelete; - const showDeleteWorkspaceErrorModal = () => { - if (isErrorModalShowingRef.current || !policyLastErrorMessage) { - return; - } - - isErrorModalShowingRef.current = true; - showConfirmModal({ - title: translate('workspace.common.delete'), - prompt: ( - { - closeModal(); - hideDeleteWorkspaceErrorModal(); - }} - /> - ), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - success: false, - // Avoid stacking browser history entries when replacing the delete confirmation modal on narrow layouts. - shouldHandleNavigationBack: false, - }).then(() => { - isErrorModalShowingRef.current = false; - hideDeleteWorkspaceErrorModal(); - }); - }; - - // Wait until the delete confirmation modal finishes its dismiss transition before showing the error - // modal. On narrow layouts it uses a bottom-docked modal with navigation-back handling, and opening - // the next modal too early can leave stale history state that prevents it from appearing. - const scheduleDeleteWorkspaceErrorModal = () => { - const handle = TransitionTracker.runAfterTransitions({callback: showDeleteWorkspaceErrorModal}); - return () => handle.cancel(); - }; - - if (isOffline && policyLastErrorMessage && hasExpensifyCard) { - return scheduleDeleteWorkspaceErrorModal(); + if (isOffline) { + return; } if (!prevIsPendingDeleteValue || isPendingDelete || !policyID) { @@ -445,19 +415,13 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa closeModal(); - if (!policyLastErrorMessage) { - if (!(isOffline && hasExpensifyCard)) { - goBackFromInvalidPolicy(); - } + if (policyLastErrorMessage && hasExpensifyCardsEnabledOnWorkspace) { + setShouldShowDeleteWorkspaceErrorModal(true); return; } - if (!isFocused || isErrorModalShowingRef.current) { - return; - } - - return scheduleDeleteWorkspaceErrorModal(); - }, [isOffline, hideDeleteWorkspaceErrorModal, showConfirmModal, translate, policyLastErrorMessage, isPendingDelete, isFocused, policyID, closeModal, hasExpensifyCard]); + goBackFromInvalidPolicy(); + }, [isOffline, isPendingDelete, policyID, policyLastErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal]); const onDeleteWorkspace = () => { if (shouldBlockWorkspaceDeletionForInvoicifyUser(isSubscriptionTypeOfInvoicing(subscriptionType), ownerPolicies, policyID)) { @@ -664,9 +628,25 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa ); + const deleteWorkspaceErrorModal = ( + // eslint-disable-next-line @typescript-eslint/no-deprecated -- Local modal avoids stacking issues with the global delete confirmation modal on mobile. + + ); + const modals = ( <> {outstandingBalanceModal} + {deleteWorkspaceErrorModal} {!!pendingRulesDocumentFile && ( { + setShouldShowDeleteWorkspaceErrorModal(false); + onDismiss(); + }, [onDismiss]); + + const isDeleteWorkspaceErrorModalOpen = shouldShowDeleteWorkspaceErrorModal && hasExpensifyCardsEnabledOnWorkspace && isFocused; + + const deleteWorkspaceErrorPrompt = ( + + + + ); // Always invoked after a re-render (from the start effect below for normal deletes, or from usePayAndDowngrade for billed deletes), // so the workspace being deleted and its derived data are read from the latest state. @@ -112,7 +134,7 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { confirmText: translate('common.delete'), cancelText: translate('common.cancel'), danger: true, - isConfirmLoading: isPendingDelete, + ...(hasDeleteWorkspaceExpensifyCardsError ? {} : {isConfirmLoading: isPendingDelete}), }).then((result) => { if (!policyName || result.action !== ModalActions.CONFIRM) { onDismiss(); @@ -138,11 +160,11 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { currentUserAccountID: session?.accountID ?? CONST.DEFAULT_NUMBER_ID, accountIDToLogin: accountIDToLogin ?? {}, }); - if (isOffline) { - closeModal(); - if (!hasDeleteWorkspaceExpensifyCardsError) { - onDismiss(); - } + + if (hasDeleteWorkspaceExpensifyCardsError) { + setShouldShowDeleteWorkspaceErrorModal(true); + } else if (isOffline) { + onDismiss(); } }); }; @@ -177,57 +199,46 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { continueDeleteWorkspace(); }); - const hideDeleteWorkspaceErrorModal = () => { - if (policy) { - dismissWorkspaceError(policy.id, policy.pendingAction); - } - onDismiss(); - }; - - const showErrorModal = () => { - if (isErrorModalShowingRef.current) { - return; - } - isErrorModalShowingRef.current = true; - showConfirmModal({ - title: translate('workspace.common.delete'), - prompt: policyLatestErrorMessage, - confirmText: translate('common.buttonConfirm'), - cancelText: translate('common.cancel'), - success: false, - shouldShowCancelButton: false, - }).then(() => { - isErrorModalShowingRef.current = false; - hideDeleteWorkspaceErrorModal(); - }); - }; - useEffect(() => { - const prevIsPendingDelete = prevIsPendingDeleteRef.current; - prevIsPendingDeleteRef.current = isPendingDelete; - - // Handle showing error modal when offline and error occurs - if (isOffline && policyLatestErrorMessage) { - showErrorModal(); + if (isOffline) { return; } if (!prevIsPendingDelete || isPendingDelete) { return; } + closeModal(); - if (!isFocused || !policyLatestErrorMessage) { - // The deletion either succeeded or there is no error modal to show, so the flow is finished. - if (!isErrorModalShowingRef.current) { - onDismiss(); - } + + if (policyLatestErrorMessage && hasExpensifyCardsEnabledOnWorkspace) { + setShouldShowDeleteWorkspaceErrorModal(true); return; } - showErrorModal(); - }, [isOffline, hideDeleteWorkspaceErrorModal, showConfirmModal, translate, policyLatestErrorMessage, isPendingDelete, isFocused, closeModal, onDismiss, showErrorModal]); + onDismiss(); + }, [isOffline, isPendingDelete, prevIsPendingDelete, policyLatestErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal, onDismiss]); + + const deleteWorkspaceErrorModal = ( + // eslint-disable-next-line @typescript-eslint/no-deprecated -- Local modal avoids stacking issues with the global delete confirmation modal on mobile. + + ); - return outstandingBalanceModal; + return ( + <> + {outstandingBalanceModal} + {deleteWorkspaceErrorModal} + + ); } export default DeleteWorkspaceFlow; From 3abb92a8f7d51dea1f59985cd53dd665208a5b79 Mon Sep 17 00:00:00 2001 From: staszekscp Date: Thu, 18 Jun 2026 11:57:54 +0200 Subject: [PATCH 12/21] Fix React import --- src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx index 1433d803f2d6..14d548c3bb94 100644 --- a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx +++ b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx @@ -1,5 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; -import {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import {ModalActions} from '@components/Modal/Global/ModalContext'; From da15d0b1db3899f6961db79fae11d4a7cd41151a Mon Sep 17 00:00:00 2001 From: staszekscp Date: Thu, 18 Jun 2026 12:28:32 +0200 Subject: [PATCH 13/21] Fix Knip --- src/libs/actions/Policy/Policy.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 63d8dff1241f..bf8ad56ef838 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -7521,7 +7521,6 @@ export { deleteWorkspace, updateAddress, updateLastAccessedWorkspace, - clearDeleteWorkspaceError, dismissWorkspaceError, setWorkspaceDefaultSpendCategory, getDisplayNameForWorkspace, From c06f23d1bad76be9bbee36050a825612522baa90 Mon Sep 17 00:00:00 2001 From: staszekscp Date: Fri, 19 Jun 2026 08:56:58 +0200 Subject: [PATCH 14/21] Fix linter --- src/pages/workspace/WorkspaceOverviewPage.tsx | 17 ++++++++++------- .../deleteWorkspace/DeleteWorkspaceFlow.tsx | 9 ++++++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 31fadd1477db..a8bd483d86b6 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -268,7 +268,6 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const isFocused = useIsFocused(); const isPendingDelete = isPendingDeletePolicy(policy); const prevIsPendingDelete = usePrevious(isPendingDelete); - const prevIsPendingDeleteRef = useRef(isPendingDelete); const policyLastErrorMessage = getLatestErrorMessage(policy); const mentionReportContextValue = useMemo(() => ({policyID, currentReportID: undefined, exactlyMatch: true}), [policyID]); @@ -315,6 +314,14 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const hasExpensifyCardsEnabledOnWorkspace = !!policy?.areExpensifyCardsEnabled && !!policy?.policyAccountID; const hasDeleteWorkspaceExpensifyCardsError = !!hasExpensifyCardsEnabledOnWorkspace && !!isOffline; const [shouldShowDeleteWorkspaceErrorModal, setShouldShowDeleteWorkspaceErrorModal] = useState(false); + const didCompletePendingDelete = !isOffline && prevIsPendingDelete && !isPendingDelete && !!policyID; + const shouldLatchDeleteWorkspaceErrorModal = didCompletePendingDelete && !!policyLastErrorMessage && hasExpensifyCardsEnabledOnWorkspace; + + // Latch open: show the delete-workspace error modal when pending delete completes with an Expensify Cards error. + // Using setState-during-render (React-recommended derived state pattern) instead of useEffect to satisfy react-hooks/set-state-in-effect. + if (shouldLatchDeleteWorkspaceErrorModal && !shouldShowDeleteWorkspaceErrorModal) { + setShouldShowDeleteWorkspaceErrorModal(true); + } const hideDeleteWorkspaceErrorModal = useCallback(() => { setShouldShowDeleteWorkspaceErrorModal(false); @@ -402,26 +409,22 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa }, [isLoadingBill]); useEffect(() => { - const prevIsPendingDeleteValue = prevIsPendingDeleteRef.current; - prevIsPendingDeleteRef.current = isPendingDelete; - if (isOffline) { return; } - if (!prevIsPendingDeleteValue || isPendingDelete || !policyID) { + if (!prevIsPendingDelete || isPendingDelete || !policyID) { return; } closeModal(); if (policyLastErrorMessage && hasExpensifyCardsEnabledOnWorkspace) { - setShouldShowDeleteWorkspaceErrorModal(true); return; } goBackFromInvalidPolicy(); - }, [isOffline, isPendingDelete, policyID, policyLastErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal]); + }, [isOffline, isPendingDelete, prevIsPendingDelete, policyID, policyLastErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal]); const onDeleteWorkspace = () => { if (shouldBlockWorkspaceDeletionForInvoicifyUser(isSubscriptionTypeOfInvoicing(subscriptionType), ownerPolicies, policyID)) { diff --git a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx index 14d548c3bb94..66747def2075 100644 --- a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx +++ b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx @@ -106,6 +106,14 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { const shouldCalculateBillNewDot = !!canDowngrade && ownedPaidPoliciesCounts?.total === 1; const {shouldBlockDeletion, outstandingBalanceModal} = useOutstandingBalanceGuard(ownedPaidPoliciesCounts?.active ?? 0, onDismiss); const [shouldShowDeleteWorkspaceErrorModal, setShouldShowDeleteWorkspaceErrorModal] = useState(false); + const didCompletePendingDelete = !isOffline && prevIsPendingDelete && !isPendingDelete; + const shouldLatchDeleteWorkspaceErrorModal = didCompletePendingDelete && !!policyLatestErrorMessage && hasExpensifyCardsEnabledOnWorkspace; + + // Latch open: show the delete-workspace error modal when pending delete completes with an Expensify Cards error. + // Using setState-during-render (React-recommended derived state pattern) instead of useEffect to satisfy react-hooks/set-state-in-effect. + if (shouldLatchDeleteWorkspaceErrorModal && !shouldShowDeleteWorkspaceErrorModal) { + setShouldShowDeleteWorkspaceErrorModal(true); + } const hideDeleteWorkspaceErrorModal = useCallback(() => { setShouldShowDeleteWorkspaceErrorModal(false); @@ -211,7 +219,6 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { closeModal(); if (policyLatestErrorMessage && hasExpensifyCardsEnabledOnWorkspace) { - setShouldShowDeleteWorkspaceErrorModal(true); return; } From 8d3d91293f44875b62af9aca201c33af3327dfe8 Mon Sep 17 00:00:00 2001 From: staszekscp Date: Fri, 19 Jun 2026 11:58:10 +0200 Subject: [PATCH 15/21] Switch to useConfirmModal --- .../deleteWorkspace/DeleteWorkspaceFlow.tsx | 79 ++++++++----------- 1 file changed, 31 insertions(+), 48 deletions(-) diff --git a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx index 66747def2075..97c4a41c17eb 100644 --- a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx +++ b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx @@ -1,7 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import ConfirmModal from '@components/ConfirmModal'; import {ModalActions} from '@components/Modal/Global/ModalContext'; import RenderHTML from '@components/RenderHTML'; import useCardFeeds from '@hooks/useCardFeeds'; @@ -105,31 +104,34 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { const shouldCalculateBillNewDot = !!canDowngrade && ownedPaidPoliciesCounts?.total === 1; const {shouldBlockDeletion, outstandingBalanceModal} = useOutstandingBalanceGuard(ownedPaidPoliciesCounts?.active ?? 0, onDismiss); - const [shouldShowDeleteWorkspaceErrorModal, setShouldShowDeleteWorkspaceErrorModal] = useState(false); - const didCompletePendingDelete = !isOffline && prevIsPendingDelete && !isPendingDelete; - const shouldLatchDeleteWorkspaceErrorModal = didCompletePendingDelete && !!policyLatestErrorMessage && hasExpensifyCardsEnabledOnWorkspace; - - // Latch open: show the delete-workspace error modal when pending delete completes with an Expensify Cards error. - // Using setState-during-render (React-recommended derived state pattern) instead of useEffect to satisfy react-hooks/set-state-in-effect. - if (shouldLatchDeleteWorkspaceErrorModal && !shouldShowDeleteWorkspaceErrorModal) { - setShouldShowDeleteWorkspaceErrorModal(true); - } - - const hideDeleteWorkspaceErrorModal = useCallback(() => { - setShouldShowDeleteWorkspaceErrorModal(false); - onDismiss(); - }, [onDismiss]); - const isDeleteWorkspaceErrorModalOpen = shouldShowDeleteWorkspaceErrorModal && hasExpensifyCardsEnabledOnWorkspace && isFocused; + const showDeleteWorkspaceErrorModal = useCallback(() => { + if (!isFocused) { + onDismiss(); + return; + } - const deleteWorkspaceErrorPrompt = ( - - - - ); + showConfirmModal({ + title: translate('workspace.common.delete'), + prompt: ( + + { + closeModal(); + onDismiss(); + }} + /> + + ), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + success: false, + shouldHandleNavigationBack: false, + }).then(() => { + onDismiss(); + }); + }, [closeModal, isFocused, onDismiss, showConfirmModal, styles.flexRow, styles.renderHTML, translate]); // Always invoked after a re-render (from the start effect below for normal deletes, or from usePayAndDowngrade for billed deletes), // so the workspace being deleted and its derived data are read from the latest state. @@ -170,7 +172,7 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { }); if (hasDeleteWorkspaceExpensifyCardsError) { - setShouldShowDeleteWorkspaceErrorModal(true); + showDeleteWorkspaceErrorModal(); } else if (isOffline) { onDismiss(); } @@ -219,33 +221,14 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { closeModal(); if (policyLatestErrorMessage && hasExpensifyCardsEnabledOnWorkspace) { + showDeleteWorkspaceErrorModal(); return; } onDismiss(); - }, [isOffline, isPendingDelete, prevIsPendingDelete, policyLatestErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal, onDismiss]); - - const deleteWorkspaceErrorModal = ( - // eslint-disable-next-line @typescript-eslint/no-deprecated -- Local modal avoids stacking issues with the global delete confirmation modal on mobile. - - ); + }, [isOffline, isPendingDelete, prevIsPendingDelete, policyLatestErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal, onDismiss, showDeleteWorkspaceErrorModal]); - return ( - <> - {outstandingBalanceModal} - {deleteWorkspaceErrorModal} - - ); + return outstandingBalanceModal; } export default DeleteWorkspaceFlow; From 765eb1a3adb9ebfbcab364900ec6cfc7123b37b5 Mon Sep 17 00:00:00 2001 From: staszekscp Date: Fri, 19 Jun 2026 14:40:44 +0200 Subject: [PATCH 16/21] Switch to useConfirmModal WorkspaceOverviewPage --- src/pages/workspace/WorkspaceOverviewPage.tsx | 64 +++++++------------ 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index a8bd483d86b6..454df120fa3f 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -8,7 +8,6 @@ import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; -import ConfirmModal from '@components/ConfirmModal'; import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; import {useLockedAccountActions, useLockedAccountState} from '@components/LockedAccountModalProvider'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -313,30 +312,28 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const hasExpensifyCardsEnabledOnWorkspace = !!policy?.areExpensifyCardsEnabled && !!policy?.policyAccountID; const hasDeleteWorkspaceExpensifyCardsError = !!hasExpensifyCardsEnabledOnWorkspace && !!isOffline; - const [shouldShowDeleteWorkspaceErrorModal, setShouldShowDeleteWorkspaceErrorModal] = useState(false); - const didCompletePendingDelete = !isOffline && prevIsPendingDelete && !isPendingDelete && !!policyID; - const shouldLatchDeleteWorkspaceErrorModal = didCompletePendingDelete && !!policyLastErrorMessage && hasExpensifyCardsEnabledOnWorkspace; - - // Latch open: show the delete-workspace error modal when pending delete completes with an Expensify Cards error. - // Using setState-during-render (React-recommended derived state pattern) instead of useEffect to satisfy react-hooks/set-state-in-effect. - if (shouldLatchDeleteWorkspaceErrorModal && !shouldShowDeleteWorkspaceErrorModal) { - setShouldShowDeleteWorkspaceErrorModal(true); - } - - const hideDeleteWorkspaceErrorModal = useCallback(() => { - setShouldShowDeleteWorkspaceErrorModal(false); - }, []); - const isDeleteWorkspaceErrorModalOpen = shouldShowDeleteWorkspaceErrorModal && hasExpensifyCardsEnabledOnWorkspace && isFocused; + const showDeleteWorkspaceErrorModal = useCallback(() => { + if (!isFocused) { + return; + } - const deleteWorkspaceErrorPrompt = ( - - - - ); + showConfirmModal({ + title: translate('workspace.common.delete'), + prompt: ( + + + + ), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + success: false, + shouldHandleNavigationBack: false, + }); + }, [closeModal, isFocused, showConfirmModal, styles.flexRow, styles.renderHTML, translate]); const confirmDelete = () => { if (!policyID || !policyName) { @@ -364,7 +361,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa }); if (hasDeleteWorkspaceExpensifyCardsError) { - setShouldShowDeleteWorkspaceErrorModal(true); + showDeleteWorkspaceErrorModal(); } else if (isOffline) { goBackFromInvalidPolicy(); } @@ -420,11 +417,12 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa closeModal(); if (policyLastErrorMessage && hasExpensifyCardsEnabledOnWorkspace) { + showDeleteWorkspaceErrorModal(); return; } goBackFromInvalidPolicy(); - }, [isOffline, isPendingDelete, prevIsPendingDelete, policyID, policyLastErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal]); + }, [isOffline, isPendingDelete, prevIsPendingDelete, policyID, policyLastErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal, showDeleteWorkspaceErrorModal]); const onDeleteWorkspace = () => { if (shouldBlockWorkspaceDeletionForInvoicifyUser(isSubscriptionTypeOfInvoicing(subscriptionType), ownerPolicies, policyID)) { @@ -631,25 +629,9 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa ); - const deleteWorkspaceErrorModal = ( - // eslint-disable-next-line @typescript-eslint/no-deprecated -- Local modal avoids stacking issues with the global delete confirmation modal on mobile. - - ); - const modals = ( <> {outstandingBalanceModal} - {deleteWorkspaceErrorModal} {!!pendingRulesDocumentFile && ( Date: Mon, 22 Jun 2026 10:47:26 +0200 Subject: [PATCH 17/21] Apply review changes --- src/pages/workspace/WorkspaceOverviewPage.tsx | 205 ++++-------------- .../deleteWorkspace/DeleteWorkspaceFlow.tsx | 65 +++++- 2 files changed, 95 insertions(+), 175 deletions(-) diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 454df120fa3f..a4fb437f078c 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -17,11 +17,9 @@ import {usePersonalDetails} from '@components/OnyxListItemProvider'; import PDFThumbnail from '@components/PDFThumbnail'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import RenderHTML from '@components/RenderHTML'; import Section from '@components/Section'; import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; -import useCardFeeds from '@hooks/useCardFeeds'; import useConfirmModal from '@hooks/useConfirmModal'; import {useCurrencyListActions} from '@hooks/useCurrencyList'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -30,24 +28,18 @@ import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hook import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import useOutstandingBalanceGuard from '@hooks/useOutstandingBalanceGuard'; -import usePayAndDowngrade from '@hooks/usePayAndDowngrade'; import usePrevious from '@hooks/usePrevious'; -import usePrivateSubscription from '@hooks/usePrivateSubscription'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useShouldBlockCurrencyChange from '@hooks/useShouldBlockCurrencyChange'; import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine'; import useThemeStyles from '@hooks/useThemeStyles'; -import useTransactionViolationOfWorkspace from '@hooks/useTransactionViolationOfWorkspace'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {close} from '@libs/actions/Modal'; import {clearInviteDraft, clearWorkspaceOwnerChangeFlow, requestWorkspaceOwnerChange} from '@libs/actions/Policy/Member'; import { - calculateBillNewDot, clearAvatarErrors, clearPolicyErrorField, deletePolicyRulesDocument, - deleteWorkspace, deleteWorkspaceAvatar, leaveWorkspace, openPolicyProfilePage, @@ -55,8 +47,8 @@ import { updatePolicyRulesDocument, updateWorkspaceAvatar, } from '@libs/actions/Policy/Policy'; -import {filterInactiveCards, getCardSettings} from '@libs/CardUtils'; -import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils'; +import {getCardSettings} from '@libs/CardUtils'; +import {getLatestErrorField} from '@libs/ErrorUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -72,23 +64,21 @@ import { isPolicyApprover, isPolicyAuditor, isPolicyOwner, - shouldBlockWorkspaceDeletionForInvoicifyUser, } from '@libs/PolicyUtils'; import {formatAddressToString} from '@libs/ReportActionsUtils'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import shouldRenderTransferOwnerButton from '@libs/shouldRenderTransferOwnerButton'; import StringUtils from '@libs/StringUtils'; -import {isSubscriptionTypeOfInvoicing, shouldCalculateBillNewDot} from '@libs/SubscriptionUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import {accountIDToLoginSelector} from '@src/selectors/PersonalDetails'; -import {ownerPoliciesSelector} from '@src/selectors/Policy'; -import {reimbursementAccountErrorSelector} from '@src/selectors/ReimbursementAccount'; +import {canDowngradeSelector} from '@src/selectors/Account'; +import {createOwnedPaidPoliciesCountsSelector} from '@src/selectors/Policy'; import type {FileObject} from '@src/types/utils/Attachment'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import DeleteWorkspaceFlow from './deleteWorkspace/DeleteWorkspaceFlow'; import type {WithPolicyProps} from './withPolicy'; import withPolicy from './withPolicy'; import WorkspacePageWithSections from './WorkspacePageWithSections'; @@ -100,7 +90,7 @@ const rulesDocumentMenuPositionStyle = {top: variables.spacing2, right: variable function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: WorkspaceOverviewPageProps) { const styles = useThemeStyles(); - const {translate, localeCompare} = useLocalize(); + const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); @@ -112,11 +102,20 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const routePolicyID = route.params.policyID; const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [fundList] = useOnyx(ONYXKEYS.FUND_LIST); - const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID); const [isComingFromGlobalReimbursementsFlow] = useOnyx(ONYXKEYS.IS_COMING_FROM_GLOBAL_REIMBURSEMENTS_FLOW); - const [lastAccessedWorkspacePolicyID] = useOnyx(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID); - const [reimbursementAccountError] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {selector: reimbursementAccountErrorSelector}); - const {showConfirmModal, closeModal} = useConfirmModal(); + const {showConfirmModal} = useConfirmModal(); + const [isDeleteWorkspaceFlowVisible, setIsDeleteWorkspaceFlowVisible] = useState(false); + + // Primitive-valued subscriptions configuring the Delete menu item (popover behavior and the loading spinner) + // before a deletion starts. The deletion itself is handled by DeleteWorkspaceFlow, mounted on demand below. + const [canDowngrade] = useOnyx(ONYXKEYS.ACCOUNT, {selector: canDowngradeSelector}); + const [amountOwed] = useOnyx(ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED); + const [isLoadingBill] = useOnyx(ONYXKEYS.IS_LOADING_BILL_WHEN_DOWNGRADE); + const [ownedPaidPoliciesCounts] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: createOwnedPaidPoliciesCountsSelector(currentUserPersonalDetails.accountID)}, [ + currentUserPersonalDetails.accountID, + ]); + const shouldCalculateBillNewDot = !!canDowngrade && ownedPaidPoliciesCounts?.total === 1; + const wouldBlockDeletion = (amountOwed ?? 0) > 0 && ownedPaidPoliciesCounts?.active === 1; // When we create a new workspace, the policy prop will be empty on the first render. Therefore, we have to use policyDraft until policy has been set in Onyx. const policy = policyDraft?.id ? policyDraft : policyProp; @@ -134,22 +133,8 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const currencySymbol = getCurrencySymbol(outputCurrency) ?? ''; const formattedCurrency = !isEmptyObject(policy) ? `${outputCurrency} - ${currencySymbol}` : ''; - // We need this to update translation for deleting a workspace when it has third party card feeds or expensify card assigned. - const workspaceAccountID = policy?.policyAccountID ?? CONST.DEFAULT_NUMBER_ID; - const [cardFeeds, , defaultCardFeeds] = useCardFeeds(policyID); - const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`); - const [lastSelectedExpensifyCardFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED}${policyID}`); - const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`, { - selector: filterInactiveCards, - }); - const hasCardFeedOrExpensifyCard = - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - !isEmptyObject(cardFeeds) || !isEmptyObject(cardsList) || ((policy?.areExpensifyCardsEnabled || policy?.areCompanyCardsEnabled) && policy?.policyAccountID); - const formattedAddress = !isEmptyObject(policy) && !isEmptyObject(policy.address) ? formatAddressToString(policy.address) : ''; - const {reportsToArchive, transactionViolations} = useTransactionViolationOfWorkspace(policyID); - const onPressCurrency = () => { if (!policyID) { return; @@ -201,7 +186,6 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const shouldShowAddress = !readOnly || !!formattedAddress; const {isAccountLocked} = useLockedAccountState(); const {showLockedAccountModal} = useLockedAccountActions(); - const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD); const [pendingRulesDocumentFile, setPendingRulesDocumentFile] = useState(); const [session] = useOnyx(ONYXKEYS.SESSION); @@ -255,19 +239,10 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const shouldShowRulesDocumentSubSection = isPolicyAdmin || hasRulesDocument; const personalDetails = usePersonalDetails(); - const [accountIDToLogin] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: accountIDToLoginSelector(reportsToArchive)}); - const privateSubscription = usePrivateSubscription(); - const accountID = currentUserPersonalDetails?.accountID; - - const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); - const ownerPolicies = ownerPoliciesSelector(policies, accountID); - const activeOwnerPoliciesCount = ownerPolicies.filter((p) => !isPendingDeletePolicy(p)).length; - const {shouldBlockDeletion, wouldBlockDeletion, outstandingBalanceModal} = useOutstandingBalanceGuard(activeOwnerPoliciesCount); const isFocused = useIsFocused(); const isPendingDelete = isPendingDeletePolicy(policy); const prevIsPendingDelete = usePrevious(isPendingDelete); - const policyLastErrorMessage = getLatestErrorMessage(policy); const mentionReportContextValue = useMemo(() => ({policyID, currentReportID: undefined, exactlyMatch: true}), [policyID]); @@ -278,10 +253,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa openPolicyProfilePage(routePolicyID); }, [policyDraftID, isFocused, routePolicyID]); - const {isOffline} = useNetwork({onReconnect: fetchPolicyData}); - - const subscriptionType = privateSubscription?.type; - const canDowngrade = account?.canDowngrade; + useNetwork({onReconnect: fetchPolicyData}); // We have the same focus effect in the WorkspaceInitialPage, this way we can get the policy data in narrow // as well as in the wide layout when looking at policy settings. @@ -308,85 +280,6 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa [expensifyIcons.FallbackWorkspaceAvatar, policy?.avatarURL, policyID, policyName, styles.alignSelfCenter, styles.avatarXLarge], ); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); - - const hasExpensifyCardsEnabledOnWorkspace = !!policy?.areExpensifyCardsEnabled && !!policy?.policyAccountID; - const hasDeleteWorkspaceExpensifyCardsError = !!hasExpensifyCardsEnabledOnWorkspace && !!isOffline; - - const showDeleteWorkspaceErrorModal = useCallback(() => { - if (!isFocused) { - return; - } - - showConfirmModal({ - title: translate('workspace.common.delete'), - prompt: ( - - - - ), - confirmText: translate('common.buttonConfirm'), - shouldShowCancelButton: false, - success: false, - shouldHandleNavigationBack: false, - }); - }, [closeModal, isFocused, showConfirmModal, styles.flexRow, styles.renderHTML, translate]); - - const confirmDelete = () => { - if (!policyID || !policyName) { - return; - } - - deleteWorkspace({ - policies, - policyID, - activePolicyID, - policyName, - lastAccessedWorkspacePolicyID, - policyCardFeeds: defaultCardFeeds, - lastSelectedFeed, - lastSelectedExpensifyCardFeed, - reportsToArchive, - transactionViolations, - reimbursementAccountError, - lastUsedPaymentMethods: lastPaymentMethod, - localeCompare, - personalPolicyID, - hasDeleteWorkspaceExpensifyCardsError, - currentUserAccountID: accountID, - accountIDToLogin: accountIDToLogin ?? {}, - }); - - if (hasDeleteWorkspaceExpensifyCardsError) { - showDeleteWorkspaceErrorModal(); - } else if (isOffline) { - goBackFromInvalidPolicy(); - } - }; - - const continueDeleteWorkspace = () => { - showConfirmModal({ - title: translate('workspace.common.delete'), - prompt: hasCardFeedOrExpensifyCard ? translate('workspace.common.deleteWithCardsConfirmation') : translate('workspace.common.deleteConfirmation'), - confirmText: translate('common.delete'), - cancelText: translate('common.cancel'), - danger: true, - ...(hasDeleteWorkspaceExpensifyCardsError ? {} : {isConfirmLoading: isPendingDelete}), - }).then((result) => { - if (result.action !== ModalActions.CONFIRM) { - return; - } - - confirmDelete(); - }); - }; - - const {setIsDeletingPaidWorkspace, isLoadingBill}: {setIsDeletingPaidWorkspace: (value: boolean) => void; isLoadingBill: boolean | undefined} = - usePayAndDowngrade(continueDeleteWorkspace); - const dropdownMenuRef = useRef<{setIsMenuVisible: (visible: boolean) => void} | null>(null); const handleLeaveWorkspace = () => { @@ -405,44 +298,6 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa dropdownMenuRef.current?.setIsMenuVisible(false); }, [isLoadingBill]); - useEffect(() => { - if (isOffline) { - return; - } - - if (!prevIsPendingDelete || isPendingDelete || !policyID) { - return; - } - - closeModal(); - - if (policyLastErrorMessage && hasExpensifyCardsEnabledOnWorkspace) { - showDeleteWorkspaceErrorModal(); - return; - } - - goBackFromInvalidPolicy(); - }, [isOffline, isPendingDelete, prevIsPendingDelete, policyID, policyLastErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal, showDeleteWorkspaceErrorModal]); - - const onDeleteWorkspace = () => { - if (shouldBlockWorkspaceDeletionForInvoicifyUser(isSubscriptionTypeOfInvoicing(subscriptionType), ownerPolicies, policyID)) { - Navigation.navigate(ROUTES.SETTINGS_SUBSCRIPTION_DOWNGRADE_BLOCKED.getRoute(Navigation.getActiveRoute())); - return; - } - - if (shouldBlockDeletion()) { - return; - } - - if (shouldCalculateBillNewDot(currentUserPersonalDetails.accountID, canDowngrade, policies)) { - setIsDeletingPaidWorkspace(true); - calculateBillNewDot(); - return; - } - - continueDeleteWorkspace(); - }; - const handleBackButtonPress = () => { if (isComingFromGlobalReimbursementsFlow) { setIsComingFromGlobalReimbursementsFlow(false); @@ -569,10 +424,17 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa value: 'delete', text: translate('common.delete'), icon: expensifyIcons.Trashcan, - onSelected: onDeleteWorkspace, + onSelected: () => { + if (isLoadingBill) { + return; + } + + // All the pre-deletion checks and the confirmation modal are handled by DeleteWorkspaceFlow, which mounts when this is set. + setIsDeleteWorkspaceFlowVisible(true); + }, disabled: isLoadingBill, shouldShowLoadingSpinnerIcon: isLoadingBill, - shouldCloseModalOnSelect: !shouldCalculateBillNewDot(currentUserPersonalDetails.accountID, account?.canDowngrade, policies) || wouldBlockDeletion, + shouldCloseModalOnSelect: !shouldCalculateBillNewDot || wouldBlockDeletion, }); } const isCurrentUserAdmin = policy?.employeeList?.[currentUserPersonalDetails?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN; @@ -631,7 +493,14 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const modals = ( <> - {outstandingBalanceModal} + {isDeleteWorkspaceFlowVisible && !!policyID && ( + setIsDeleteWorkspaceFlowVisible(false)} + onDeleteComplete={goBackFromInvalidPolicy} + /> + )} {!!pendingRulesDocumentFile && ( void; + + /** Called when the workspace has been deleted (optimistically while offline, or after a successful online delete) */ + onDeleteComplete?: () => void; }; /** @@ -45,7 +48,7 @@ type DeleteWorkspaceFlowProps = { * On mount (once the data is ready) it runs the pre-deletion checks (Invoicify block, outstanding balance, * bill calculation for the last paid workspace) and then shows the delete confirmation modal. */ -function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { +function DeleteWorkspaceFlow({policyID, onDismiss, onDeleteComplete}: DeleteWorkspaceFlowProps) { const {translate, localeCompare} = useLocalize(); const styles = useThemeStyles(); const {isOffline} = useNetwork(); @@ -105,9 +108,18 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { const shouldCalculateBillNewDot = !!canDowngrade && ownedPaidPoliciesCounts?.total === 1; const {shouldBlockDeletion, outstandingBalanceModal} = useOutstandingBalanceGuard(ownedPaidPoliciesCounts?.active ?? 0, onDismiss); + const hideDeleteWorkspaceErrorModal = useCallback(() => { + dismissWorkspaceError(policyID, policy?.pendingAction); + }, [policyID, policy?.pendingAction]); + + const dismissDeleteWorkspaceFlow = useCallback(() => { + hideDeleteWorkspaceErrorModal(); + onDismiss(); + }, [hideDeleteWorkspaceErrorModal, onDismiss]); + const showDeleteWorkspaceErrorModal = useCallback(() => { if (!isFocused) { - onDismiss(); + dismissDeleteWorkspaceFlow(); return; } @@ -119,7 +131,7 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { html={translate('workspace.common.deleteOpenExpensifyCardsError')} onConciergeLinkPress={() => { closeModal(); - onDismiss(); + dismissDeleteWorkspaceFlow(); }} /> @@ -129,9 +141,30 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { success: false, shouldHandleNavigationBack: false, }).then(() => { - onDismiss(); + dismissDeleteWorkspaceFlow(); }); - }, [closeModal, isFocused, onDismiss, showConfirmModal, styles.flexRow, styles.renderHTML, translate]); + }, [closeModal, dismissDeleteWorkspaceFlow, isFocused, showConfirmModal, styles.flexRow, styles.renderHTML, translate]); + + const showGenericDeleteWorkspaceErrorModal = useCallback( + (errorMessage: string) => { + if (!isFocused) { + dismissDeleteWorkspaceFlow(); + return; + } + + showConfirmModal({ + title: translate('workspace.common.delete'), + prompt: errorMessage, + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + success: false, + shouldHandleNavigationBack: false, + }).then(() => { + dismissDeleteWorkspaceFlow(); + }); + }, + [dismissDeleteWorkspaceFlow, isFocused, showConfirmModal, translate], + ); // Always invoked after a re-render (from the start effect below for normal deletes, or from usePayAndDowngrade for billed deletes), // so the workspace being deleted and its derived data are read from the latest state. @@ -174,6 +207,7 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { if (hasDeleteWorkspaceExpensifyCardsError) { showDeleteWorkspaceErrorModal(); } else if (isOffline) { + onDeleteComplete?.(); onDismiss(); } }); @@ -225,8 +259,25 @@ function DeleteWorkspaceFlow({policyID, onDismiss}: DeleteWorkspaceFlowProps) { return; } + if (policyLatestErrorMessage) { + showGenericDeleteWorkspaceErrorModal(policyLatestErrorMessage); + return; + } + + onDeleteComplete?.(); onDismiss(); - }, [isOffline, isPendingDelete, prevIsPendingDelete, policyLatestErrorMessage, hasExpensifyCardsEnabledOnWorkspace, closeModal, onDismiss, showDeleteWorkspaceErrorModal]); + }, [ + isOffline, + isPendingDelete, + prevIsPendingDelete, + policyLatestErrorMessage, + hasExpensifyCardsEnabledOnWorkspace, + closeModal, + onDeleteComplete, + onDismiss, + showDeleteWorkspaceErrorModal, + showGenericDeleteWorkspaceErrorModal, + ]); return outstandingBalanceModal; } From fdf73b34dfce9ce2d7e476258e58c36fd96837ff Mon Sep 17 00:00:00 2001 From: staszekscp Date: Mon, 22 Jun 2026 10:53:14 +0200 Subject: [PATCH 18/21] Fix prettier --- src/pages/workspace/WorkspaceOverviewPage.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 3d21d9d94d24..1d62eeca909c 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -53,14 +53,7 @@ import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/crea import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; -import { - canEditWorkspaceSettings, - getRulesDocumentSourceURL, - getUserFriendlyWorkspaceType, - goBackFromInvalidPolicy, - isPendingDeletePolicy, - isPolicyOwner, -} from '@libs/PolicyUtils'; +import {canEditWorkspaceSettings, getRulesDocumentSourceURL, getUserFriendlyWorkspaceType, goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyOwner} from '@libs/PolicyUtils'; import {formatAddressToString} from '@libs/ReportActionsUtils'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import shouldRenderTransferOwnerButton from '@libs/shouldRenderTransferOwnerButton'; From c8dd6cac91ebbd60224362c8c9939188a5266be9 Mon Sep 17 00:00:00 2001 From: staszekscp Date: Mon, 22 Jun 2026 13:19:21 +0200 Subject: [PATCH 19/21] Fix ESLint --- src/pages/workspace/WorkspaceOverviewPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 1d62eeca909c..a740e656ac83 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -58,7 +58,6 @@ import {formatAddressToString} from '@libs/ReportActionsUtils'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import shouldRenderTransferOwnerButton from '@libs/shouldRenderTransferOwnerButton'; import StringUtils from '@libs/StringUtils'; -import {isSubscriptionTypeOfInvoicing, shouldCalculateBillNewDot} from '@libs/SubscriptionUtils'; import {getLeaveWorkspaceConfirmationPrompt} from '@libs/WorkspacesSettingsUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; From b603369f7fc59c30624c92e8713d625099fe0d9b Mon Sep 17 00:00:00 2001 From: staszekscp Date: Mon, 22 Jun 2026 15:07:00 +0200 Subject: [PATCH 20/21] Apply changes --- src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx index b385167c0547..e74663218414 100644 --- a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx +++ b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx @@ -98,7 +98,7 @@ function DeleteWorkspaceFlow({policyID, onDismiss, onDeleteComplete}: DeleteWork !isEmptyObject(cardsList) || // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ((policy?.areExpensifyCardsEnabled || policy?.areCompanyCardsEnabled) && policy?.policyAccountID); - const hasExpensifyCardsEnabledOnWorkspace = !!policy?.areExpensifyCardsEnabled && !!policy?.policyAccountID; + const hasExpensifyCardsEnabledOnWorkspace = !!policy?.areExpensifyCardsEnabled && !!policy?.policyAccountID && !isEmptyObject(cardsList); const hasDeleteWorkspaceExpensifyCardsError = !!hasExpensifyCardsEnabledOnWorkspace && !!isOffline; const policyLatestErrorMessage = getLatestErrorMessage(policy); @@ -205,6 +205,7 @@ function DeleteWorkspaceFlow({policyID, onDismiss, onDeleteComplete}: DeleteWork }); if (hasDeleteWorkspaceExpensifyCardsError) { + closeModal(); showDeleteWorkspaceErrorModal(); } else if (isOffline) { onDeleteComplete?.(); From f5c663db88c0c9615c11709fd9790af2c6e93fe8 Mon Sep 17 00:00:00 2001 From: staszekscp Date: Mon, 22 Jun 2026 15:13:38 +0200 Subject: [PATCH 21/21] Fix --- src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx index e74663218414..864230ce46e4 100644 --- a/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx +++ b/src/pages/workspace/deleteWorkspace/DeleteWorkspaceFlow.tsx @@ -205,9 +205,9 @@ function DeleteWorkspaceFlow({policyID, onDismiss, onDeleteComplete}: DeleteWork }); if (hasDeleteWorkspaceExpensifyCardsError) { - closeModal(); showDeleteWorkspaceErrorModal(); } else if (isOffline) { + closeModal(); onDeleteComplete?.(); onDismiss(); }