From cbda4ce552fe38c0edc754cf13b16c8ccbac419a Mon Sep 17 00:00:00 2001 From: "thelullabyy (via MelvinBot)" Date: Tue, 18 Aug 2026 06:38:06 +0000 Subject: [PATCH 1/2] Gate Vendors More features row on a supported integration and explain the lock Co-authored-by: thelullabyy --- src/languages/de.ts | 7 +++- src/languages/el.ts | 2 + src/languages/en.ts | 2 + src/languages/es.ts | 7 +++- src/languages/fr.ts | 7 +++- src/languages/it.ts | 7 +++- src/languages/ja.ts | 7 +++- src/languages/nl.ts | 7 +++- src/languages/pl.ts | 7 +++- src/languages/pt-BR.ts | 7 +++- src/languages/zh-hans.ts | 7 +++- .../WorkspaceMoreFeaturesPage/index.tsx | 23 +++++++++-- tests/ui/WorkspaceMoreFeaturesPageTest.tsx | 41 +++++++++++++++++++ 13 files changed, 118 insertions(+), 13 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 8c05f8030e54..828666829336 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6359,7 +6359,12 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU defaultHourlyRate: 'Standardstundensatz', }, hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `Um HR zu deaktivieren, trennen Sie bitte zuerst ${integration} von diesem Workspace.`}, - vendors: {title: 'Lieferanten', subtitle: 'Ordnen Sie Kartenausgaben den aus Ihrer Buchhaltungssoftware importierten Lieferanten zu.'}, + vendors: { + title: 'Lieferanten', + subtitle: 'Ordnen Sie Kartenausgaben den aus Ihrer Buchhaltungssoftware importierten Lieferanten zu.', + disabledTitle: 'Nicht so schnell...', + disabledMessage: 'Um diese Funktion zu aktivieren oder zu deaktivieren, müssen Sie Ihre Buchhaltungsimporteinstellungen ändern.', + }, }, reports: { reportsCustomTitleExamples: 'Beispiele:', diff --git a/src/languages/el.ts b/src/languages/el.ts index 31947dc2388e..b40eaf0a9ee8 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -6501,6 +6501,8 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ vendors: { title: 'Προμηθευτές', subtitle: 'Ταιριάξτε τα έξοδα κάρτας με προμηθευτές που έχουν εισαχθεί από το λογιστικό σας λογισμικό.', + disabledTitle: 'Όχι τόσο γρήγορα...', + disabledMessage: 'Για να ενεργοποιήσετε ή να απενεργοποιήσετε αυτήν τη λειτουργία, θα πρέπει να αλλάξετε τις ρυθμίσεις εισαγωγής λογιστικής.', }, }, reports: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 8d96a8e32945..522c09445897 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6409,6 +6409,8 @@ const translations = { vendors: { title: 'Vendors', subtitle: 'Match card expenses to vendors imported from your accounting software.', + disabledTitle: 'Not so fast...', + disabledMessage: "To enable or disable this feature, you'll need to change your accounting import settings.", }, reportFields: { title: 'Report fields', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0ee050a29e06..0231f79c1e38 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6197,7 +6197,12 @@ ${amount} para ${merchant} - ${date}`, title: 'Impuestos', subtitle: 'Documenta y reclama los impuestos aplicables.', }, - vendors: {title: 'Proveedores', subtitle: 'Asocia los gastos de tarjeta con los proveedores importados de tu software de contabilidad.'}, + vendors: { + title: 'Proveedores', + subtitle: 'Asocia los gastos de tarjeta con los proveedores importados de tu software de contabilidad.', + disabledTitle: 'No tan rápido...', + disabledMessage: 'Para activar o desactivar esta función, deberás cambiar la configuración de importación de contabilidad.', + }, reportFields: { title: 'Campos de informes', subtitle: 'Configura campos personalizados para los gastos.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index cfb3837132b9..899013369a08 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6382,7 +6382,12 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. defaultHourlyRate: 'Taux horaire par défaut', }, hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `Pour désactiver les RH, veuillez d'abord déconnecter ${integration} de cet espace de travail.`}, - vendors: {title: 'Fournisseurs', subtitle: 'Faites correspondre les dépenses de carte aux fournisseurs importés depuis votre logiciel de comptabilité.'}, + vendors: { + title: 'Fournisseurs', + subtitle: 'Faites correspondre les dépenses de carte aux fournisseurs importés depuis votre logiciel de comptabilité.', + disabledTitle: 'Pas si vite...', + disabledMessage: 'Pour activer ou désactiver cette fonctionnalité, vous devrez modifier vos paramètres d’importation comptable.', + }, }, reports: { reportsCustomTitleExamples: 'Exemples :', diff --git a/src/languages/it.ts b/src/languages/it.ts index 9325e5428359..fffc1f5c8be8 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6334,7 +6334,12 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. defaultHourlyRate: 'Tariffa oraria predefinita', }, hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `Per disattivare HR, scollega prima ${integration} da questo workspace.`}, - vendors: {title: 'Fornitori', subtitle: 'Abbina le spese della carta ai fornitori importati dal tuo software di contabilità.'}, + vendors: { + title: 'Fornitori', + subtitle: 'Abbina le spese della carta ai fornitori importati dal tuo software di contabilità.', + disabledTitle: 'Non così in fretta...', + disabledMessage: 'Per abilitare o disabilitare questa funzione, dovrai modificare le impostazioni di importazione della contabilità.', + }, }, reports: { reportsCustomTitleExamples: 'Esempi:', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 239a3821249b..b9dbc5ffd9d1 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6253,7 +6253,12 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO defaultHourlyRate: 'デフォルトの時給率', }, hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `HR を無効にするには、まずこのワークスペースから ${integration} を切断してください。`}, - vendors: {title: 'ベンダー', subtitle: '会計ソフトからインポートした取引先にカード経費を照合します。'}, + vendors: { + title: 'ベンダー', + subtitle: '会計ソフトからインポートした取引先にカード経費を照合します。', + disabledTitle: 'ちょっと待ってください...', + disabledMessage: 'この機能を有効または無効にするには、会計インポート設定を変更する必要があります。', + }, }, reports: { reportsCustomTitleExamples: '例:', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 4c29626b285b..5c487d6f420d 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6320,7 +6320,12 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ defaultHourlyRate: 'Standaard uurtarief', }, hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `Om HR uit te schakelen, koppel eerst ${integration} los van deze workspace.`}, - vendors: {title: 'Leveranciers', subtitle: 'Koppel kaartuitgaven aan leveranciers die zijn geïmporteerd uit je boekhoudsoftware.'}, + vendors: { + title: 'Leveranciers', + subtitle: 'Koppel kaartuitgaven aan leveranciers die zijn geïmporteerd uit je boekhoudsoftware.', + disabledTitle: 'Niet zo snel...', + disabledMessage: 'Om deze functie in of uit te schakelen, moet je je boekhoudimportinstellingen wijzigen.', + }, }, reports: { reportsCustomTitleExamples: 'Voorbeelden:', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index c4c917519468..0a093db9a1ac 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6299,7 +6299,12 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy defaultHourlyRate: 'Domyślna stawka godzinowa', }, hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `Aby wyłączyć HR, najpierw odłącz ${integration} od tego workspace’a.`}, - vendors: {title: 'Dostawcy', subtitle: 'Dopasuj wydatki z karty do kontrahentów zaimportowanych z twojego oprogramowania księgowego.'}, + vendors: { + title: 'Dostawcy', + subtitle: 'Dopasuj wydatki z karty do kontrahentów zaimportowanych z twojego oprogramowania księgowego.', + disabledTitle: 'Nie tak szybko...', + disabledMessage: 'Aby włączyć lub wyłączyć tę funkcję, musisz zmienić ustawienia importu księgowego.', + }, }, reports: { reportsCustomTitleExamples: 'Przykłady:', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index dbe285463584..9a290e792dbe 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6320,7 +6320,12 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS defaultHourlyRate: 'Taxa horária padrão', }, hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `Para desativar RH, desconecte primeiro a integração ${integration} deste workspace.`}, - vendors: {title: 'Fornecedores', subtitle: 'Combine as despesas do cartão com fornecedores importados do seu software de contabilidade.'}, + vendors: { + title: 'Fornecedores', + subtitle: 'Combine as despesas do cartão com fornecedores importados do seu software de contabilidade.', + disabledTitle: 'Calma lá...', + disabledMessage: 'Para ativar ou desativar este recurso, você precisará alterar suas configurações de importação contábil.', + }, }, reports: { reportsCustomTitleExamples: 'Exemplos:', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 726c8f95adf9..c5b3dcf15d85 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6105,7 +6105,12 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM defaultHourlyRate: '默认时薪', }, hrWarningModal: {disconnectText: ({integration}: {integration: string}) => `若要禁用人力资源功能,请先将此工作区与 ${integration} 断开连接。`}, - vendors: {title: '供应商', subtitle: '将卡片报销与从您的会计软件导入的供应商进行匹配。'}, + vendors: { + title: '供应商', + subtitle: '将卡片报销与从您的会计软件导入的供应商进行匹配。', + disabledTitle: '别急...', + disabledMessage: '要启用或禁用此功能,您需要更改会计导入设置。', + }, }, reports: { reportsCustomTitleExamples: '示例:', diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx index 68ff5e96502b..972f03136fa0 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx @@ -31,6 +31,7 @@ import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import { arePolicyRulesEnabled, canPolicyAccessFeature, + getConnectedIntegration, getDistanceRateCustomUnit, getPerDiemCustomUnit, hasAccountingConnections, @@ -158,9 +159,14 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro // connection, and when the data has already been fetched. usePolicyConnectionsPrefetch(policy, true); - // Beta members see the row on any workspace so they can tell the feature exists. Everyone else - // only sees it once a connection actually scopes the vendor field, which post-GA means QBO. - const shouldShowVendorsFeature = isVendorMatchingEnabled || hasVendorFeature(policy, isVendorMatchingEnabled); + // Visibility is gated on a supported integration (QBO / Xero / Sage Intacct) being connected, + // not on the export config actually scoping vendors. That way beta members on a supported + // workspace still see the row so they can discover the feature even when the row is locked OFF + // (export config not yet set). NetSuite / QuickBooks Desktop / no connection hide the row. + // `hasVendorFeature` stays as the narrower `isActive` predicate (is the export config scoping + // vendors right now), so it can't double as the visibility gate. + const vendorMatchingConnection = getConnectedIntegration(policy, [CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]); + const shouldShowVendorsFeature = isVendorMatchingEnabled && !!vendorMatchingConnection; const warnAccountingManagesOrganizeFeature = async () => { if (!hasAccountingConnection || !policyID) { @@ -178,6 +184,15 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); }; + const warnVendorsManagedByAccounting = async () => { + await showConfirmModal({ + title: translate('workspace.moreFeatures.vendors.disabledTitle'), + prompt: translate('workspace.moreFeatures.vendors.disabledMessage'), + confirmText: translate('common.buttonConfirm'), + shouldShowCancelButton: false, + }); + }; + const warnDisconnectAccountingFirst = async () => { if (!hasAccountingConnection || !policyID) { return; @@ -476,7 +491,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro // get the read-only modal via withReadOnlyFallback(). Row-body navigation stays active when the // feature is available. This will be unlocked in the follow-up PR that wires up the toggle. disabled - disabledAction={withReadOnlyFallback()} + disabledAction={withReadOnlyFallback(warnVendorsManagedByAccounting)} onToggle={() => {}} onPress={() => { if (!policyID) { diff --git a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx index 3b60e9bdffb0..fe0a9b4c24b9 100644 --- a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx +++ b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx @@ -407,4 +407,45 @@ describe('WorkspaceMoreFeaturesPage', () => { expect(navigateToConciergeChatSpy).not.toHaveBeenCalled(); }); }); + + describe('Vendors row (visibility gated on a supported integration)', () => { + const renderWithVendorMatching = async (connections: Record) => { + await TestHelper.signInWithTestUser(); + await act(async () => { + await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.VENDOR_MATCHING]); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID, connections})); + }); + renderPage({policyID: POLICY_ID}); + await waitForBatchedUpdatesWithAct(); + }; + + it('hides the Vendors row when no accounting connection is present', async () => { + await renderWithVendorMatching({}); + expect(screen.queryByRole(CONST.ROLE.SWITCH, {name: new RegExp(escapeRegExp(TestHelper.translateLocal('workspace.moreFeatures.vendors.subtitle')), 'i')})).toBeNull(); + }); + + it('hides the Vendors row for an unsupported integration (NetSuite)', async () => { + await renderWithVendorMatching({[CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: {config: {}}}); + expect(screen.queryByRole(CONST.ROLE.SWITCH, {name: new RegExp(escapeRegExp(TestHelper.translateLocal('workspace.moreFeatures.vendors.subtitle')), 'i')})).toBeNull(); + }); + + it('shows the Vendors row locked for a supported integration and explains the lock on press', async () => { + await renderWithVendorMatching({ + [CONST.POLICY.CONNECTIONS.NAME.QBO]: {config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}}, + }); + + fireEvent.press(await findLockedSwitch('workspace.moreFeatures.vendors.subtitle')); + + await waitFor(() => { + expect(screen.getByText(TestHelper.translateLocal('workspace.moreFeatures.vendors.disabledMessage'))).toBeOnTheScreen(); + }); + }); + + it('shows the Vendors row for a supported integration even when its export config does not scope vendors (discovery state)', async () => { + await renderWithVendorMatching({ + [CONST.POLICY.CONNECTIONS.NAME.QBO]: {config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL}}, + }); + await expect(findLockedSwitch('workspace.moreFeatures.vendors.subtitle')).resolves.toBeOnTheScreen(); + }); + }); }); From b17a19afce5cba443640c3124b48b08a4bc8c983 Mon Sep 17 00:00:00 2001 From: "thelullabyy (via MelvinBot)" Date: Wed, 19 Aug 2026 05:25:19 +0000 Subject: [PATCH 2/2] Keep Vendors row visible for GA QBO regardless of vendorMatching beta Co-authored-by: thelullabyy --- .../WorkspaceMoreFeaturesPage/index.tsx | 12 +++++--- tests/ui/WorkspaceMoreFeaturesPageTest.tsx | 29 +++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx index 972f03136fa0..7c4770afbc3e 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx @@ -160,13 +160,17 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro usePolicyConnectionsPrefetch(policy, true); // Visibility is gated on a supported integration (QBO / Xero / Sage Intacct) being connected, - // not on the export config actually scoping vendors. That way beta members on a supported - // workspace still see the row so they can discover the feature even when the row is locked OFF - // (export config not yet set). NetSuite / QuickBooks Desktop / no connection hide the row. + // not on the export config actually scoping vendors. That way members on a supported workspace + // still see the row so they can discover the feature even when the row is locked OFF (export + // config not yet set). NetSuite / QuickBooks Desktop / no connection hide the row. // `hasVendorFeature` stays as the narrower `isActive` predicate (is the export config scoping // vendors right now), so it can't double as the visibility gate. + // + // Beta gating mirrors `hasVendorFeature`: QBO (R1) is GA, so a connected QBO workspace always + // sees the row regardless of the `vendorMatching` beta. Sage Intacct (R2) and Xero (R3) haven't + // reached GA, so they only show the row while the beta is enabled. const vendorMatchingConnection = getConnectedIntegration(policy, [CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]); - const shouldShowVendorsFeature = isVendorMatchingEnabled && !!vendorMatchingConnection; + const shouldShowVendorsFeature = vendorMatchingConnection === CONST.POLICY.CONNECTIONS.NAME.QBO || (isVendorMatchingEnabled && !!vendorMatchingConnection); const warnAccountingManagesOrganizeFeature = async () => { if (!hasAccountingConnection || !policyID) { diff --git a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx index fe0a9b4c24b9..a42cb7309d89 100644 --- a/tests/ui/WorkspaceMoreFeaturesPageTest.tsx +++ b/tests/ui/WorkspaceMoreFeaturesPageTest.tsx @@ -409,16 +409,18 @@ describe('WorkspaceMoreFeaturesPage', () => { }); describe('Vendors row (visibility gated on a supported integration)', () => { - const renderWithVendorMatching = async (connections: Record) => { + const renderWithVendorMatching = async (connections: Record, isBetaEnabled = true) => { await TestHelper.signInWithTestUser(); await act(async () => { - await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.VENDOR_MATCHING]); + await Onyx.merge(ONYXKEYS.BETAS, isBetaEnabled ? [CONST.BETAS.VENDOR_MATCHING] : []); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, buildPolicy({id: POLICY_ID, connections})); }); renderPage({policyID: POLICY_ID}); await waitForBatchedUpdatesWithAct(); }; + const vendorsSwitchQuery = () => screen.queryByRole(CONST.ROLE.SWITCH, {name: new RegExp(escapeRegExp(TestHelper.translateLocal('workspace.moreFeatures.vendors.subtitle')), 'i')}); + it('hides the Vendors row when no accounting connection is present', async () => { await renderWithVendorMatching({}); expect(screen.queryByRole(CONST.ROLE.SWITCH, {name: new RegExp(escapeRegExp(TestHelper.translateLocal('workspace.moreFeatures.vendors.subtitle')), 'i')})).toBeNull(); @@ -447,5 +449,28 @@ describe('WorkspaceMoreFeaturesPage', () => { }); await expect(findLockedSwitch('workspace.moreFeatures.vendors.subtitle')).resolves.toBeOnTheScreen(); }); + + // QBO R1 is GA, so a connected QBO workspace shows the row regardless of the vendorMatching beta. + it('shows the Vendors row locked ON for QBO scoping vendors even with the beta disabled (QBO is GA)', async () => { + await renderWithVendorMatching( + {[CONST.POLICY.CONNECTIONS.NAME.QBO]: {config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}}}, + false, + ); + await expect(findLockedSwitch('workspace.moreFeatures.vendors.subtitle')).resolves.toBeOnTheScreen(); + }); + + it('shows the Vendors row locked OFF for QBO not scoping vendors even with the beta disabled (discovery state, QBO is GA)', async () => { + await renderWithVendorMatching( + {[CONST.POLICY.CONNECTIONS.NAME.QBO]: {config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL}}}, + false, + ); + await expect(findLockedSwitch('workspace.moreFeatures.vendors.subtitle')).resolves.toBeOnTheScreen(); + }); + + // Sage Intacct (R2) and Xero (R3) are still beta-gated, so they stay hidden when the beta is off. + it('hides the Vendors row for a beta-gated integration (Xero) when the beta is disabled', async () => { + await renderWithVendorMatching({[CONST.POLICY.CONNECTIONS.NAME.XERO]: {config: {}}}, false); + expect(vendorsSwitchQuery()).toBeNull(); + }); }); });