From f037fc3d1b67a662594ea0c6d0976c48061eb7d4 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 15 Jul 2026 16:50:55 -0600 Subject: [PATCH 1/4] Allow clearing the Default Vendor by re-selecting it Tapping the currently-selected vendor on any accounting integration's Default Vendor selector now clears it. Previously the guarded update call skipped this case, leaving no path to unset the field from the UI. QBO and QBD send CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE; Sage Intacct, Certinia, and NetSuite send an empty string. Fixes https://github.com/Expensify/Expensify/issues/659748 --- .../accounting/certinia/export/CertiniaDefaultVendorPage.tsx | 5 +++-- .../intacct/export/DynamicSageIntacctDefaultVendorPage.tsx | 5 ++--- .../export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx | 5 +++-- ...ickbooksDesktopNonReimbursableDefaultVendorSelectPage.tsx | 5 ++--- .../qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx | 5 ++--- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx b/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx index ce97572045f2..8b8759a442e9 100644 --- a/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx +++ b/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx @@ -54,8 +54,9 @@ function CertiniaDefaultVendorPage({policy}: WithPolicyConnectionsProps) { ); const selectVendor = (row: VendorListItem) => { - if (row.value !== exportConfig?.vendorAccount && policyID) { - updateFinancialForceDefaultVendor(policyID, row.value, exportConfig?.vendorAccount ?? null); + if (policyID) { + const newValue = row.value === exportConfig?.vendorAccount ? '' : row.value; + updateFinancialForceDefaultVendor(policyID, newValue, exportConfig?.vendorAccount ?? null); } Navigation.goBack(backPath); }; diff --git a/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx b/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx index d56e2523a3ab..5b389050f43c 100644 --- a/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx @@ -77,9 +77,8 @@ function DynamicSageIntacctDefaultVendorPage() { const updateDefaultVendor = useCallback( ({value}: SelectorType) => { - if (value !== defaultVendor) { - updateSageIntacctDefaultVendor(policyID, settingName, value, defaultVendor); - } + const newValue = value === defaultVendor ? '' : value; + updateSageIntacctDefaultVendor(policyID, settingName, newValue, defaultVendor); goBack(); }, [defaultVendor, policyID, settingName, goBack], diff --git a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx index ae9a97b5ba98..5e0e3d4b517d 100644 --- a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx @@ -51,8 +51,9 @@ function DynamicNetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConne const updateDefaultVendor = useCallback( ({value}: SelectorType) => { - if (config?.defaultVendor !== value && policyID) { - updateNetSuiteDefaultVendor(policyID, value, config?.defaultVendor); + if (policyID) { + const newValue = value === config?.defaultVendor ? '' : value; + updateNetSuiteDefaultVendor(policyID, newValue, config?.defaultVendor); } goBack(); }, diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopNonReimbursableDefaultVendorSelectPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopNonReimbursableDefaultVendorSelectPage.tsx index 63ed6500c4f4..3737b8038906 100644 --- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopNonReimbursableDefaultVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopNonReimbursableDefaultVendorSelectPage.tsx @@ -49,9 +49,8 @@ function QuickbooksDesktopNonReimbursableDefaultVendorSelectPage({policy}: WithP const selectVendor = useCallback( (row: CardListItem) => { - if (row.value !== nonReimbursableBillDefaultVendor) { - updateQuickbooksDesktopNonReimbursableBillDefaultVendor(policyID, row.value, nonReimbursableBillDefaultVendor); - } + const newValue = row.value === nonReimbursableBillDefaultVendor ? (CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE as string) : row.value; + updateQuickbooksDesktopNonReimbursableBillDefaultVendor(policyID, newValue, nonReimbursableBillDefaultVendor); Navigation.goBack(); }, [nonReimbursableBillDefaultVendor, policyID], diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx index f8cd812f8c64..83e313990404 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx @@ -59,9 +59,8 @@ function QuickbooksNonReimbursableVendorSelectPage({policy, configKey, updateVen })) ?? []; const selectVendor = (row: CardListItem) => { - if (row.value !== currentVendor) { - updateVendor(policyID, row.value, currentVendor); - } + const newValue = row.value === currentVendor ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : row.value; + updateVendor(policyID, newValue, currentVendor); Navigation.goBack(); }; From 9c7dcfdcec91641553caa20529242fcfe360ad43 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 15 Jul 2026 17:39:05 -0600 Subject: [PATCH 2/4] Replace bare '' with named CLEAR_DEFAULT_VENDOR constant --- .../accounting/certinia/export/CertiniaDefaultVendorPage.tsx | 5 ++++- .../intacct/export/DynamicSageIntacctDefaultVendorPage.tsx | 5 ++++- .../export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx b/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx index 8b8759a442e9..1aa9dfdeb61c 100644 --- a/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx +++ b/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx @@ -26,6 +26,9 @@ type VendorListItem = ListItem & { value: string; }; +// Certinia's vendorAccount field holds a raw vendor ID; sending this empty string clears the default. +const CLEAR_DEFAULT_VENDOR = ''; + function CertiniaDefaultVendorPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -55,7 +58,7 @@ function CertiniaDefaultVendorPage({policy}: WithPolicyConnectionsProps) { const selectVendor = (row: VendorListItem) => { if (policyID) { - const newValue = row.value === exportConfig?.vendorAccount ? '' : row.value; + const newValue = row.value === exportConfig?.vendorAccount ? CLEAR_DEFAULT_VENDOR : row.value; updateFinancialForceDefaultVendor(policyID, newValue, exportConfig?.vendorAccount ?? null); } Navigation.goBack(backPath); diff --git a/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx b/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx index 5b389050f43c..a06eaff1a59b 100644 --- a/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx @@ -30,6 +30,9 @@ import {useRoute} from '@react-navigation/native'; import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; +// Sage Intacct's default-vendor fields hold a raw vendor ID; sending this empty string clears the default. +const CLEAR_DEFAULT_VENDOR = ''; + function DynamicSageIntacctDefaultVendorPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -77,7 +80,7 @@ function DynamicSageIntacctDefaultVendorPage() { const updateDefaultVendor = useCallback( ({value}: SelectorType) => { - const newValue = value === defaultVendor ? '' : value; + const newValue = value === defaultVendor ? CLEAR_DEFAULT_VENDOR : value; updateSageIntacctDefaultVendor(policyID, settingName, newValue, defaultVendor); goBack(); }, diff --git a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx index 5e0e3d4b517d..9133c7f755c7 100644 --- a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx @@ -28,6 +28,9 @@ import type SCREENS from '@src/SCREENS'; import {useRoute} from '@react-navigation/native'; import React, {useCallback, useMemo} from 'react'; +// NetSuite's defaultVendor field holds a raw vendor ID; sending this empty string clears the default. +const CLEAR_DEFAULT_VENDOR = ''; + function DynamicNetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -52,7 +55,7 @@ function DynamicNetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConne const updateDefaultVendor = useCallback( ({value}: SelectorType) => { if (policyID) { - const newValue = value === config?.defaultVendor ? '' : value; + const newValue = value === config?.defaultVendor ? CLEAR_DEFAULT_VENDOR : value; updateNetSuiteDefaultVendor(policyID, newValue, config?.defaultVendor); } goBack(); From c429fc2e87c2c8ab587d34bfbab7fa4dcb90ed1c Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 16 Jul 2026 10:50:05 -0600 Subject: [PATCH 3/4] Reduce scope: toggle-to-clear only for CC/DC export paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blank default vendor has a defined UX only on the CC/DC export path (falls back to "Credit Card Misc"). For Vendor Bill export, blank falls back to the report submitter and is set by disabling the parent toggle, so re-tapping to clear is out of scope. Per team alignment on https://expensify.slack.com/archives/C03TME82F/p1784118449309309, the PR now covers: - QBO shared page: gated on configKey === 'nonReimbursableCreditCardDefaultVendor' - Sage Intacct: gated on settingName === NON_REIMBURSABLE_CREDIT_CARD_VENDOR Reverts the QBD, NetSuite, and Certinia changes — those integrations only support Vendor Bill export, not CC/DC. --- .../certinia/export/CertiniaDefaultVendorPage.tsx | 8 ++------ .../export/DynamicSageIntacctDefaultVendorPage.tsx | 14 +++++++++++--- ...namicNetSuiteExportExpensesVendorSelectPage.tsx | 8 ++------ ...sktopNonReimbursableDefaultVendorSelectPage.tsx | 5 +++-- .../QuickbooksNonReimbursableVendorSelectPage.tsx | 12 ++++++++++-- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx b/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx index 1aa9dfdeb61c..ce97572045f2 100644 --- a/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx +++ b/src/pages/workspace/accounting/certinia/export/CertiniaDefaultVendorPage.tsx @@ -26,9 +26,6 @@ type VendorListItem = ListItem & { value: string; }; -// Certinia's vendorAccount field holds a raw vendor ID; sending this empty string clears the default. -const CLEAR_DEFAULT_VENDOR = ''; - function CertiniaDefaultVendorPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -57,9 +54,8 @@ function CertiniaDefaultVendorPage({policy}: WithPolicyConnectionsProps) { ); const selectVendor = (row: VendorListItem) => { - if (policyID) { - const newValue = row.value === exportConfig?.vendorAccount ? CLEAR_DEFAULT_VENDOR : row.value; - updateFinancialForceDefaultVendor(policyID, newValue, exportConfig?.vendorAccount ?? null); + if (row.value !== exportConfig?.vendorAccount && policyID) { + updateFinancialForceDefaultVendor(policyID, row.value, exportConfig?.vendorAccount ?? null); } Navigation.goBack(backPath); }; diff --git a/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx b/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx index a06eaff1a59b..577ac9222985 100644 --- a/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/DynamicSageIntacctDefaultVendorPage.tsx @@ -78,13 +78,21 @@ function DynamicSageIntacctDefaultVendorPage() { [translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal, isReimbursable], ); + // Blank vendor only has a defined meaning for the non-reimbursable credit-card-charge path. The bill and reimbursable paths handle blank differently and re-tapping to clear them is out of scope here. + const canClearByReSelecting = settingName === CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR; + const updateDefaultVendor = useCallback( ({value}: SelectorType) => { - const newValue = value === defaultVendor ? CLEAR_DEFAULT_VENDOR : value; - updateSageIntacctDefaultVendor(policyID, settingName, newValue, defaultVendor); + if (value === defaultVendor) { + if (canClearByReSelecting) { + updateSageIntacctDefaultVendor(policyID, settingName, CLEAR_DEFAULT_VENDOR, defaultVendor); + } + } else { + updateSageIntacctDefaultVendor(policyID, settingName, value, defaultVendor); + } goBack(); }, - [defaultVendor, policyID, settingName, goBack], + [defaultVendor, policyID, settingName, goBack, canClearByReSelecting], ); const listEmptyContent = useMemo( diff --git a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx index 9133c7f755c7..ae9a97b5ba98 100644 --- a/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/netsuite/export/DynamicNetSuiteExportExpensesVendorSelectPage.tsx @@ -28,9 +28,6 @@ import type SCREENS from '@src/SCREENS'; import {useRoute} from '@react-navigation/native'; import React, {useCallback, useMemo} from 'react'; -// NetSuite's defaultVendor field holds a raw vendor ID; sending this empty string clears the default. -const CLEAR_DEFAULT_VENDOR = ''; - function DynamicNetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConnectionsProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -54,9 +51,8 @@ function DynamicNetSuiteExportExpensesVendorSelectPage({policy}: WithPolicyConne const updateDefaultVendor = useCallback( ({value}: SelectorType) => { - if (policyID) { - const newValue = value === config?.defaultVendor ? CLEAR_DEFAULT_VENDOR : value; - updateNetSuiteDefaultVendor(policyID, newValue, config?.defaultVendor); + if (config?.defaultVendor !== value && policyID) { + updateNetSuiteDefaultVendor(policyID, value, config?.defaultVendor); } goBack(); }, diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopNonReimbursableDefaultVendorSelectPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopNonReimbursableDefaultVendorSelectPage.tsx index 3737b8038906..63ed6500c4f4 100644 --- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopNonReimbursableDefaultVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopNonReimbursableDefaultVendorSelectPage.tsx @@ -49,8 +49,9 @@ function QuickbooksDesktopNonReimbursableDefaultVendorSelectPage({policy}: WithP const selectVendor = useCallback( (row: CardListItem) => { - const newValue = row.value === nonReimbursableBillDefaultVendor ? (CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE as string) : row.value; - updateQuickbooksDesktopNonReimbursableBillDefaultVendor(policyID, newValue, nonReimbursableBillDefaultVendor); + if (row.value !== nonReimbursableBillDefaultVendor) { + updateQuickbooksDesktopNonReimbursableBillDefaultVendor(policyID, row.value, nonReimbursableBillDefaultVendor); + } Navigation.goBack(); }, [nonReimbursableBillDefaultVendor, policyID], diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx index 83e313990404..0aa0195b1938 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx @@ -58,9 +58,17 @@ function QuickbooksNonReimbursableVendorSelectPage({policy, configKey, updateVen isSelected: vendor.id === currentVendor, })) ?? []; + // Blank vendor only has a defined meaning for the CC/DC export path (falls back to "Credit Card Misc"). For Vendor Bill export, blank falls back to the report submitter — but the user can already leave that setting disabled from the parent page, so re-tapping to clear is out of scope here. + const canClearByReSelecting = configKey === 'nonReimbursableCreditCardDefaultVendor'; + const selectVendor = (row: CardListItem) => { - const newValue = row.value === currentVendor ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : row.value; - updateVendor(policyID, newValue, currentVendor); + if (row.value === currentVendor) { + if (canClearByReSelecting) { + updateVendor(policyID, CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE, currentVendor); + } + } else { + updateVendor(policyID, row.value, currentVendor); + } Navigation.goBack(); }; From 400be4703e5f397c254d7796a7f0a177793e61de Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Fri, 17 Jul 2026 08:46:31 -0600 Subject: [PATCH 4/4] Use CONST.QUICKBOOKS_CONFIG constant for QBO gate --- .../qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx index 0aa0195b1938..fe3f564fa5a1 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksNonReimbursableVendorSelectPage.tsx @@ -59,7 +59,7 @@ function QuickbooksNonReimbursableVendorSelectPage({policy, configKey, updateVen })) ?? []; // Blank vendor only has a defined meaning for the CC/DC export path (falls back to "Credit Card Misc"). For Vendor Bill export, blank falls back to the report submitter — but the user can already leave that setting disabled from the parent page, so re-tapping to clear is out of scope here. - const canClearByReSelecting = configKey === 'nonReimbursableCreditCardDefaultVendor'; + const canClearByReSelecting = configKey === CONST.QUICKBOOKS_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_DEFAULT_VENDOR; const selectVendor = (row: CardListItem) => { if (row.value === currentVendor) {